amca 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amca/__init__.py +17 -0
- amca/api.py +120 -0
- amca/cli/__init__.py +1 -0
- amca/cli/amca_cli.py +190 -0
- amca/cli/amcapl_cli.py +388 -0
- amca/cli/commands/__init__.py +1 -0
- amca/cli/commands/args_edit.py +67 -0
- amca/cli/commands/completions_cmd.py +136 -0
- amca/cli/commands/config_cmd.py +270 -0
- amca/cli/commands/doctor.py +161 -0
- amca/cli/commands/root_cmd.py +102 -0
- amca/cli/commands/run_cmd.py +36 -0
- amca/cli/common.py +142 -0
- amca/cli/completions.py +588 -0
- amca/cli/prompt.py +138 -0
- amca/config/__init__.py +1 -0
- amca/config/migrate.py +152 -0
- amca/config/schema.py +206 -0
- amca/config/store.py +256 -0
- amca/core/__init__.py +1 -0
- amca/core/context.py +162 -0
- amca/core/dirparse.py +76 -0
- amca/core/logger.py +233 -0
- amca/core/paths.py +103 -0
- amca/core/proc.py +114 -0
- amca/plugins/__init__.py +1 -0
- amca/plugins/argfiles.py +46 -0
- amca/plugins/loader.py +199 -0
- amca/plugins/markers.py +170 -0
- amca/plugins/registry.py +201 -0
- amca/plugins/sources.py +191 -0
- amca-3.0.0.dist-info/METADATA +548 -0
- amca-3.0.0.dist-info/RECORD +52 -0
- amca-3.0.0.dist-info/WHEEL +5 -0
- amca-3.0.0.dist-info/entry_points.txt +3 -0
- amca-3.0.0.dist-info/licenses/LICENSE +674 -0
- amca-3.0.0.dist-info/top_level.txt +2 -0
- amca_presets/__init__.py +14 -0
- amca_presets/autoscript/__init__.py +1 -0
- amca_presets/autoscript/_impl/__init__.py +1 -0
- amca_presets/autoscript/_impl/scripts.py +61 -0
- amca_presets/autoscript/plugin.py +152 -0
- amca_presets/example/__init__.py +1 -0
- amca_presets/example/plugin.py +106 -0
- amca_presets/meson/__init__.py +1 -0
- amca_presets/meson/_impl/__init__.py +1 -0
- amca_presets/meson/_impl/args.py +145 -0
- amca_presets/meson/_impl/project.py +93 -0
- amca_presets/meson/_impl/source_cache.py +93 -0
- amca_presets/meson/_impl/steps.py +248 -0
- amca_presets/meson/meson.build.template +55 -0
- amca_presets/meson/plugin.py +97 -0
amca/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""amca — directory-aware auto-executer.
|
|
2
|
+
|
|
3
|
+
Importing this package has no side effects: no config is read, no directory is
|
|
4
|
+
scanned, no prompt is shown. Everything that touches the filesystem happens
|
|
5
|
+
inside an explicitly constructed :class:`amca.core.context.AmcaContext`.
|
|
6
|
+
|
|
7
|
+
That is a deliberate constraint, not an accident. In the previous version,
|
|
8
|
+
importing ``impl.util.globals`` walked the directory tree and could block on an
|
|
9
|
+
interactive y/n prompt, which meant ``amca --help`` prompted you before it
|
|
10
|
+
printed help. Keep this module free of work.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
__version__ = "3.0.0"
|
|
16
|
+
|
|
17
|
+
__all__ = ["__version__"]
|
amca/api.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""The plugin SDK. This module is the public, stable surface for plugin authors.
|
|
2
|
+
|
|
3
|
+
A plugin is a directory containing ``plugin.py`` (or ``init.py``, still
|
|
4
|
+
accepted) that defines one subclass of :class:`Plugin`:
|
|
5
|
+
|
|
6
|
+
from amca.api import Plugin, PluginContext
|
|
7
|
+
|
|
8
|
+
class hello(Plugin):
|
|
9
|
+
name = "hello"
|
|
10
|
+
|
|
11
|
+
def should_load(self, ctx: PluginContext) -> bool:
|
|
12
|
+
return ctx.working_dir.has_file("Makefile")
|
|
13
|
+
|
|
14
|
+
def load(self, ctx: PluginContext) -> int:
|
|
15
|
+
ctx.log.log(f"args: {ctx.args}")
|
|
16
|
+
return 0
|
|
17
|
+
|
|
18
|
+
Two things changed from the previous plugin API, both for the same reason.
|
|
19
|
+
|
|
20
|
+
First, plugins now ``import amca.api`` instead of importing from a copy of
|
|
21
|
+
amca's internals vendored into every plugin directory. Those copies existed
|
|
22
|
+
because the old amca was a frozen binary with no importable package: each
|
|
23
|
+
preset shipped its own ``amca/logger.py``, ``amca/dirparse.py`` and
|
|
24
|
+
``amca/plugin_base.py``, which then drifted from the originals. amca is a real
|
|
25
|
+
package now, so a plugin can just import it.
|
|
26
|
+
|
|
27
|
+
Second, the five positional parameters became one :class:`PluginContext`.
|
|
28
|
+
Adding a sixth piece of context used to be a breaking change for every plugin
|
|
29
|
+
in existence; now it is a new attribute. Old-style five-argument plugins are
|
|
30
|
+
still detected and adapted — see ``amca.plugins.loader``.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import abc
|
|
36
|
+
from dataclasses import dataclass
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import TYPE_CHECKING
|
|
39
|
+
|
|
40
|
+
from .core import proc
|
|
41
|
+
from .core.dirparse import DirInfo, DirParser
|
|
42
|
+
from .core.logger import Logger
|
|
43
|
+
|
|
44
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
45
|
+
from .core.context import AmcaRoot
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"DirInfo",
|
|
49
|
+
"DirParser",
|
|
50
|
+
"Logger",
|
|
51
|
+
"Plugin",
|
|
52
|
+
"PluginContext",
|
|
53
|
+
"PluginError",
|
|
54
|
+
"proc",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PluginError(RuntimeError):
|
|
59
|
+
"""Raise from a plugin to fail cleanly with a message and no traceback."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass(frozen=True, slots=True)
|
|
63
|
+
class PluginContext:
|
|
64
|
+
"""Everything a plugin is given for one invocation."""
|
|
65
|
+
|
|
66
|
+
#: Arguments routed to this plugin via its ``---name`` marker.
|
|
67
|
+
args: list[str]
|
|
68
|
+
#: Directory amca was invoked from.
|
|
69
|
+
working_dir: DirInfo
|
|
70
|
+
#: The resolved amca root, or None when the user is outside a project.
|
|
71
|
+
root: AmcaRoot | None
|
|
72
|
+
#: This plugin's private per-project directory
|
|
73
|
+
#: (``<root>/.Amca/plugins/<name>/``), or None when there is no root.
|
|
74
|
+
#: Created before the plugin runs. Put caches and per-project state here.
|
|
75
|
+
plugin_dir: Path | None
|
|
76
|
+
#: Shared directory reader — use this instead of ``Path.iterdir`` so
|
|
77
|
+
#: repeated lookups within one run stay cached.
|
|
78
|
+
dirs: DirParser
|
|
79
|
+
#: Logger honouring the user's verbosity settings.
|
|
80
|
+
log: Logger
|
|
81
|
+
#: True when the user passed ``--dry-run``. Plugins must print what they
|
|
82
|
+
#: would do and change nothing.
|
|
83
|
+
dry_run: bool = False
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def project_dir(self) -> Path:
|
|
87
|
+
"""Root directory if inside a project, else the working directory."""
|
|
88
|
+
return self.root.path if self.root is not None else self.working_dir.path
|
|
89
|
+
|
|
90
|
+
def project_dir_info(self) -> DirInfo:
|
|
91
|
+
return self.dirs.parse_dir(self.project_dir)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Plugin(abc.ABC):
|
|
95
|
+
"""Base class for plugins.
|
|
96
|
+
|
|
97
|
+
Subclasses must be constructible with no arguments. amca instantiates the
|
|
98
|
+
class once per invocation, then calls ``should_load`` and, if that returns
|
|
99
|
+
True and the plugin is enabled, ``load``.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
#: Display name. Defaults to the class name when unset. The *folder* name
|
|
103
|
+
#: is what the user types after the marker prefix; this is what appears in
|
|
104
|
+
#: log messages.
|
|
105
|
+
name: str = ""
|
|
106
|
+
|
|
107
|
+
#: Optional one-line summary shown by ``amcapl list``.
|
|
108
|
+
description: str = ""
|
|
109
|
+
|
|
110
|
+
@abc.abstractmethod
|
|
111
|
+
def should_load(self, ctx: PluginContext) -> bool:
|
|
112
|
+
"""Cheap test for whether this plugin applies here.
|
|
113
|
+
|
|
114
|
+
Must not run subprocesses, write files, or take noticeable time — it is
|
|
115
|
+
called for every enabled plugin on every invocation.
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
@abc.abstractmethod
|
|
119
|
+
def load(self, ctx: PluginContext) -> int | None:
|
|
120
|
+
"""Do the work. Return a process exit status; 0 or None means success."""
|
amca/cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Command-line entry points and shared CLI plumbing."""
|
amca/cli/amca_cli.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""``amca`` — run the plugins that apply to the current directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from ..config.schema import SCHEMA
|
|
10
|
+
from ..config.store import ConfigError
|
|
11
|
+
from ..core.context import AmcaContext
|
|
12
|
+
from ..plugins.markers import UnknownMarker, split_argv
|
|
13
|
+
from ..plugins.registry import Registry
|
|
14
|
+
from . import common
|
|
15
|
+
from .commands import args_edit, completions_cmd, config_cmd, doctor, root_cmd, run_cmd
|
|
16
|
+
|
|
17
|
+
__all__ = ["build_parser", "main"]
|
|
18
|
+
|
|
19
|
+
_GLOB_CHARS = "*?[]{}~"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _epilog(prefix: str, markers: list[str]) -> str:
|
|
23
|
+
"""Build the help epilog from the *effective* configuration.
|
|
24
|
+
|
|
25
|
+
The previous version hardcoded '---' and a made-up example. After
|
|
26
|
+
`config set plugins.marker_prefix ?` the help still told you to type
|
|
27
|
+
`---meson`, which is the one thing that no longer worked.
|
|
28
|
+
"""
|
|
29
|
+
listed = " ".join(markers) if markers else "(no plugins installed)"
|
|
30
|
+
example = (
|
|
31
|
+
f"amca {markers[0]} -s" if markers else f"amca {prefix}<plugin> <plugin args>"
|
|
32
|
+
)
|
|
33
|
+
warning = ""
|
|
34
|
+
if any(char in prefix for char in _GLOB_CHARS):
|
|
35
|
+
warning = (
|
|
36
|
+
f"\n WARNING: the marker prefix {prefix!r} contains shell glob characters.\n"
|
|
37
|
+
f" You will have to quote every marker (amca '{prefix}meson'), because the\n"
|
|
38
|
+
f" shell expands it before amca sees it. Consider a prefix like --- or +++.\n"
|
|
39
|
+
)
|
|
40
|
+
return f"""\
|
|
41
|
+
plugin passthrough:
|
|
42
|
+
Arguments after a plugin marker go to that plugin, up to the next marker.
|
|
43
|
+
|
|
44
|
+
{example}
|
|
45
|
+
|
|
46
|
+
Current marker prefix: {prefix!r}
|
|
47
|
+
Markers here: {listed}
|
|
48
|
+
{warning}
|
|
49
|
+
Markers are the prefix plus the plugin's folder name, with underscores
|
|
50
|
+
written as dashes. An unknown marker is an error, not a silently discarded
|
|
51
|
+
argument. A bare -- stops marker parsing, for values starting with the prefix:
|
|
52
|
+
|
|
53
|
+
amca config set plugins.marker_prefix -- +++
|
|
54
|
+
|
|
55
|
+
troubleshooting:
|
|
56
|
+
amca config list --origin show every setting and which layer it came from
|
|
57
|
+
amca plugins --markers the exact markers this directory accepts
|
|
58
|
+
amca doctor check tools, paths, and plugin health
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def build_parser(
|
|
63
|
+
prefix: str = "---", markers: list[str] | None = None
|
|
64
|
+
) -> argparse.ArgumentParser:
|
|
65
|
+
parser = argparse.ArgumentParser(
|
|
66
|
+
prog="amca",
|
|
67
|
+
description="Run the build/script pipeline that fits the current directory.",
|
|
68
|
+
epilog=_epilog(prefix, markers or []),
|
|
69
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
70
|
+
)
|
|
71
|
+
common.add_global_flags(parser, marker_prefix=prefix)
|
|
72
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
73
|
+
help="show what would run without running it")
|
|
74
|
+
|
|
75
|
+
sub = parser.add_subparsers(dest="command", metavar="<command>")
|
|
76
|
+
|
|
77
|
+
run_parser = sub.add_parser("run", aliases=["r"],
|
|
78
|
+
help="run applicable plugins (default)")
|
|
79
|
+
run_parser.set_defaults(handler=run_cmd.handle)
|
|
80
|
+
|
|
81
|
+
root_cmd.register(sub)
|
|
82
|
+
args_edit.register(sub)
|
|
83
|
+
config_cmd.register(sub)
|
|
84
|
+
doctor.register(sub)
|
|
85
|
+
|
|
86
|
+
plugins_parser = sub.add_parser("plugins", help="list plugins and their markers")
|
|
87
|
+
# Machine-readable forms, for shell completion. One name per line, no
|
|
88
|
+
# decoration, no headers, and never an error exit — a completion function
|
|
89
|
+
# that has to parse a table is a completion function that breaks.
|
|
90
|
+
plugins_parser.add_argument("--names", action="store_true",
|
|
91
|
+
help="print installed plugin names, one per line")
|
|
92
|
+
plugins_parser.add_argument("--markers", action="store_true",
|
|
93
|
+
help="print plugin markers, one per line")
|
|
94
|
+
plugins_parser.add_argument("--enabled-only", action="store_true",
|
|
95
|
+
help="restrict --names/--markers to enabled plugins")
|
|
96
|
+
plugins_parser.set_defaults(handler=_handle_plugins)
|
|
97
|
+
|
|
98
|
+
completions_cmd.register(sub)
|
|
99
|
+
|
|
100
|
+
return parser
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _handle_plugins(ctx: AmcaContext, args: argparse.Namespace, _: dict[str, list[str]]) -> int:
|
|
104
|
+
registry = Registry(ctx)
|
|
105
|
+
prefix = ctx.config.get_str("plugins.marker_prefix")
|
|
106
|
+
entries = registry.entries
|
|
107
|
+
|
|
108
|
+
if getattr(args, "names", False) or getattr(args, "markers", False):
|
|
109
|
+
wanted = [e for e in entries if e.enabled or not args.enabled_only]
|
|
110
|
+
for entry in wanted:
|
|
111
|
+
if args.markers:
|
|
112
|
+
print(f"{prefix}{entry.name.replace('_', '-')}")
|
|
113
|
+
else:
|
|
114
|
+
print(entry.name)
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
if not entries:
|
|
118
|
+
print(f"No plugins installed in {ctx.plugins_dir}")
|
|
119
|
+
print("Install one with: amcapl install meson")
|
|
120
|
+
return 0
|
|
121
|
+
width = max(len(entry.name) for entry in entries)
|
|
122
|
+
for entry in entries:
|
|
123
|
+
state = "enabled " if entry.enabled else "disabled"
|
|
124
|
+
print(f" {state} {entry.name:<{width}} {prefix}{entry.name.replace('_', '-')}")
|
|
125
|
+
missing = registry.missing_enabled()
|
|
126
|
+
if missing:
|
|
127
|
+
print(f"\n enabled but not installed: {', '.join(missing)}")
|
|
128
|
+
return 0
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _main(argv: Sequence[str] | None = None) -> int:
|
|
132
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
133
|
+
|
|
134
|
+
# The marker prefix must be known before argv can be split, but it lives in
|
|
135
|
+
# config, which lives behind --config-dir. A hand-rolled scan rather than a
|
|
136
|
+
# throwaway argparse: argparse assigns `--` a special meaning and would
|
|
137
|
+
# print its own bare usage block on a malformed flag, before amca has even
|
|
138
|
+
# decided what the command is.
|
|
139
|
+
pre = common.pre_scan(raw)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
ctx = AmcaContext(config_dir_override=pre.get("config-dir"))
|
|
143
|
+
except ConfigError as exc:
|
|
144
|
+
print(f"amca: {exc}", file=sys.stderr)
|
|
145
|
+
return 2
|
|
146
|
+
|
|
147
|
+
prefix = pre.get("marker-prefix") or ctx.config.get_str("plugins.marker_prefix")
|
|
148
|
+
try:
|
|
149
|
+
SCHEMA["plugins.marker_prefix"].check(prefix)
|
|
150
|
+
except ConfigError as exc:
|
|
151
|
+
print(f"amca: {exc}", file=sys.stderr)
|
|
152
|
+
return 2
|
|
153
|
+
registry_names = [entry.name for entry in Registry(ctx).entries]
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
split = split_argv(raw, registry_names, prefix)
|
|
157
|
+
except UnknownMarker as exc:
|
|
158
|
+
print(f"amca: {exc}", file=sys.stderr)
|
|
159
|
+
return 2
|
|
160
|
+
except ValueError as exc:
|
|
161
|
+
print(f"amca: {exc}", file=sys.stderr)
|
|
162
|
+
return 2
|
|
163
|
+
|
|
164
|
+
parser = build_parser(prefix, [f"{prefix}{n.replace('_', '-')}" for n in registry_names])
|
|
165
|
+
args = parser.parse_args(split.main)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
common.apply_global_flags(ctx, args)
|
|
169
|
+
except ConfigError as exc:
|
|
170
|
+
print(f"amca: {exc}", file=sys.stderr)
|
|
171
|
+
return 2
|
|
172
|
+
|
|
173
|
+
common.first_run_completion_notice(ctx)
|
|
174
|
+
|
|
175
|
+
if ctx.config.get_bool("core.greet"):
|
|
176
|
+
print("Hello Master")
|
|
177
|
+
|
|
178
|
+
handler = getattr(args, "handler", None)
|
|
179
|
+
if handler is None:
|
|
180
|
+
handler = run_cmd.handle
|
|
181
|
+
return int(handler(ctx, args, split.per_plugin) or 0)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
185
|
+
"""Console-script entry point. Never lets a bare traceback reach the user."""
|
|
186
|
+
return common.run_entry(_main, argv)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
if __name__ == "__main__": # pragma: no cover
|
|
190
|
+
raise SystemExit(main())
|