dotagents-cli 0.8.0__tar.gz → 0.8.1__tar.gz
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.
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/CHANGELOG.md +21 -1
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/PKG-INFO +2 -1
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/authoring.md +35 -6
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/overlays.md +5 -2
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/pyproject.toml +2 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/AGENTS.md +59 -7
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/__init__.py +1 -1
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_env.py +25 -1
- dotagents_cli-0.8.1/src/dotagents/_launchers.py +241 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlays.py +95 -4
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_pyvenv.py +32 -2
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/build_pyz.py +5 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_build_pyz.py +2 -1
- dotagents_cli-0.8.1/tests/test_launchers.py +322 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_pyvenv_core.py +10 -2
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/.gitignore +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/LICENSE +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/README.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/agents.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/cli.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/commands.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/context.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/env.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/fs.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/hooks.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/merge.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/overlays.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/reference.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/resources.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/scope.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/skills.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/sources.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/api/wrappers.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/changelog.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/commands.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/install.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/guide/private-sync.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/docs/index.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/install.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/mkdocs.yml +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/__main__.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_agents.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_context.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_fs.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_hooks.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_ignore.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_merge.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_pipwheel.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_resources.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_scope.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_skills.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_sources.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/_wrappers.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/__init__.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/_common.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/about.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/context.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/env.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/init.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/overlays.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/cli/path.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/src/dotagents/py.typed +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/_helpers.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/_shell.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/conftest.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_about.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_agents.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_api_header.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_audit_leak.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_cli_main.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_cloud_setup.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_cmds_discovery.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_context.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_cli_scope.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_format.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_into.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_regressions.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_env_safety.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_findings_cmd.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_adapters.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_cli.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_env.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_hooks.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_init.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_overlays.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_packaging.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_scope_context.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_fix_skills_adapters.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_git_ssl_revoke.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_hooks.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_ignore.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_init_base.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_last_mile.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_launch_cmd.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_local_additions.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_mcp.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_no_internal_labels.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_overlay_libs.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_overlays_cli.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_overlays_command.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_path_timeout_frontmatter.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_pipwheel.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_powershell_env_hook.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_quoting_injection_safety.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_scope.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_sources.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_sources_uri.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_wire_hooks.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tests/test_wrappers.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tools/audit.py +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tools/cloud-setup.sh +0 -0
- {dotagents_cli-0.8.0 → dotagents_cli-0.8.1}/tools/pyz_smoke.sh +0 -0
|
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.8.1] - 2026-10-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Overlay commands get a launcher on `PATH`: each top-level `bin/<name>.py`, and
|
|
15
|
+
each `[scripts]` entry in `overlay.toml` (`name = "path"` adds or replaces one,
|
|
16
|
+
`name = false` opts a `bin/<name>.py` out). The launchers live in the agents'
|
|
17
|
+
venv, in `<venv>/overlays/`: `<name>.exe` on Windows, made with distlib's
|
|
18
|
+
`ScriptMaker` so no command interpreter parses the arguments, and a `/bin/sh`
|
|
19
|
+
script elsewhere. They run the script through `$<NAME>_OVERLAY_ROOT` when they
|
|
20
|
+
run, so only a new interpreter needs them made again. `init` and `overlays add` /
|
|
21
|
+
`sync` / `remove` keep them current and delete those no overlay has; two
|
|
22
|
+
overlays with one script name is an error naming both.
|
|
23
|
+
- `dotagents env` puts the launcher directory of the venv `AGENTS_PYTHON` names
|
|
24
|
+
first on `PATH`, ahead of the levels' `bin` dirs, and takes any other venv's
|
|
25
|
+
off it.
|
|
26
|
+
- A project gets its own venv when one of its overlays has scripts.
|
|
27
|
+
- New dependency: `distlib>=0.4.0,<0.5`, vendored in the `.pyz`.
|
|
28
|
+
|
|
10
29
|
## [0.8.0] - 2026-10-16
|
|
11
30
|
|
|
12
31
|
### Added
|
|
@@ -1527,7 +1546,8 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
|
|
|
1527
1546
|
affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
|
|
1528
1547
|
before dispatch; a no-op for a plain install.
|
|
1529
1548
|
|
|
1530
|
-
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.8.
|
|
1549
|
+
[Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.8.1...HEAD
|
|
1550
|
+
[0.8.1]: https://github.com/jose-pr/dotagents/compare/v0.8.0...v0.8.1
|
|
1531
1551
|
[0.8.0]: https://github.com/jose-pr/dotagents/compare/v0.7.6...v0.8.0
|
|
1532
1552
|
[0.7.6]: https://github.com/jose-pr/dotagents/compare/v0.7.5...v0.7.6
|
|
1533
1553
|
[0.7.5]: https://github.com/jose-pr/dotagents/compare/v0.7.4...v0.7.5
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dotagents-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.1
|
|
4
4
|
Summary: The dotagents CLI: install and manage a portable ~/.agents config for AI coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/jose-pr/dotagents/
|
|
6
6
|
Project-URL: Repository, https://github.com/jose-pr/dotagents
|
|
@@ -23,6 +23,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
23
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
24
|
Classifier: Typing :: Typed
|
|
25
25
|
Requires-Python: >=3.9
|
|
26
|
+
Requires-Dist: distlib<0.5,>=0.4.0
|
|
26
27
|
Requires-Dist: duho<0.8,>=0.7.5
|
|
27
28
|
Requires-Dist: pathapi<0.2,>=0.1.0
|
|
28
29
|
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
|
@@ -72,12 +72,41 @@ dotagents starts with the libs in place whatever shell it runs from. A `lib/` mo
|
|
|
72
72
|
comes before site-packages and the standard library: do not reuse a package name you
|
|
73
73
|
do not mean to replace.
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
## Commands on PATH: `bin/*.py` and `[scripts]`
|
|
76
|
+
|
|
77
|
+
A Python command needs no launcher of your own. Every top-level `bin/<name>.py`
|
|
78
|
+
gets one named `<name>`: `<name>.exe` on Windows (distlib's `ScriptMaker`, the
|
|
79
|
+
launcher pip gives console scripts) and a `/bin/sh` script elsewhere, made in the
|
|
80
|
+
agents' venv, in `<venv>/overlays/`, whenever `overlays add` / `sync` / `remove` or
|
|
81
|
+
`init` brings the venv up to date. `dotagents env` puts that directory first on
|
|
82
|
+
`PATH` for the venv `$AGENTS_PYTHON` names, so `bin/curl.py` answers to `curl`
|
|
83
|
+
ahead of the system's, and drops any other venv's.
|
|
84
|
+
|
|
85
|
+
The launcher runs your script under the venv's interpreter, as `python
|
|
86
|
+
bin/<name>.py` would, found when it runs through `$<NAME>_OVERLAY_ROOT`: editing
|
|
87
|
+
the script or moving the store needs nothing regenerated, only a new interpreter
|
|
88
|
+
does. Unset, or with the script missing, it exits 2 and says so. A file whose name
|
|
89
|
+
starts with `_` is not a command. Name other scripts, rename one, or opt one out
|
|
90
|
+
in `overlay.toml`:
|
|
91
|
+
|
|
92
|
+
```toml
|
|
93
|
+
[scripts]
|
|
94
|
+
tool = "lib/tool/main.py" # a launcher `tool` for another path
|
|
95
|
+
helper = false # none for bin/helper.py
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Two overlays with one script name is an error naming both. A launcher no overlay
|
|
99
|
+
has any more is deleted at the next sync.
|
|
100
|
+
|
|
101
|
+
**Never ship a `.cmd` or `.bat` that takes arguments.** cmd.exe parses its command
|
|
102
|
+
line before the first line runs, so an argument's `&`, `|`, `>` or `%VAR%` acts:
|
|
103
|
+
from PowerShell, `curl "http://h/?a=1|hostname"` would run `hostname`. The `.exe`
|
|
104
|
+
above hands the arguments over as passed.
|
|
105
|
+
|
|
106
|
+
Your script may run outside a `dotagents env` session too, so set `PYTHONPATH`
|
|
107
|
+
for what it starts, if anything needs the libs: its own `lib/`, then
|
|
108
|
+
`$AGENTS_PYTHONPATH`, then the caller's, leaving empty parts out (an empty entry
|
|
109
|
+
means the current directory). The net overlay's `bin/curl.py` does this.
|
|
81
110
|
|
|
82
111
|
## Setup scripts
|
|
83
112
|
|
|
@@ -48,7 +48,7 @@ or `--repo <checkout>/overlays` for a local checkout of the branch.
|
|
|
48
48
|
| `python`, `node`, `rust` | Per-language `kb/` conventions + manifest templates + CI workflow templates. |
|
|
49
49
|
| `release` | A host-agnostic release helper: an agent-driven commit-plan loop plus tag/CI monitoring across GitHub (`gh`) and GitLab. |
|
|
50
50
|
| `private-sync` | The one-private-repo, per-project `.agents` model (`kb/PRIVATE_SYNC.md` + cloud hooks) — see [Private sync](private-sync.md). |
|
|
51
|
-
| `net` |
|
|
51
|
+
| `net` | HTTP tooling: a drop-in `curl` launcher (curlike), an OS-trust-store `certifi` shim, and syshttp (the system proxy and the OS verifier by default), both packages installed from the overlay's `requirements.txt`; settings are `AGENTS_*` (`AGENTS_PROXY` and kin). |
|
|
52
52
|
| `recovery` | A config-recovery playbook for reconstructing a lost `~/.agents`. |
|
|
53
53
|
|
|
54
54
|
## Managing overlays
|
|
@@ -182,7 +182,9 @@ dotagents overlays sync -g # r
|
|
|
182
182
|
|
|
183
183
|
Nothing in an overlay has to set up its own paths. For every installed overlay,
|
|
184
184
|
`dotagents env` prepends `bin/` to `PATH` and an existing `lib/` to `PYTHONPATH`
|
|
185
|
-
(listing the libs in `AGENTS_PYTHONPATH` too),
|
|
185
|
+
(listing the libs in `AGENTS_PYTHONPATH` too), puts the launchers of its scripts
|
|
186
|
+
(`bin/*.py` and `[scripts]`, in `<venv>/overlays/` of the agents' venv) first on
|
|
187
|
+
`PATH`, and exports **`$<NAME>_OVERLAY_ROOT`** — the overlay's
|
|
186
188
|
installed directory (`NAME` is the overlay's name upper-cased, `-` → `_`:
|
|
187
189
|
`private-sync` → `PRIVATE_SYNC_OVERLAY_ROOT`).
|
|
188
190
|
That variable is how overlay content refers to itself and to other overlays: a
|
|
@@ -214,6 +216,7 @@ small built-in reader that covers these keys. Keys:
|
|
|
214
216
|
| `rules` | Overlay-relative markdown paths whose rule bullets append to "Always-on rules". |
|
|
215
217
|
| `priority` | Merge order (lower sorts earlier; unprioritized default is 500). |
|
|
216
218
|
| `setup_timeout` | Seconds the setup script may run before it is stopped (default 300; `0` = no limit). |
|
|
219
|
+
| `[scripts]` | Launchers on `PATH`: `name = "path/in/overlay.py"` adds or replaces one, `name = false` drops the one for a `bin/<name>.py`. See [Authoring → Commands on PATH](authoring.md#commands-on-path-binpy-and-scripts). |
|
|
217
220
|
|
|
218
221
|
`overlay.toml` is the only manifest. A leading `---` frontmatter block in a
|
|
219
222
|
`CONTEXT.md` (the `priority:` / `scope:` / `dependencies:` an older overlay format
|
|
@@ -37,6 +37,8 @@ classifiers = [
|
|
|
37
37
|
dependencies = [
|
|
38
38
|
"duho>=0.7.5,<0.8",
|
|
39
39
|
"pathapi>=0.1.0,<0.2",
|
|
40
|
+
# The launchers of overlays' [scripts]: ScriptMaker writes the Windows .exe.
|
|
41
|
+
"distlib>=0.4.0,<0.5",
|
|
40
42
|
# overlay.toml manifests and TOML registries; tomllib is stdlib from 3.11.
|
|
41
43
|
'tomli>=1.1.0; python_version < "3.11"',
|
|
42
44
|
]
|
|
@@ -239,8 +239,10 @@ returns the exit code.
|
|
|
239
239
|
otherwise; `0` from either means no limit.
|
|
240
240
|
- `parse_manifest_text(text, origin="overlay.toml") -> Optional[dict]` — the TOML
|
|
241
241
|
document via `tomllib` / `tomli`, else a built-in reader (top-level strings,
|
|
242
|
-
string arrays including multi-line strings, an integer
|
|
243
|
-
when it is not valid TOML.
|
|
242
|
+
string arrays including multi-line strings, an integer, and the `[scripts]` table
|
|
243
|
+
of strings and booleans); `None` plus one warning when it is not valid TOML.
|
|
244
|
+
- `SCRIPT_NAME_RE` — a script (launcher) name: a letter or digit, then letters,
|
|
245
|
+
digits, `.`, `_`, `-`; never ending in `.exe` nor a Windows device name.
|
|
244
246
|
- `class InstallResult` — what `install_to` did: `written`, `unchanged`,
|
|
245
247
|
`replaced` (edited here, replaced by the source's file), `removed` (not in the
|
|
246
248
|
source, or cleared by `prune`), `backed_up`, `lines`; property `skipped`
|
|
@@ -270,8 +272,16 @@ returns the exit code.
|
|
|
270
272
|
(the later copy shadows it).
|
|
271
273
|
- `Overlay.sort_by_priority(overlays) -> list[Overlay]` — sorted by `sort_key`.
|
|
272
274
|
- `read_manifest() -> dict` — keys `name`, `description`, `routing`, `rules`,
|
|
273
|
-
`requires` (valid names only, normalized, de-duplicated), `priority
|
|
274
|
-
|
|
275
|
+
`requires` (valid names only, normalized, de-duplicated), `priority`,
|
|
276
|
+
`setup_timeout`, `scripts` (the `[scripts]` table as written: name -> path
|
|
277
|
+
relative to the overlay, `/`-separated, plain file-name characters, no `..`,
|
|
278
|
+
or `false`; any other entry is dropped with a warning); a missing or invalid
|
|
279
|
+
manifest gives empty contributions.
|
|
280
|
+
- `scripts() -> dict[str, str]` — the scripts that get a launcher
|
|
281
|
+
(`dotagents._launchers`): each top-level `bin/<name>.py` whose stem is a
|
|
282
|
+
`SCRIPT_NAME_RE` name (so not `_private.py`, not `__init__.py`), then
|
|
283
|
+
`[scripts]`, which adds or replaces a path by name, or drops a detected one
|
|
284
|
+
with `false`.
|
|
275
285
|
- `find_setup_script() -> Optional[Path]`.
|
|
276
286
|
- `setup_timeout(override=None) -> int` — `override`, else the manifest's
|
|
277
287
|
`setup_timeout` (a non-negative int; anything else is ignored with a warning),
|
|
@@ -307,6 +317,35 @@ returns the exit code.
|
|
|
307
317
|
`sort_key` order; creates the file when absent; content outside the markers kept;
|
|
308
318
|
returns whether it changed (`False` and a warning when the file has no block).
|
|
309
319
|
|
|
320
|
+
## `dotagents._launchers`
|
|
321
|
+
|
|
322
|
+
Launchers for overlays' scripts (`Overlay.scripts()`), in `<venv>/overlays/` of a
|
|
323
|
+
dotagents venv; `_pyvenv.sync_venvs` brings them up to date on every sync, and
|
|
324
|
+
`_env.get_environment` puts that dir first on `PATH` for the venv `AGENTS_PYTHON`
|
|
325
|
+
names. A launcher runs `$<NAME>_OVERLAY_ROOT/<script>` under the venv's
|
|
326
|
+
interpreter, resolved when it runs (argv[0] the script, its dir first on
|
|
327
|
+
`sys.path`, run as `__main__`); with the variable unset or the script missing it
|
|
328
|
+
exits 2 naming both. Windows: `<name>.exe`, distlib's `ScriptMaker` launcher (no
|
|
329
|
+
command interpreter parses its arguments). POSIX: `<name>`, mode 0755, a `/bin/sh`
|
|
330
|
+
script that `exec`s the interpreter.
|
|
331
|
+
|
|
332
|
+
- `DIR = "overlays"`, `MANIFEST = ".dotagents-launchers.json"` (in that dir:
|
|
333
|
+
`{name: overlay name}` of the launchers dotagents wrote).
|
|
334
|
+
- `class LauncherError(SystemExit)` — two overlays with one script name; the
|
|
335
|
+
message names both.
|
|
336
|
+
- `launcher_dir(venv) -> Path`. `file_name(name) -> str` (`.exe` added on Windows).
|
|
337
|
+
`is_launcher_dir(path) -> bool` — the shape `<store>/.pyvenv/<triplet>/overlays`,
|
|
338
|
+
no filesystem access.
|
|
339
|
+
- `declared(overlays) -> dict[str, tuple[Overlay, str]]` — every script by name;
|
|
340
|
+
raises `LauncherError` on a name two overlays have.
|
|
341
|
+
- `render(name, var, script, python) -> bytes` — one launcher's bytes; equal
|
|
342
|
+
inputs give equal bytes (the `.exe`'s archive carries a fixed time).
|
|
343
|
+
- `sync(venv, python, overlays, *, dry_run=False, logger=None) -> list[str]` —
|
|
344
|
+
writes each launcher whose bytes differ (atomic replace), deletes those
|
|
345
|
+
`MANIFEST` lists that no overlay has any more (other files there are left
|
|
346
|
+
alone), rewrites `MANIFEST`; returns the names written or deleted. Raises
|
|
347
|
+
`LauncherError` before any write.
|
|
348
|
+
|
|
310
349
|
## `dotagents._sources`
|
|
311
350
|
|
|
312
351
|
Spec grammar: `[git+]<location>[@<ref>][#<path>]`. A location ending `.git`, starting
|
|
@@ -383,7 +422,12 @@ a directory of overlays or a registry file (default: the root); for a **source**
|
|
|
383
422
|
|
|
384
423
|
Assembly order (`get_environment`): identity (`stamp_identity`), then the roots,
|
|
385
424
|
then `PATH`, then `PYTHONPATH` / `AGENTS_PYTHONPATH`, then the env files, then the
|
|
386
|
-
proxy model.
|
|
425
|
+
proxy model. `PATH`: every level's `bin` prepended, then, first of all, the active
|
|
426
|
+
venv's launcher dir (`<venv>/overlays`, `_launchers`) when `AGENTS_PYTHON` is that
|
|
427
|
+
venv's interpreter and the dir exists; every other dotagents venv's launcher dir is
|
|
428
|
+
taken out. First even before the `bin` dirs: cmd.exe and PowerShell try every
|
|
429
|
+
`PATHEXT` extension in a directory before the next one, so a `bin/<name>.py` (with
|
|
430
|
+
`.PY` in `PATHEXT`) or a `.cmd` there would otherwise win.
|
|
387
431
|
|
|
388
432
|
- Roots: `AGENTS_HOME` and `AGENTS_PROJECT_ROOT` name the stores walked, absolute —
|
|
389
433
|
an inherited value naming the same directory holds, another is replaced; in a
|
|
@@ -706,10 +750,16 @@ code. `AGENTS_NO_PYVENV=1` (or `true`/`yes`/`on`) turns it all off.
|
|
|
706
750
|
when the requirements' digest changed, in one `pip install -r ... -r ...` per
|
|
707
751
|
venv, with `--find-links` from `AGENTS_PIP_SOURCE`. The `.pth` is rewritten when
|
|
708
752
|
it differs: the store's own libs, then each lower store's venv site-packages and
|
|
709
|
-
libs, then `runtime_entry()`.
|
|
753
|
+
libs, then `runtime_entry()`. Each venv's launchers (`_launchers.sync`) serve its
|
|
754
|
+
store's overlays and every lower store's; a project venv a sync made that is no
|
|
755
|
+
longer wanted keeps its launchers current too. With `AGENTS_NO_PYVENV` it logs
|
|
756
|
+
which scripts get no launcher. Raises `VenvError` or `LauncherError` (both
|
|
757
|
+
`SystemExit`) on failure.
|
|
710
758
|
- `wants_venv(scope, level, store, libs) -> bool` — always for `user`; for `project`
|
|
711
|
-
when it has overlay requirements or
|
|
759
|
+
when it has overlay requirements, libs or scripts; never for `system` (a venv an
|
|
712
760
|
administrator made there is still layered).
|
|
761
|
+
- `store_overlays(scope, stores) -> list[Overlay]` — the overlays in play
|
|
762
|
+
installed in any of `stores`.
|
|
713
763
|
- `ensure_venv(target, python=None, *, with_pip=None, dry_run=False, logger=None)
|
|
714
764
|
-> Optional[Path]` — the venv's interpreter, created when missing; `with_pip=None`
|
|
715
765
|
tries ensurepip and falls back to `--without-pip`, `False` never uses it.
|
|
@@ -802,6 +852,8 @@ proxy vars, plus whatever the env files set. `AGENTS_AGENT` is never emitted.
|
|
|
802
852
|
Set for an overlay's `setup.py`: `AGENTS_HOME` (user store), `AGENTS_SCOPE_ROOT`,
|
|
803
853
|
`AGENTS_SCOPE`, `AGENTS_OVERLAY_DIR`.
|
|
804
854
|
|
|
855
|
+
Read by a script launcher (`_launchers`): its overlay's `<NAME>_OVERLAY_ROOT`.
|
|
856
|
+
|
|
805
857
|
## Gotchas
|
|
806
858
|
|
|
807
859
|
- **Python 3.9 floor.** Runtime-evaluated annotations need `from __future__ import
|
|
@@ -4,7 +4,8 @@ Contract B, the exact sequence :func:`get_environment` performs:
|
|
|
4
4
|
|
|
5
5
|
1. **Bins onto PATH FIRST**, before any env eval. Each level's ``bin`` dir
|
|
6
6
|
(contract-A precedence order, *except* project-root) is prepended to
|
|
7
|
-
``PATH`` so env scripts can call overlay helpers by name
|
|
7
|
+
``PATH`` so env scripts can call overlay helpers by name, and ahead of
|
|
8
|
+
them the active venv's launcher dir (:func:`_launcher_path`). **Libs go on
|
|
8
9
|
``PYTHONPATH`` the same way**: each level's ``lib`` dir that EXISTS
|
|
9
10
|
(:func:`get_lib_paths`) is prepended to ``PYTHONPATH`` and listed,
|
|
10
11
|
``os.pathsep``-joined, in ``AGENTS_PYTHONPATH`` -- so an ``env.py``, a
|
|
@@ -852,6 +853,25 @@ def _anchored(entry: str) -> bool:
|
|
|
852
853
|
return bool(entry) and (os.path.isabs(entry) or entry[0] in "/\\")
|
|
853
854
|
|
|
854
855
|
|
|
856
|
+
def _launcher_path(path: str, venv: "Optional[Path]") -> "Optional[str]":
|
|
857
|
+
"""``PATH`` with ``venv``'s launcher dir (``<venv>/overlays``, the
|
|
858
|
+
overlays' ``[scripts]``: :mod:`dotagents._launchers`) first when it
|
|
859
|
+
exists, and every other dotagents venv's taken out: a launcher names its
|
|
860
|
+
own venv's interpreter. ``None`` if nothing changes.
|
|
861
|
+
|
|
862
|
+
First, ahead of the levels' ``bin`` dirs too: cmd.exe and PowerShell try
|
|
863
|
+
every ``PATHEXT`` extension in one directory before the next, so a
|
|
864
|
+
``bin`` holding the script itself (``curl.py``, with ``.PY`` in
|
|
865
|
+
``PATHEXT``) or a ``.cmd`` of the same name would otherwise win."""
|
|
866
|
+
from dotagents import _launchers
|
|
867
|
+
|
|
868
|
+
entries = [e for e in path.split(os.pathsep) if not (e and _launchers.is_launcher_dir(e))] if path else []
|
|
869
|
+
if venv is not None and _launchers.launcher_dir(venv).is_dir():
|
|
870
|
+
entries.insert(0, str(_launchers.launcher_dir(venv)))
|
|
871
|
+
updated = os.pathsep.join(entries)
|
|
872
|
+
return updated if updated != path else None
|
|
873
|
+
|
|
874
|
+
|
|
855
875
|
def _prepended_path_var(osenv: "dict[str, str]", var: str, entries: "list[str]") -> "Optional[str]":
|
|
856
876
|
"""The new value of an ``os.pathsep``-joined path var with ``entries``
|
|
857
877
|
prepended (:func:`_prepend_missing` order), or ``None`` if nothing changes.
|
|
@@ -1106,6 +1126,10 @@ def get_environment(
|
|
|
1106
1126
|
updated = _prepended_path_var(osenv, "PATH", bin_paths)
|
|
1107
1127
|
if updated is not None:
|
|
1108
1128
|
_apply({"PATH": updated})
|
|
1129
|
+
updated = _launcher_path(osenv.get("PATH", ""), venv if venv is not None
|
|
1130
|
+
and osenv.get("AGENTS_PYTHON") == str(_pyvenv.venv_python(venv)) else None)
|
|
1131
|
+
if updated is not None:
|
|
1132
|
+
_apply({"PATH": updated})
|
|
1109
1133
|
|
|
1110
1134
|
# --- Libs: on PYTHONPATH, and published as AGENTS_PYTHONPATH, same moment.
|
|
1111
1135
|
# Everything an overlay starts -- a skill script the agent runs, a `bin`
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Launchers for overlays' scripts: each top-level ``bin/<name>.py``, plus
|
|
2
|
+
what ``overlay.toml`` declares (``Overlay.scripts``)::
|
|
3
|
+
|
|
4
|
+
[scripts]
|
|
5
|
+
tool = "lib/tool/main.py" # another path, or another name
|
|
6
|
+
helper = false # no launcher for bin/helper.py
|
|
7
|
+
|
|
8
|
+
Each becomes an executable named after the key in ``<venv>/overlays/`` of a
|
|
9
|
+
dotagents venv (:mod:`dotagents._pyvenv`), which ``env`` puts first on
|
|
10
|
+
``PATH`` for the venv ``AGENTS_PYTHON`` names. A launcher runs the script
|
|
11
|
+
under that venv's interpreter, found at RUN time through the overlay's
|
|
12
|
+
``<NAME>_OVERLAY_ROOT``, so moving a store or syncing an overlay never
|
|
13
|
+
strands it; only a new interpreter needs a new launcher. With the variable
|
|
14
|
+
unset or the script missing it exits 2, saying so.
|
|
15
|
+
|
|
16
|
+
* Windows: ``<name>.exe``, made by distlib's ``ScriptMaker`` (pip's console
|
|
17
|
+
script launcher) around a short Python bootstrap. A native program: no
|
|
18
|
+
command interpreter parses its arguments, as cmd.exe would a ``.cmd``'s.
|
|
19
|
+
* POSIX: ``<name>``, a ``/bin/sh`` script that ``exec``s the interpreter.
|
|
20
|
+
|
|
21
|
+
:data:`MANIFEST` in the directory records which overlay made which launcher,
|
|
22
|
+
so one no overlay declares any more is deleted; files it does not list are
|
|
23
|
+
left alone. Writes are atomic and happen only when the bytes differ.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import sys
|
|
32
|
+
import tempfile
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Optional
|
|
35
|
+
|
|
36
|
+
#: ``<venv>/overlays``: the launchers, and nothing of the venv's own.
|
|
37
|
+
DIR = "overlays"
|
|
38
|
+
MANIFEST = ".dotagents-launchers.json"
|
|
39
|
+
#: The time stamped on the bootstrap in an ``.exe``'s archive (1980-01-01, the
|
|
40
|
+
#: earliest a ZIP can hold): fixed, so equal inputs give equal bytes.
|
|
41
|
+
_EPOCH = "315532800"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LauncherError(SystemExit):
|
|
45
|
+
"""Two overlays declare the same script name."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def launcher_dir(venv: "str | os.PathLike[str]") -> Path:
|
|
49
|
+
return Path(venv) / DIR
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_launcher_dir(path: str) -> bool:
|
|
53
|
+
"""Whether ``path`` has the shape of any dotagents venv's launcher dir
|
|
54
|
+
(``<store>/.pyvenv/<triplet>/overlays``); the filesystem is not consulted."""
|
|
55
|
+
from dotagents._pyvenv import PYVENV_DIR
|
|
56
|
+
|
|
57
|
+
parts = Path(os.path.normpath(path)).parts
|
|
58
|
+
fold = (lambda s: s.lower()) if os.name == "nt" else (lambda s: s)
|
|
59
|
+
return len(parts) >= 3 and fold(parts[-1]) == fold(DIR) and fold(parts[-3]) == fold(PYVENV_DIR)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def file_name(name: str) -> str:
|
|
63
|
+
return name + ".exe" if os.name == "nt" else name
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def declared(overlays) -> "dict[str, tuple]":
|
|
67
|
+
"""``{name: (overlay, script)}`` over ``overlays``; a name two overlays
|
|
68
|
+
declare is a :class:`LauncherError` naming both."""
|
|
69
|
+
out: "dict[str, tuple]" = {}
|
|
70
|
+
seen: "dict[str, str]" = {}
|
|
71
|
+
for overlay in overlays:
|
|
72
|
+
for name, script in overlay.scripts().items():
|
|
73
|
+
key = name.lower() if os.name == "nt" else name
|
|
74
|
+
if key in seen and out[seen[key]][0].path != overlay.path:
|
|
75
|
+
raise LauncherError(
|
|
76
|
+
"error: overlays %s and %s both declare the script %r"
|
|
77
|
+
% (out[seen[key]][0].name, overlay.name, name)
|
|
78
|
+
)
|
|
79
|
+
seen[key] = name
|
|
80
|
+
out[name] = (overlay, script)
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _bootstrap(name: str, var: str, script: str) -> str:
|
|
85
|
+
"""The Python an ``.exe`` runs: the script as ``__main__``, as
|
|
86
|
+
``python <script>`` would."""
|
|
87
|
+
return "\n".join([
|
|
88
|
+
"#!python",
|
|
89
|
+
"import os",
|
|
90
|
+
"import runpy",
|
|
91
|
+
"import sys",
|
|
92
|
+
"",
|
|
93
|
+
"root = os.environ.get(%r, '')" % var,
|
|
94
|
+
"script = os.path.join(root, *%r)" % (script.split("/"),),
|
|
95
|
+
"if not root or not os.path.isfile(script):",
|
|
96
|
+
" sys.stderr.write(%r %% root)" % (
|
|
97
|
+
"%s: %s=%%s has no %s: run it in a `dotagents env` session\n" % (name, var, script)),
|
|
98
|
+
" sys.exit(2)",
|
|
99
|
+
"sys.argv[0] = script",
|
|
100
|
+
"sys.path[0] = os.path.dirname(script)",
|
|
101
|
+
"runpy.run_path(script, run_name='__main__')",
|
|
102
|
+
"",
|
|
103
|
+
])
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _sh_quote(text: str) -> str:
|
|
107
|
+
return "'%s'" % text.replace("'", "'\\''")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _shell(name: str, var: str, script: str, python: str) -> bytes:
|
|
111
|
+
message = "%s: %s=${%s:-} has no %s: run it in a dotagents env session" % (name, var, var, script)
|
|
112
|
+
path = '"$%s"%s' % (var, _sh_quote("/" + script))
|
|
113
|
+
return "\n".join([
|
|
114
|
+
"#!/bin/sh",
|
|
115
|
+
"# Made by dotagents from overlay.toml [scripts]; regenerated on overlays sync.",
|
|
116
|
+
'if [ -z "${%s:-}" ] || [ ! -f %s ]; then' % (var, path),
|
|
117
|
+
' echo "%s" >&2' % message,
|
|
118
|
+
" exit 2",
|
|
119
|
+
"fi",
|
|
120
|
+
"exec %s %s \"$@\"" % (_sh_quote(python), path),
|
|
121
|
+
"",
|
|
122
|
+
]).encode("utf-8")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _exe(name: str, var: str, script: str, python: str, work: str) -> bytes:
|
|
126
|
+
from distlib.scripts import ScriptMaker
|
|
127
|
+
|
|
128
|
+
source = os.path.join(work, "source")
|
|
129
|
+
out = os.path.join(work, "out")
|
|
130
|
+
os.mkdir(source)
|
|
131
|
+
os.mkdir(out)
|
|
132
|
+
with open(os.path.join(source, name + ".py"), "w", encoding="utf-8", newline="\n") as handle:
|
|
133
|
+
handle.write(_bootstrap(name, var, script))
|
|
134
|
+
maker = ScriptMaker(source, out, add_launchers=True)
|
|
135
|
+
maker.executable = python
|
|
136
|
+
maker.variants = {""}
|
|
137
|
+
saved = os.environ.get("SOURCE_DATE_EPOCH")
|
|
138
|
+
os.environ["SOURCE_DATE_EPOCH"] = _EPOCH
|
|
139
|
+
try:
|
|
140
|
+
maker.make(name + ".py")
|
|
141
|
+
finally:
|
|
142
|
+
if saved is None:
|
|
143
|
+
del os.environ["SOURCE_DATE_EPOCH"]
|
|
144
|
+
else:
|
|
145
|
+
os.environ["SOURCE_DATE_EPOCH"] = saved
|
|
146
|
+
with open(os.path.join(out, name + ".exe"), "rb") as handle:
|
|
147
|
+
return handle.read()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def render(name: str, var: str, script: str, python: str) -> bytes:
|
|
151
|
+
"""The bytes of launcher ``name`` for this platform."""
|
|
152
|
+
if os.name != "nt":
|
|
153
|
+
return _shell(name, var, script, python)
|
|
154
|
+
work = tempfile.mkdtemp(prefix="dotagents-launcher-")
|
|
155
|
+
try:
|
|
156
|
+
return _exe(name, var, script, python, work)
|
|
157
|
+
finally:
|
|
158
|
+
shutil.rmtree(work, ignore_errors=True)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _read(path: Path) -> "Optional[bytes]":
|
|
162
|
+
try:
|
|
163
|
+
with open(str(path), "rb") as handle:
|
|
164
|
+
return handle.read()
|
|
165
|
+
except OSError:
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _write(path: Path, data: bytes, mode: "Optional[int]") -> None:
|
|
170
|
+
partial = path.with_name(".%s.new" % path.name)
|
|
171
|
+
with open(str(partial), "wb") as handle:
|
|
172
|
+
handle.write(data)
|
|
173
|
+
if mode is not None:
|
|
174
|
+
os.chmod(str(partial), mode)
|
|
175
|
+
try:
|
|
176
|
+
os.replace(str(partial), str(path))
|
|
177
|
+
except OSError:
|
|
178
|
+
os.unlink(str(partial))
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def sync(venv: Path, python: Path, overlays, *, dry_run: bool = False, logger=None) -> "list[str]":
|
|
183
|
+
"""Make ``<venv>/overlays`` hold exactly the launchers ``overlays``
|
|
184
|
+
declare (interpreter ``python``), deleting those :data:`MANIFEST` lists
|
|
185
|
+
that no overlay declares any more. Returns the launcher names written or
|
|
186
|
+
deleted. A name declared twice is a :class:`LauncherError`, before any
|
|
187
|
+
write."""
|
|
188
|
+
wanted = declared(overlays)
|
|
189
|
+
target = launcher_dir(venv)
|
|
190
|
+
manifest_path = target / MANIFEST
|
|
191
|
+
try:
|
|
192
|
+
recorded = json.loads((_read(manifest_path) or b"{}").decode("utf-8"))
|
|
193
|
+
except ValueError:
|
|
194
|
+
recorded = {}
|
|
195
|
+
if not isinstance(recorded, dict):
|
|
196
|
+
recorded = {}
|
|
197
|
+
changed: "list[str]" = []
|
|
198
|
+
for name in sorted(set(recorded) - set(wanted)):
|
|
199
|
+
path = target / file_name(name)
|
|
200
|
+
if path.exists():
|
|
201
|
+
_log(logger, "info", "launcher %s: removed (no overlay declares it)%s", path, _dry(dry_run))
|
|
202
|
+
if not dry_run:
|
|
203
|
+
path.unlink()
|
|
204
|
+
changed.append(name)
|
|
205
|
+
for name, (overlay, script) in sorted(wanted.items()):
|
|
206
|
+
if not (overlay.path / script).is_file():
|
|
207
|
+
_log(logger, "warning", "overlay %s: script %s (%s) not found; its launcher exits 2 until it is",
|
|
208
|
+
overlay.name, name, script)
|
|
209
|
+
data = render(name, overlay.root_var, script, str(python))
|
|
210
|
+
path = target / file_name(name)
|
|
211
|
+
if _read(path) == data:
|
|
212
|
+
continue
|
|
213
|
+
_log(logger, "info", "launcher %s: %s (%s)%s", path, "updated" if path.exists() else "created",
|
|
214
|
+
overlay.name, _dry(dry_run))
|
|
215
|
+
changed.append(name)
|
|
216
|
+
if not dry_run:
|
|
217
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
218
|
+
_write(path, data, None if os.name == "nt" else 0o755)
|
|
219
|
+
record = {name: overlay.name for name, (overlay, _script) in wanted.items()}
|
|
220
|
+
if not dry_run and record != recorded:
|
|
221
|
+
if record:
|
|
222
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
223
|
+
_write(manifest_path, (json.dumps(record, indent=1, sort_keys=True) + "\n").encode("utf-8"), None)
|
|
224
|
+
elif manifest_path.exists():
|
|
225
|
+
manifest_path.unlink()
|
|
226
|
+
try:
|
|
227
|
+
target.rmdir()
|
|
228
|
+
except OSError:
|
|
229
|
+
pass # something else lives there: left alone
|
|
230
|
+
return changed
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _dry(dry_run: bool) -> str:
|
|
234
|
+
return " [dry-run]" if dry_run else ""
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _log(logger, level: str, message: str, *args) -> None:
|
|
238
|
+
if logger is not None:
|
|
239
|
+
getattr(logger, level)(message, *args)
|
|
240
|
+
elif level == "warning":
|
|
241
|
+
sys.stderr.write("warning: " + (message % args) + "\n")
|