env-module-manager 0.3.0__tar.gz → 0.5.0__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.
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/.github/workflows/test.yml +21 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/.gitignore +1 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/AGENTS.md +3 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/PKG-INFO +19 -10
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/README.md +18 -9
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/collective-environments.md +1 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/configuration.md +9 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/development.md +8 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/installation.md +9 -3
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/python-tools.md +27 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/reference.md +1 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/module_manager/__init__.py +1 -1
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/module_manager/cli.py +128 -21
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/module_manager/config.py +4 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/module_manager/deploy.py +45 -12
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/pyproject.toml +6 -1
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/tests/test_cli.py +34 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/tests/test_config.py +2 -0
- env_module_manager-0.5.0/tests/test_integration.py +49 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/tests/test_modulefile.py +50 -1
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/uv.lock +1 -1
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/.github/workflows/publish.yml +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/.github/workflows/tag.yml +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/.python-version +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/.readthedocs.yaml +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/CONTRIBUTING.md +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/LICENSE.md +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/environment-modules.md +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/index.md +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/rust-tools.md +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/shell-tools.md +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/stylesheets/extra.css +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/docs/uninstall.md +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/example.bash +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/mkdocs.yml +0 -0
- {env_module_manager-0.3.0 → env_module_manager-0.5.0}/module_manager/modulefile.py +0 -0
|
@@ -74,3 +74,24 @@ jobs:
|
|
|
74
74
|
|
|
75
75
|
- name: Verify artifacts
|
|
76
76
|
run: ls -l dist/
|
|
77
|
+
|
|
78
|
+
integration:
|
|
79
|
+
runs-on: ${{ matrix.os }}
|
|
80
|
+
strategy:
|
|
81
|
+
fail-fast: false
|
|
82
|
+
matrix:
|
|
83
|
+
os: [ubuntu-latest, macos-latest]
|
|
84
|
+
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
|
|
88
|
+
- name: Set up Python
|
|
89
|
+
uses: astral-sh/setup-uv@v3
|
|
90
|
+
with:
|
|
91
|
+
python-version: "3.12"
|
|
92
|
+
|
|
93
|
+
- name: Install dependencies
|
|
94
|
+
run: uv sync --group dev
|
|
95
|
+
|
|
96
|
+
- name: Test real uv deployment
|
|
97
|
+
run: uv run pytest tests/test_integration.py -m integration --no-cov
|
|
@@ -90,6 +90,9 @@ Common settings:
|
|
|
90
90
|
* `module_root` or `MODULE_MANAGER_MODULE_ROOT`
|
|
91
91
|
* `[python].indexes`, `MODULE_MANAGER_INDEX`, or `MODULE_MANAGER_INDEXES`
|
|
92
92
|
* `[python].find_links` or `MODULE_MANAGER_FIND_LINKS`
|
|
93
|
+
* `[python].uv_config_file` for the uv `--config-file` option. Do not add a
|
|
94
|
+
`MODULE_MANAGER_*` wrapper for this; users should use uv's `UV_CONFIG_FILE`
|
|
95
|
+
environment variable when they want environment-based uv configuration.
|
|
93
96
|
|
|
94
97
|
## Documentation
|
|
95
98
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: env-module-manager
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Deploy Python, Rust, shell, and collective CLI environments behind GNU environment modulefiles
|
|
5
5
|
Project-URL: Homepage, https://github.com/ejb90/module-manager
|
|
6
6
|
Project-URL: Documentation, https://module-manager.readthedocs.io/
|
|
@@ -50,6 +50,22 @@ It writes tools and modulefiles into predictable versioned locations:
|
|
|
50
50
|
/prod/modulefiles/<name>/<version>
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
The installation prefix and module root must be different directories.
|
|
54
|
+
|
|
55
|
+
## Installation
|
|
56
|
+
|
|
57
|
+
Install from PyPI:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
pip install env-module-manager
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Or install as a uv-managed tool:
|
|
64
|
+
|
|
65
|
+
```sh
|
|
66
|
+
uv tool install env-module-manager
|
|
67
|
+
```
|
|
68
|
+
|
|
53
69
|
## Quick Start
|
|
54
70
|
|
|
55
71
|
Deploy a Python CLI with `uv tool install`:
|
|
@@ -108,12 +124,5 @@ module-manager uninstall ruff 0.8.0 \
|
|
|
108
124
|
|
|
109
125
|
## Documentation
|
|
110
126
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
```sh
|
|
114
|
-
uv run mkdocs serve
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Start with `docs/index.md` for installation, configuration, Python tools, Rust
|
|
118
|
-
tools, shell scripts, collective environments, uninstall, and local development
|
|
119
|
-
notes.
|
|
127
|
+
Read the full documentation on
|
|
128
|
+
[Read the Docs](https://module-manager.readthedocs.io/).
|
|
@@ -18,6 +18,22 @@ It writes tools and modulefiles into predictable versioned locations:
|
|
|
18
18
|
/prod/modulefiles/<name>/<version>
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
The installation prefix and module root must be different directories.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Install from PyPI:
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
pip install env-module-manager
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or install as a uv-managed tool:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
uv tool install env-module-manager
|
|
35
|
+
```
|
|
36
|
+
|
|
21
37
|
## Quick Start
|
|
22
38
|
|
|
23
39
|
Deploy a Python CLI with `uv tool install`:
|
|
@@ -76,12 +92,5 @@ module-manager uninstall ruff 0.8.0 \
|
|
|
76
92
|
|
|
77
93
|
## Documentation
|
|
78
94
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
```sh
|
|
82
|
-
uv run mkdocs serve
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Start with `docs/index.md` for installation, configuration, Python tools, Rust
|
|
86
|
-
tools, shell scripts, collective environments, uninstall, and local development
|
|
87
|
-
notes.
|
|
95
|
+
Read the full documentation on
|
|
96
|
+
[Read the Docs](https://module-manager.readthedocs.io/).
|
|
@@ -17,8 +17,13 @@ module_root = "/prod/modulefiles"
|
|
|
17
17
|
[python]
|
|
18
18
|
indexes = ["https://packages.example/simple"]
|
|
19
19
|
find_links = ["/prod/wheels"]
|
|
20
|
+
uv_config_file = "/prod/config/uv.toml"
|
|
20
21
|
```
|
|
21
22
|
|
|
23
|
+
`prefix` and `module_root` must resolve to different directories. Using the
|
|
24
|
+
same root makes `<prefix>/<name>/<version>` both an installation directory and
|
|
25
|
+
a modulefile path.
|
|
26
|
+
|
|
22
27
|
Use a different config file with `--config`:
|
|
23
28
|
|
|
24
29
|
```sh
|
|
@@ -36,6 +41,10 @@ export MODULE_MANAGER_FIND_LINKS=/prod/wheels,/prod/more-wheels
|
|
|
36
41
|
|
|
37
42
|
`MODULE_MANAGER_CONFIG` can point to an alternate TOML config file.
|
|
38
43
|
|
|
44
|
+
There is no `MODULE_MANAGER_*` environment variable for uv configuration files.
|
|
45
|
+
Use uv's own `UV_CONFIG_FILE` environment variable when you want to set that via
|
|
46
|
+
the environment.
|
|
47
|
+
|
|
39
48
|
## CLI Precedence
|
|
40
49
|
|
|
41
50
|
CLI options override both environment variables and TOML config:
|
|
@@ -14,6 +14,14 @@ uv run ruff format --check .
|
|
|
14
14
|
uv run pytest
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Run the real `uv tool install` integration test with:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
uv run pytest tests/test_integration.py -m integration --no-cov
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
CI runs this integration test on Linux and macOS.
|
|
24
|
+
|
|
17
25
|
Build distributions:
|
|
18
26
|
|
|
19
27
|
```sh
|
|
@@ -38,18 +38,24 @@ in every new shell.
|
|
|
38
38
|
|
|
39
39
|
## Install module-manager
|
|
40
40
|
|
|
41
|
-
From
|
|
41
|
+
From PyPI with pip:
|
|
42
42
|
|
|
43
43
|
```sh
|
|
44
|
-
|
|
44
|
+
pip install env-module-manager
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
As a uv-managed tool:
|
|
48
48
|
|
|
49
49
|
```sh
|
|
50
50
|
uv tool install env-module-manager
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
From a local checkout:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
uv tool install .
|
|
57
|
+
```
|
|
58
|
+
|
|
53
59
|
During development, use:
|
|
54
60
|
|
|
55
61
|
```sh
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
Python CLIs are installed with `uv tool install`. Each deployed version gets
|
|
4
4
|
isolated `UV_TOOL_DIR` and `UV_TOOL_BIN_DIR` paths.
|
|
5
5
|
|
|
6
|
+
The install subprocess ignores inherited `UV_TOOL_*` settings so the managed
|
|
7
|
+
destination and install behavior are not changed by the invoking environment.
|
|
8
|
+
Other uv settings, including `UV_CONFIG_FILE`, are preserved.
|
|
9
|
+
|
|
6
10
|
`uv` is an external runtime requirement for `--execute-install`. It is not a
|
|
7
11
|
Python package dependency because `module-manager` shells out to the `uv`
|
|
8
12
|
executable and is intended to be installable by `uv` itself.
|
|
@@ -51,6 +55,29 @@ module-manager deploy-python gitconductor 0.1.0 \
|
|
|
51
55
|
|
|
52
56
|
`--find-links` may be used more than once.
|
|
53
57
|
|
|
58
|
+
## Use a uv Config File
|
|
59
|
+
|
|
60
|
+
Pass a specific uv configuration file to `uv tool install`:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
module-manager deploy-python internal-tool 1.2.3 \
|
|
64
|
+
--package internal-tool==1.2.3 \
|
|
65
|
+
--uv-config-file /prod/config/uv.toml \
|
|
66
|
+
--prefix /prod/tools \
|
|
67
|
+
--module-root /prod/modulefiles \
|
|
68
|
+
--execute-install
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The same option can be set in the module-manager config file:
|
|
72
|
+
|
|
73
|
+
```toml
|
|
74
|
+
[python]
|
|
75
|
+
uv_config_file = "/prod/config/uv.toml"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Use uv's own `UV_CONFIG_FILE` environment variable if you want to configure uv
|
|
79
|
+
through the environment.
|
|
80
|
+
|
|
54
81
|
## Deploy from VCS
|
|
55
82
|
|
|
56
83
|
`--package` is passed directly to `uv tool install`, so VCS package specs are
|
|
@@ -29,14 +29,61 @@ click.rich_click.STYLE_ARGUMENT = "bold yellow"
|
|
|
29
29
|
click.rich_click.STYLE_SWITCH = "bold magenta"
|
|
30
30
|
click.rich_click.STYLE_METAVAR = "yellow"
|
|
31
31
|
click.rich_click.HEADER_TEXT = "module-manager"
|
|
32
|
-
click.rich_click.FOOTER_TEXT = (
|
|
33
|
-
"Examples: [cyan]module-manager deploy-python ruff 0.8.0 --package "
|
|
34
|
-
"ruff==0.8.0 --prefix /prod/tools --module-root /prod/modulefiles[/cyan]"
|
|
35
|
-
)
|
|
36
32
|
|
|
37
33
|
PATH = click.Path(path_type=Path)
|
|
38
34
|
ClickCommand = TypeVar("ClickCommand", bound=Command)
|
|
39
35
|
|
|
36
|
+
TOP_LEVEL_EXAMPLES = (
|
|
37
|
+
"Examples:\n\n"
|
|
38
|
+
"[cyan]module-manager deploy-python ruff 0.8.0 --package ruff==0.8.0 "
|
|
39
|
+
"--prefix /prod/tools --module-root /prod/modulefiles[/cyan]\n\n"
|
|
40
|
+
"[cyan]module-manager deploy-rust ripgrep 14.1.1 --binary ./target/release/rg "
|
|
41
|
+
"--prefix /prod/tools --module-root /prod/modulefiles[/cyan]\n\n"
|
|
42
|
+
"[cyan]module-manager deploy-script my-tool 1.0.0 --script ./scripts/my-tool "
|
|
43
|
+
"--prefix /prod/tools --module-root /prod/modulefiles[/cyan]\n\n"
|
|
44
|
+
"[cyan]module-manager deploy-env --file dev-tools.toml[/cyan]\n\n"
|
|
45
|
+
"[cyan]module-manager uninstall ruff 0.8.0 --prefix /prod/tools --module-root /prod/modulefiles[/cyan]"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
DEPLOY_PYTHON_EXAMPLES = (
|
|
49
|
+
"Examples:\n\n"
|
|
50
|
+
"[cyan]module-manager deploy-python ruff 0.8.0 --package ruff==0.8.0 "
|
|
51
|
+
"--prefix /prod/tools --module-root /prod/modulefiles --execute-install[/cyan]\n\n"
|
|
52
|
+
"[cyan]module-manager deploy-python internal-tool 1.2.3 --package internal-tool==1.2.3 "
|
|
53
|
+
"--uv-config-file /prod/config/uv.toml --prefix /prod/tools --module-root /prod/modulefiles[/cyan]"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
DEPLOY_RUST_EXAMPLES = (
|
|
57
|
+
"Examples:\n\n"
|
|
58
|
+
"[cyan]module-manager deploy-rust ripgrep 14.1.1 --binary ./target/release/rg "
|
|
59
|
+
"--prefix /prod/tools --module-root /prod/modulefiles[/cyan]\n\n"
|
|
60
|
+
"[cyan]module-manager deploy-rust ripgrep 14.1.1 --binary ./target/release/rg "
|
|
61
|
+
"--prefix /prod/tools --module-root /prod/modulefiles --dry-run[/cyan]"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
DEPLOY_SCRIPT_EXAMPLES = (
|
|
65
|
+
"Examples:\n\n"
|
|
66
|
+
"[cyan]module-manager deploy-script my-tool 1.0.0 --script ./scripts/my-tool "
|
|
67
|
+
"--prefix /prod/tools --module-root /prod/modulefiles[/cyan]\n\n"
|
|
68
|
+
"[cyan]module-manager deploy-script my-tool 1.0.0 --script ./scripts/my-tool "
|
|
69
|
+
"--prefix /prod/tools --module-root /prod/modulefiles --dry-run[/cyan]"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
DEPLOY_ENV_EXAMPLES = (
|
|
73
|
+
"Examples:\n\n"
|
|
74
|
+
"[cyan]module-manager deploy-env --file dev-tools.toml[/cyan]\n\n"
|
|
75
|
+
"[cyan]module-manager deploy-env --file dev-tools.toml --prefix /scratch/tools "
|
|
76
|
+
"--module-root /scratch/modulefiles --dry-run[/cyan]"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
UNINSTALL_EXAMPLES = (
|
|
80
|
+
"Examples:\n\n"
|
|
81
|
+
"[cyan]module-manager uninstall ruff 0.8.0 --prefix /prod/tools "
|
|
82
|
+
"--module-root /prod/modulefiles[/cyan]\n\n"
|
|
83
|
+
"[cyan]module-manager uninstall ruff 0.8.0 --prefix /prod/tools "
|
|
84
|
+
"--module-root /prod/modulefiles --dry-run[/cyan]"
|
|
85
|
+
)
|
|
86
|
+
|
|
40
87
|
|
|
41
88
|
def common_options(command: ClickCommand) -> ClickCommand:
|
|
42
89
|
"""Attach options shared by deployment commands.
|
|
@@ -108,6 +155,7 @@ def location_options(command: ClickCommand) -> ClickCommand:
|
|
|
108
155
|
"Build versioned modulefiles for Python tools, Rust binaries, shell scripts, "
|
|
109
156
|
"and collective environments."
|
|
110
157
|
),
|
|
158
|
+
epilog=TOP_LEVEL_EXAMPLES,
|
|
111
159
|
)
|
|
112
160
|
@click.version_option(__version__)
|
|
113
161
|
@click.option(
|
|
@@ -131,7 +179,11 @@ def main(ctx: click.Context, config: Path | None) -> None:
|
|
|
131
179
|
ctx.obj = load_config(config)
|
|
132
180
|
|
|
133
181
|
|
|
134
|
-
@main.command(
|
|
182
|
+
@main.command(
|
|
183
|
+
"deploy-python",
|
|
184
|
+
help="Write a modulefile for a uv tool install Python CLI.",
|
|
185
|
+
epilog=DEPLOY_PYTHON_EXAMPLES,
|
|
186
|
+
)
|
|
135
187
|
@common_options
|
|
136
188
|
@click.option("--package", "package", required=True, help="Package spec passed to uv tool install.")
|
|
137
189
|
@click.option("--python", "python", help="Python interpreter/version passed to uv.")
|
|
@@ -147,6 +199,11 @@ def main(ctx: click.Context, config: Path | None) -> None:
|
|
|
147
199
|
multiple=True,
|
|
148
200
|
help="Directory or HTML page of packages passed to uv. May be used more than once.",
|
|
149
201
|
)
|
|
202
|
+
@click.option(
|
|
203
|
+
"--uv-config-file",
|
|
204
|
+
type=PATH,
|
|
205
|
+
help="uv.toml file passed to uv tool install with --config-file.",
|
|
206
|
+
)
|
|
150
207
|
@click.option(
|
|
151
208
|
"--execute-install",
|
|
152
209
|
is_flag=True,
|
|
@@ -166,6 +223,7 @@ def deploy_python(
|
|
|
166
223
|
python: str | None,
|
|
167
224
|
indexes: tuple[str, ...],
|
|
168
225
|
find_links: tuple[str, ...],
|
|
226
|
+
uv_config_file: Path | None,
|
|
169
227
|
execute_install: bool,
|
|
170
228
|
) -> None:
|
|
171
229
|
"""Write a modulefile for a [cyan]uv tool install[/cyan] Python CLI.
|
|
@@ -183,6 +241,7 @@ def deploy_python(
|
|
|
183
241
|
python: Optional Python interpreter or version passed to uv.
|
|
184
242
|
indexes: Additional package index URLs passed to uv.
|
|
185
243
|
find_links: Wheelhouse directories or HTML package pages passed to uv.
|
|
244
|
+
uv_config_file: Optional uv configuration file passed to `uv tool`.
|
|
186
245
|
execute_install: Whether to run `uv tool install` immediately.
|
|
187
246
|
|
|
188
247
|
Raises:
|
|
@@ -191,8 +250,10 @@ def deploy_python(
|
|
|
191
250
|
`PATH`.
|
|
192
251
|
subprocess.CalledProcessError: If `uv tool install` fails.
|
|
193
252
|
"""
|
|
194
|
-
resolved_module_root =
|
|
195
|
-
|
|
253
|
+
resolved_module_root, resolved_prefix = require_locations(
|
|
254
|
+
module_root or config.module_root,
|
|
255
|
+
prefix or config.prefix,
|
|
256
|
+
)
|
|
196
257
|
paths = deploy_python_tool(
|
|
197
258
|
name=name,
|
|
198
259
|
version=version,
|
|
@@ -204,6 +265,7 @@ def deploy_python(
|
|
|
204
265
|
python=python,
|
|
205
266
|
indexes=indexes or config.indexes,
|
|
206
267
|
find_links=find_links or config.find_links,
|
|
268
|
+
uv_config_file=(uv_config_file.expanduser() if uv_config_file else config.uv_config_file),
|
|
207
269
|
execute_install=execute_install,
|
|
208
270
|
make_default=make_default,
|
|
209
271
|
)
|
|
@@ -212,7 +274,11 @@ def deploy_python(
|
|
|
212
274
|
)
|
|
213
275
|
|
|
214
276
|
|
|
215
|
-
@main.command(
|
|
277
|
+
@main.command(
|
|
278
|
+
"deploy-rust",
|
|
279
|
+
help="Copy a Rust CLI binary and write a modulefile for it.",
|
|
280
|
+
epilog=DEPLOY_RUST_EXAMPLES,
|
|
281
|
+
)
|
|
216
282
|
@common_options
|
|
217
283
|
@click.option(
|
|
218
284
|
"--binary",
|
|
@@ -255,8 +321,10 @@ def deploy_rust(
|
|
|
255
321
|
Raises:
|
|
256
322
|
click.UsageError: If required paths are missing.
|
|
257
323
|
"""
|
|
258
|
-
resolved_module_root =
|
|
259
|
-
|
|
324
|
+
resolved_module_root, resolved_prefix = require_locations(
|
|
325
|
+
module_root or config.module_root,
|
|
326
|
+
prefix or config.prefix,
|
|
327
|
+
)
|
|
260
328
|
paths = deploy_rust_tool(
|
|
261
329
|
name=name,
|
|
262
330
|
version=version,
|
|
@@ -277,7 +345,11 @@ def deploy_rust(
|
|
|
277
345
|
)
|
|
278
346
|
|
|
279
347
|
|
|
280
|
-
@main.command(
|
|
348
|
+
@main.command(
|
|
349
|
+
"deploy-script",
|
|
350
|
+
help="Copy a shell script and write a modulefile for it.",
|
|
351
|
+
epilog=DEPLOY_SCRIPT_EXAMPLES,
|
|
352
|
+
)
|
|
281
353
|
@common_options
|
|
282
354
|
@click.option(
|
|
283
355
|
"--script",
|
|
@@ -319,8 +391,10 @@ def deploy_script(
|
|
|
319
391
|
Raises:
|
|
320
392
|
click.UsageError: If required paths are missing.
|
|
321
393
|
"""
|
|
322
|
-
resolved_module_root =
|
|
323
|
-
|
|
394
|
+
resolved_module_root, resolved_prefix = require_locations(
|
|
395
|
+
module_root or config.module_root,
|
|
396
|
+
prefix or config.prefix,
|
|
397
|
+
)
|
|
324
398
|
paths = deploy_script_tool(
|
|
325
399
|
name=name,
|
|
326
400
|
version=version,
|
|
@@ -341,7 +415,11 @@ def deploy_script(
|
|
|
341
415
|
)
|
|
342
416
|
|
|
343
417
|
|
|
344
|
-
@main.command(
|
|
418
|
+
@main.command(
|
|
419
|
+
"deploy-env",
|
|
420
|
+
help="Deploy a collective environment from a TOML manifest.",
|
|
421
|
+
epilog=DEPLOY_ENV_EXAMPLES,
|
|
422
|
+
)
|
|
345
423
|
@click.option(
|
|
346
424
|
"--file",
|
|
347
425
|
"manifest",
|
|
@@ -399,12 +477,10 @@ def deploy_env(
|
|
|
399
477
|
except (OSError, TypeError, tomllib.TOMLDecodeError) as error:
|
|
400
478
|
raise click.ClickException(str(error)) from error
|
|
401
479
|
|
|
402
|
-
resolved_module_root =
|
|
480
|
+
resolved_module_root, resolved_prefix = require_locations(
|
|
403
481
|
module_root or spec.module_root or config.module_root,
|
|
404
|
-
|
|
405
|
-
"--module-root",
|
|
482
|
+
prefix or spec.prefix or config.prefix,
|
|
406
483
|
)
|
|
407
|
-
resolved_prefix = require_path(prefix or spec.prefix or config.prefix, "install prefix", "--prefix")
|
|
408
484
|
if make_default is not None:
|
|
409
485
|
spec = spec.__class__(
|
|
410
486
|
name=spec.name,
|
|
@@ -421,7 +497,11 @@ def deploy_env(
|
|
|
421
497
|
print_environment_result(result, dry_run)
|
|
422
498
|
|
|
423
499
|
|
|
424
|
-
@main.command(
|
|
500
|
+
@main.command(
|
|
501
|
+
"uninstall",
|
|
502
|
+
help="Remove a deployed tool version and its modulefile.",
|
|
503
|
+
epilog=UNINSTALL_EXAMPLES,
|
|
504
|
+
)
|
|
425
505
|
@location_options
|
|
426
506
|
@click.option(
|
|
427
507
|
"--keep-default",
|
|
@@ -457,8 +537,10 @@ def uninstall(
|
|
|
457
537
|
Raises:
|
|
458
538
|
click.UsageError: If required paths are missing.
|
|
459
539
|
"""
|
|
460
|
-
resolved_module_root =
|
|
461
|
-
|
|
540
|
+
resolved_module_root, resolved_prefix = require_locations(
|
|
541
|
+
module_root or config.module_root,
|
|
542
|
+
prefix or config.prefix,
|
|
543
|
+
)
|
|
462
544
|
result = uninstall_tool(
|
|
463
545
|
name=name,
|
|
464
546
|
version=version,
|
|
@@ -493,6 +575,31 @@ def require_path(value: Path | None, label: str, option: str) -> Path:
|
|
|
493
575
|
return value.expanduser()
|
|
494
576
|
|
|
495
577
|
|
|
578
|
+
def require_locations(module_root: Path | None, prefix: Path | None) -> tuple[Path, Path]:
|
|
579
|
+
"""Return distinct configured module and installation roots.
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
module_root: Candidate root of the module tree.
|
|
583
|
+
prefix: Candidate installation prefix.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Expanded module root and installation prefix.
|
|
587
|
+
|
|
588
|
+
Raises:
|
|
589
|
+
click.UsageError: If either path is missing or both resolve to the same
|
|
590
|
+
directory.
|
|
591
|
+
"""
|
|
592
|
+
resolved_module_root = require_path(module_root, "module root", "--module-root")
|
|
593
|
+
resolved_prefix = require_path(prefix, "install prefix", "--prefix")
|
|
594
|
+
if resolved_module_root.resolve() == resolved_prefix.resolve():
|
|
595
|
+
msg = (
|
|
596
|
+
"--module-root and --prefix must resolve to different directories; "
|
|
597
|
+
"otherwise the versioned modulefile collides with the install root."
|
|
598
|
+
)
|
|
599
|
+
raise click.UsageError(msg)
|
|
600
|
+
return resolved_module_root, resolved_prefix
|
|
601
|
+
|
|
602
|
+
|
|
496
603
|
def print_result(
|
|
497
604
|
modulefile: Path,
|
|
498
605
|
install_root: Path,
|
|
@@ -22,12 +22,14 @@ class AppConfig:
|
|
|
22
22
|
indexes: Default Python package indexes passed to `uv tool install`.
|
|
23
23
|
find_links: Default Python package locations passed to
|
|
24
24
|
`uv tool install`.
|
|
25
|
+
uv_config_file: Default uv configuration file passed to `uv tool`.
|
|
25
26
|
"""
|
|
26
27
|
|
|
27
28
|
prefix: Path | None = None
|
|
28
29
|
module_root: Path | None = None
|
|
29
30
|
indexes: tuple[str, ...] = ()
|
|
30
31
|
find_links: tuple[str, ...] = ()
|
|
32
|
+
uv_config_file: Path | None = None
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
def default_config_path() -> Path:
|
|
@@ -81,6 +83,7 @@ def load_file_config(path: Path) -> AppConfig:
|
|
|
81
83
|
module_root=optional_path(data.get("module_root"), "module_root"),
|
|
82
84
|
indexes=string_tuple(python_data.get("indexes"), "python.indexes"),
|
|
83
85
|
find_links=string_tuple(python_data.get("find_links"), "python.find_links"),
|
|
86
|
+
uv_config_file=optional_path(python_data.get("uv_config_file"), "python.uv_config_file"),
|
|
84
87
|
)
|
|
85
88
|
|
|
86
89
|
|
|
@@ -98,6 +101,7 @@ def overlay_env_config(config: AppConfig) -> AppConfig:
|
|
|
98
101
|
module_root=env_path("MODULE_MANAGER_MODULE_ROOT") or config.module_root,
|
|
99
102
|
indexes=env_tuple("MODULE_MANAGER_INDEXES") or env_tuple("MODULE_MANAGER_INDEX") or config.indexes,
|
|
100
103
|
find_links=env_tuple("MODULE_MANAGER_FIND_LINKS") or config.find_links,
|
|
104
|
+
uv_config_file=config.uv_config_file,
|
|
101
105
|
)
|
|
102
106
|
|
|
103
107
|
|
|
@@ -62,6 +62,7 @@ class EnvironmentToolSpec:
|
|
|
62
62
|
name: Tool name used for copied binaries and reporting.
|
|
63
63
|
version: Optional source tool version for documentation.
|
|
64
64
|
package: Python package spec passed to `uv tool install`.
|
|
65
|
+
uv_config_file: uv configuration file passed to `uv tool`.
|
|
65
66
|
binary: Rust binary path to copy into the shared `bin` directory.
|
|
66
67
|
script: Shell script path to copy into the shared `bin` directory.
|
|
67
68
|
python: Optional Python interpreter or version passed to uv.
|
|
@@ -75,6 +76,7 @@ class EnvironmentToolSpec:
|
|
|
75
76
|
name: str
|
|
76
77
|
version: str | None = None
|
|
77
78
|
package: str | None = None
|
|
79
|
+
uv_config_file: Path | None = None
|
|
78
80
|
binary: Path | None = None
|
|
79
81
|
script: Path | None = None
|
|
80
82
|
python: str | None = None
|
|
@@ -148,6 +150,7 @@ def uv_install_command(
|
|
|
148
150
|
python: str | None = None,
|
|
149
151
|
indexes: tuple[str, ...] = (),
|
|
150
152
|
find_links: tuple[str, ...] = (),
|
|
153
|
+
uv_config_file: Path | None = None,
|
|
151
154
|
) -> list[str]:
|
|
152
155
|
"""Build the uv command used to install a Python CLI tool.
|
|
153
156
|
|
|
@@ -156,11 +159,15 @@ def uv_install_command(
|
|
|
156
159
|
python: Optional Python interpreter or version passed to uv.
|
|
157
160
|
indexes: Additional package index URLs.
|
|
158
161
|
find_links: Wheelhouse directories or HTML package pages.
|
|
162
|
+
uv_config_file: Optional uv configuration file passed to `uv tool`.
|
|
159
163
|
|
|
160
164
|
Returns:
|
|
161
165
|
Tokenized uv command suitable for `subprocess.run`.
|
|
162
166
|
"""
|
|
163
|
-
command = ["uv", "tool"
|
|
167
|
+
command = ["uv", "tool"]
|
|
168
|
+
if uv_config_file:
|
|
169
|
+
command.extend(["--config-file", str(uv_config_file)])
|
|
170
|
+
command.append("install")
|
|
164
171
|
if python:
|
|
165
172
|
command.extend(["--python", python])
|
|
166
173
|
for index in indexes:
|
|
@@ -171,6 +178,23 @@ def uv_install_command(
|
|
|
171
178
|
return command
|
|
172
179
|
|
|
173
180
|
|
|
181
|
+
def uv_install_environment(tool_dir: Path, bin_dir: Path) -> dict[str, str]:
|
|
182
|
+
"""Build an isolated environment for a managed uv tool installation.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
tool_dir: Directory where uv should store installed tools.
|
|
186
|
+
bin_dir: Directory where uv should install tool executables.
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Process environment with inherited `UV_TOOL_*` settings removed and
|
|
190
|
+
the managed tool destinations set.
|
|
191
|
+
"""
|
|
192
|
+
env = {key: value for key, value in os.environ.items() if not key.startswith("UV_TOOL_")}
|
|
193
|
+
env["UV_TOOL_DIR"] = str(tool_dir)
|
|
194
|
+
env["UV_TOOL_BIN_DIR"] = str(bin_dir)
|
|
195
|
+
return env
|
|
196
|
+
|
|
197
|
+
|
|
174
198
|
def write_text(path: Path, content: str) -> None:
|
|
175
199
|
"""Write UTF-8 text to a path, creating parent directories first.
|
|
176
200
|
|
|
@@ -482,6 +506,7 @@ def parse_environment_tool(data: object, base_dir: Path, index: int) -> Environm
|
|
|
482
506
|
name=name,
|
|
483
507
|
version=optional_string(data, "version"),
|
|
484
508
|
package=optional_string(data, "package"),
|
|
509
|
+
uv_config_file=optional_manifest_path(data, "uv_config_file", base_dir),
|
|
485
510
|
binary=optional_manifest_path(data, "binary", base_dir),
|
|
486
511
|
script=optional_manifest_path(data, "script", base_dir),
|
|
487
512
|
python=optional_string(data, "python"),
|
|
@@ -540,7 +565,13 @@ def environment_actions(spec: EnvironmentSpec, paths: DeploymentPaths) -> tuple[
|
|
|
540
565
|
tool_dir = paths.install_root / "uv-tools"
|
|
541
566
|
for tool in spec.tools:
|
|
542
567
|
if tool.tool_type == "python" and tool.package:
|
|
543
|
-
command = uv_install_command(
|
|
568
|
+
command = uv_install_command(
|
|
569
|
+
tool.package,
|
|
570
|
+
tool.python,
|
|
571
|
+
tool.indexes,
|
|
572
|
+
tool.find_links,
|
|
573
|
+
tool.uv_config_file,
|
|
574
|
+
)
|
|
544
575
|
actions.append(
|
|
545
576
|
f"install python {tool.name}: "
|
|
546
577
|
f"UV_TOOL_DIR={shlex.quote(str(tool_dir))} "
|
|
@@ -591,13 +622,16 @@ def deploy_environment(
|
|
|
591
622
|
for tool in spec.tools:
|
|
592
623
|
if tool.tool_type == "python" and tool.package:
|
|
593
624
|
require_executable("uv")
|
|
594
|
-
env = os.environ.copy()
|
|
595
|
-
env["UV_TOOL_DIR"] = str(tool_dir)
|
|
596
|
-
env["UV_TOOL_BIN_DIR"] = str(paths.bin_dir)
|
|
597
625
|
subprocess.run(
|
|
598
|
-
uv_install_command(
|
|
626
|
+
uv_install_command(
|
|
627
|
+
tool.package,
|
|
628
|
+
tool.python,
|
|
629
|
+
tool.indexes,
|
|
630
|
+
tool.find_links,
|
|
631
|
+
tool.uv_config_file,
|
|
632
|
+
),
|
|
599
633
|
check=True,
|
|
600
|
-
env=
|
|
634
|
+
env=uv_install_environment(tool_dir, paths.bin_dir),
|
|
601
635
|
)
|
|
602
636
|
elif tool.tool_type == "rust" and tool.binary:
|
|
603
637
|
copy_executable(tool.binary, paths.bin_dir / tool.name)
|
|
@@ -634,6 +668,7 @@ def deploy_python_tool(
|
|
|
634
668
|
python: str | None = None,
|
|
635
669
|
indexes: tuple[str, ...] = (),
|
|
636
670
|
find_links: tuple[str, ...] = (),
|
|
671
|
+
uv_config_file: Path | None = None,
|
|
637
672
|
execute_install: bool = False,
|
|
638
673
|
make_default: bool = True,
|
|
639
674
|
) -> DeploymentPaths:
|
|
@@ -650,6 +685,7 @@ def deploy_python_tool(
|
|
|
650
685
|
python: Optional Python interpreter or version passed to uv.
|
|
651
686
|
indexes: Additional package index URLs passed to uv.
|
|
652
687
|
find_links: Wheelhouse directories or HTML package pages passed to uv.
|
|
688
|
+
uv_config_file: Optional uv configuration file passed to `uv tool`.
|
|
653
689
|
execute_install: Whether to run `uv tool install` immediately.
|
|
654
690
|
make_default: Whether to make this version the module default.
|
|
655
691
|
|
|
@@ -664,14 +700,11 @@ def deploy_python_tool(
|
|
|
664
700
|
paths = deployment_paths(module_root, prefix, name, version)
|
|
665
701
|
paths.bin_dir.mkdir(parents=True, exist_ok=True)
|
|
666
702
|
tool_dir = paths.install_root / "uv-tools"
|
|
667
|
-
command = uv_install_command(package, python, indexes, find_links)
|
|
703
|
+
command = uv_install_command(package, python, indexes, find_links, uv_config_file)
|
|
668
704
|
|
|
669
705
|
if execute_install:
|
|
670
706
|
require_executable("uv")
|
|
671
|
-
|
|
672
|
-
env["UV_TOOL_DIR"] = str(tool_dir)
|
|
673
|
-
env["UV_TOOL_BIN_DIR"] = str(paths.bin_dir)
|
|
674
|
-
subprocess.run(command, check=True, env=env)
|
|
707
|
+
subprocess.run(command, check=True, env=uv_install_environment(tool_dir, paths.bin_dir))
|
|
675
708
|
|
|
676
709
|
install_hint = (
|
|
677
710
|
f"UV_TOOL_DIR={shlex.quote(str(tool_dir))} "
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "env-module-manager"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Deploy Python, Rust, shell, and collective CLI environments behind GNU environment modulefiles"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -111,8 +111,13 @@ docstring-code-format = true
|
|
|
111
111
|
[tool.pytest.ini_options]
|
|
112
112
|
minversion = "7.0"
|
|
113
113
|
pythonpath = ["."]
|
|
114
|
+
markers = [
|
|
115
|
+
"integration: executes external tools for end-to-end verification",
|
|
116
|
+
]
|
|
114
117
|
addopts = [
|
|
115
118
|
"-ra",
|
|
119
|
+
"-m",
|
|
120
|
+
"not integration",
|
|
116
121
|
"--strict-markers",
|
|
117
122
|
"--strict-config",
|
|
118
123
|
"--tb=short",
|
|
@@ -56,12 +56,16 @@ def test_deploy_python_command_writes_modulefile() -> None:
|
|
|
56
56
|
"https://packages.example/simple",
|
|
57
57
|
"--find-links",
|
|
58
58
|
"/prod/wheels",
|
|
59
|
+
"--uv-config-file",
|
|
60
|
+
"/prod/uv.toml",
|
|
59
61
|
],
|
|
60
62
|
)
|
|
63
|
+
modulefile = Path("modules/ruff/0.8.0").read_text(encoding="utf-8")
|
|
61
64
|
|
|
62
65
|
assert result.exit_code == 0
|
|
63
66
|
assert "modulefile: modules/ruff/0.8.0" in result.output
|
|
64
67
|
assert "default version: modules/ruff/.version" in result.output
|
|
68
|
+
assert "--config-file /prod/uv.toml" in modulefile
|
|
65
69
|
|
|
66
70
|
|
|
67
71
|
def test_deploy_python_command_uses_config_defaults() -> None:
|
|
@@ -78,6 +82,7 @@ module_root = "modules"
|
|
|
78
82
|
[python]
|
|
79
83
|
indexes = ["https://packages.example/simple"]
|
|
80
84
|
find_links = ["/prod/wheels"]
|
|
85
|
+
uv_config_file = "uv.toml"
|
|
81
86
|
""".strip(),
|
|
82
87
|
encoding="utf-8",
|
|
83
88
|
)
|
|
@@ -93,9 +98,38 @@ find_links = ["/prod/wheels"]
|
|
|
93
98
|
"ruff==0.8.0",
|
|
94
99
|
],
|
|
95
100
|
)
|
|
101
|
+
modulefile = Path("modules/ruff/0.8.0").read_text(encoding="utf-8")
|
|
96
102
|
|
|
97
103
|
assert result.exit_code == 0
|
|
98
104
|
assert "modulefile: modules/ruff/0.8.0" in result.output
|
|
105
|
+
assert "--config-file uv.toml" in modulefile
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_deploy_command_rejects_matching_prefix_and_module_root() -> None:
|
|
109
|
+
"""Deployment roots must differ to avoid a modulefile path collision."""
|
|
110
|
+
runner = CliRunner()
|
|
111
|
+
|
|
112
|
+
with runner.isolated_filesystem():
|
|
113
|
+
result = runner.invoke(
|
|
114
|
+
main,
|
|
115
|
+
[
|
|
116
|
+
"deploy-python",
|
|
117
|
+
"gitconductor",
|
|
118
|
+
"0.7.0",
|
|
119
|
+
"--package",
|
|
120
|
+
"gitconductor",
|
|
121
|
+
"--prefix",
|
|
122
|
+
"test",
|
|
123
|
+
"--module-root",
|
|
124
|
+
"test",
|
|
125
|
+
],
|
|
126
|
+
)
|
|
127
|
+
collision_path_exists = Path("test/gitconductor/0.7.0").exists()
|
|
128
|
+
|
|
129
|
+
assert result.exit_code != 0
|
|
130
|
+
output = " ".join(strip_ansi(result.output).split())
|
|
131
|
+
assert "--module-root and --prefix must resolve to different directories" in output
|
|
132
|
+
assert not collision_path_exists
|
|
99
133
|
|
|
100
134
|
|
|
101
135
|
def test_cli_options_override_config_defaults() -> None:
|
|
@@ -22,6 +22,7 @@ module_root = "/scratch/modules"
|
|
|
22
22
|
[python]
|
|
23
23
|
indexes = ["https://packages.example/simple"]
|
|
24
24
|
find_links = ["/scratch/wheels"]
|
|
25
|
+
uv_config_file = "/scratch/uv.toml"
|
|
25
26
|
""".strip(),
|
|
26
27
|
encoding="utf-8",
|
|
27
28
|
)
|
|
@@ -31,6 +32,7 @@ find_links = ["/scratch/wheels"]
|
|
|
31
32
|
module_root=Path("/scratch/modules"),
|
|
32
33
|
indexes=("https://packages.example/simple",),
|
|
33
34
|
find_links=("/scratch/wheels",),
|
|
35
|
+
uv_config_file=Path("/scratch/uv.toml"),
|
|
34
36
|
)
|
|
35
37
|
|
|
36
38
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Integration tests that execute external deployment tools."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
from click.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
from module_manager.cli import main
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.mark.integration
|
|
12
|
+
def test_deploy_python_real_uv_install_uses_cli_roots(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
13
|
+
"""A real uv install should use the CLI prefix and module root.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
tmp_path: Temporary deployment root.
|
|
17
|
+
monkeypatch: Pytest helper used to control inherited uv destinations.
|
|
18
|
+
"""
|
|
19
|
+
project_root = Path(__file__).parent.parent
|
|
20
|
+
prefix = tmp_path / "tools"
|
|
21
|
+
module_root = tmp_path / "modules"
|
|
22
|
+
inherited_tool_dir = tmp_path / "inherited-tools"
|
|
23
|
+
inherited_bin_dir = tmp_path / "inherited-bin"
|
|
24
|
+
monkeypatch.setenv("UV_TOOL_DIR", str(inherited_tool_dir))
|
|
25
|
+
monkeypatch.setenv("UV_TOOL_BIN_DIR", str(inherited_bin_dir))
|
|
26
|
+
|
|
27
|
+
result = CliRunner().invoke(
|
|
28
|
+
main,
|
|
29
|
+
[
|
|
30
|
+
"deploy-python",
|
|
31
|
+
"module-manager",
|
|
32
|
+
"0.5.0",
|
|
33
|
+
"--package",
|
|
34
|
+
str(project_root),
|
|
35
|
+
"--prefix",
|
|
36
|
+
str(prefix),
|
|
37
|
+
"--module-root",
|
|
38
|
+
str(module_root),
|
|
39
|
+
"--execute-install",
|
|
40
|
+
],
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
install_root = prefix / "module-manager" / "0.5.0"
|
|
44
|
+
assert result.exit_code == 0, result.output
|
|
45
|
+
assert (install_root / "bin" / "module-manager").exists()
|
|
46
|
+
assert (install_root / "uv-tools" / "env-module-manager" / "uv-receipt.toml").exists()
|
|
47
|
+
assert (module_root / "module-manager" / "0.5.0").is_file()
|
|
48
|
+
assert not inherited_tool_dir.exists()
|
|
49
|
+
assert not inherited_bin_dir.exists()
|
|
@@ -14,6 +14,7 @@ from module_manager.deploy import (
|
|
|
14
14
|
require_executable,
|
|
15
15
|
uninstall_tool,
|
|
16
16
|
uv_install_command,
|
|
17
|
+
uv_install_environment,
|
|
17
18
|
)
|
|
18
19
|
from module_manager.modulefile import (
|
|
19
20
|
ModuleSpec,
|
|
@@ -109,6 +110,40 @@ def test_python_tool_install_command_accepts_python() -> None:
|
|
|
109
110
|
]
|
|
110
111
|
|
|
111
112
|
|
|
113
|
+
def test_python_tool_install_command_accepts_uv_config_file() -> None:
|
|
114
|
+
"""Uv install commands should include a requested uv config file."""
|
|
115
|
+
assert uv_install_command("ruff==0.8.0", uv_config_file=Path("/prod/uv.toml")) == [
|
|
116
|
+
"uv",
|
|
117
|
+
"tool",
|
|
118
|
+
"--config-file",
|
|
119
|
+
"/prod/uv.toml",
|
|
120
|
+
"install",
|
|
121
|
+
"ruff==0.8.0",
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def test_uv_install_environment_ignores_inherited_uv_tool_settings(
|
|
126
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Managed uv installs should ignore inherited tool-specific settings.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
tmp_path: Temporary deployment root.
|
|
132
|
+
monkeypatch: Pytest helper used to control the process environment.
|
|
133
|
+
"""
|
|
134
|
+
monkeypatch.setenv("UV_TOOL_DIR", "/inherited/tools")
|
|
135
|
+
monkeypatch.setenv("UV_TOOL_BIN_DIR", "/inherited/bin")
|
|
136
|
+
monkeypatch.setenv("UV_TOOL_UPGRADE", "1")
|
|
137
|
+
monkeypatch.setenv("UV_CONFIG_FILE", "/prod/uv.toml")
|
|
138
|
+
|
|
139
|
+
env = uv_install_environment(tmp_path / "tools", tmp_path / "bin")
|
|
140
|
+
|
|
141
|
+
assert env["UV_TOOL_DIR"] == str(tmp_path / "tools")
|
|
142
|
+
assert env["UV_TOOL_BIN_DIR"] == str(tmp_path / "bin")
|
|
143
|
+
assert "UV_TOOL_UPGRADE" not in env
|
|
144
|
+
assert env["UV_CONFIG_FILE"] == "/prod/uv.toml"
|
|
145
|
+
|
|
146
|
+
|
|
112
147
|
def test_deploy_python_tool_writes_package_sources_to_install_hint(tmp_path: Path) -> None:
|
|
113
148
|
"""Generated module help should show the package source options.
|
|
114
149
|
|
|
@@ -123,9 +158,11 @@ def test_deploy_python_tool_writes_package_sources_to_install_hint(tmp_path: Pat
|
|
|
123
158
|
prefix=tmp_path / "tools",
|
|
124
159
|
indexes=("https://packages.example/simple",),
|
|
125
160
|
find_links=("/prod/wheels",),
|
|
161
|
+
uv_config_file=Path("/prod/uv.toml"),
|
|
126
162
|
)
|
|
127
163
|
|
|
128
164
|
modulefile = paths.modulefile.read_text(encoding="utf-8")
|
|
165
|
+
assert "--config-file /prod/uv.toml" in modulefile
|
|
129
166
|
assert "--index https://packages.example/simple" in modulefile
|
|
130
167
|
assert "--find-links /prod/wheels" in modulefile
|
|
131
168
|
|
|
@@ -366,6 +403,7 @@ package = "ruff==0.8.0"
|
|
|
366
403
|
python = "3.12"
|
|
367
404
|
indexes = ["https://packages.example/simple"]
|
|
368
405
|
find_links = ["/prod/wheels"]
|
|
406
|
+
uv_config_file = "uv.toml"
|
|
369
407
|
|
|
370
408
|
[[tools]]
|
|
371
409
|
type = "script"
|
|
@@ -384,6 +422,7 @@ script = "scripts/helper"
|
|
|
384
422
|
assert not spec.make_default
|
|
385
423
|
assert spec.tools[0].package == "ruff==0.8.0"
|
|
386
424
|
assert spec.tools[0].indexes == ("https://packages.example/simple",)
|
|
425
|
+
assert spec.tools[0].uv_config_file == tmp_path / "uv.toml"
|
|
387
426
|
assert spec.tools[1].script == tmp_path / "scripts/helper"
|
|
388
427
|
|
|
389
428
|
|
|
@@ -491,6 +530,7 @@ type = "python"
|
|
|
491
530
|
name = "ruff"
|
|
492
531
|
package = "ruff==0.8.0"
|
|
493
532
|
python = "3.12"
|
|
533
|
+
uv_config_file = "uv.toml"
|
|
494
534
|
""".strip(),
|
|
495
535
|
encoding="utf-8",
|
|
496
536
|
)
|
|
@@ -502,7 +542,16 @@ python = "3.12"
|
|
|
502
542
|
prefix=tmp_path / "tools",
|
|
503
543
|
)
|
|
504
544
|
|
|
505
|
-
assert calls[0][0] == [
|
|
545
|
+
assert calls[0][0] == [
|
|
546
|
+
"uv",
|
|
547
|
+
"tool",
|
|
548
|
+
"--config-file",
|
|
549
|
+
str(tmp_path / "uv.toml"),
|
|
550
|
+
"install",
|
|
551
|
+
"--python",
|
|
552
|
+
"3.12",
|
|
553
|
+
"ruff==0.8.0",
|
|
554
|
+
]
|
|
506
555
|
assert calls[0][1]["UV_TOOL_DIR"] == str(result.paths.install_root / "uv-tools")
|
|
507
556
|
assert calls[0][1]["UV_TOOL_BIN_DIR"] == str(result.paths.bin_dir)
|
|
508
557
|
assert result.paths.modulefile.exists()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|