ixt-cli 0.9.0__tar.gz → 0.10.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.
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/PKG-INFO +32 -28
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/README.md +31 -27
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/pyproject.toml +4 -3
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/backends/binary.py +212 -26
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/backends/node.py +51 -18
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/__init__.py +9 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/cmd_config.py +69 -51
- ixt_cli-0.10.0/src/ixt/cli/cmd_exec.py +71 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/cmd_info.py +5 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/cmd_install.py +5 -2
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/cmd_lock.py +2 -2
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/cmd_misc.py +24 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/commands.py +3 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/parser.py +37 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/asset_pattern_cache.py +2 -12
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/env_policy.py +7 -2
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/flags.py +3 -3
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/heuristics.py +86 -4
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/models.py +3 -12
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/apply.py +28 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/cache.py +45 -25
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/discover.py +143 -31
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/doctor.py +56 -0
- ixt_cli-0.10.0/src/ixt/core/exec_tool.py +165 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/expose.py +106 -42
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/extract.py +84 -12
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/inject.py +51 -20
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/install.py +70 -80
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/install_local.py +51 -63
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/lockfile.py +214 -20
- ixt_cli-0.10.0/src/ixt/core/locks.py +112 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/pathlink.py +52 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/resolve_cache.py +4 -12
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/save.py +62 -25
- ixt_cli-0.10.0/src/ixt/core/transactions.py +216 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/upgrade.py +41 -114
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/data/heuristics.toml +8 -2
- ixt_cli-0.10.0/src/ixt/libs/atomic.py +56 -0
- ixt_cli-0.10.0/src/ixt/libs/elf.py +174 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/net/gitlab_api.py +8 -8
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/net/http.py +124 -3
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/conftest.py +54 -20
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/README.md +6 -4
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/docker/Dockerfile.alpine +1 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/docker/Dockerfile.fedora +1 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/docker/Dockerfile.ubuntu +1 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/run_e2e_incus.sh +1 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/test_e2e.py +18 -8
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/integration/test_lifecycle.py +3 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_apply.py +37 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_argparse_completion.py +1 -1
- ixt_cli-0.10.0/tests/unit/test_atomic.py +61 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_binary_backend.py +548 -40
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_cache.py +49 -2
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_cli.py +130 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_discover.py +175 -6
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_doctor.py +94 -0
- ixt_cli-0.10.0/tests/unit/test_elf.py +230 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_env_policy.py +18 -0
- ixt_cli-0.10.0/tests/unit/test_exec.py +289 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_export.py +1 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_expose.py +76 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_extract.py +60 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_gitlab_api.py +41 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_heuristics.py +53 -6
- ixt_cli-0.10.0/tests/unit/test_http.py +339 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_install.py +135 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_install_local.py +30 -4
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_lock_write.py +308 -6
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_lockfile.py +1 -1
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_node_backend.py +111 -9
- ixt_cli-0.10.0/tests/unit/test_pathlink.py +145 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_save.py +46 -0
- ixt_cli-0.10.0/tests/unit/test_transactions.py +92 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_upgrade.py +107 -48
- ixt_cli-0.9.0/src/ixt/core/locks.py +0 -54
- ixt_cli-0.9.0/tests/unit/test_http.py +0 -163
- ixt_cli-0.9.0/tests/unit/test_pathlink.py +0 -62
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/.gitignore +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/LICENSE +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/__main__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/backends/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/backends/binary_resolver.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/backends/python.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/argparse_completion.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/cmd_apply.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/cmd_cache.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/cmd_registry.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/cmd_upgrade.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/cli/render.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/asset_index.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/fs_policy.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/lockfile.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/registry.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/settings.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/setup_toml.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/config/toml.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/apply_actions.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/backend.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/bootstrap.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/export.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/hooks.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/identity.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/resolution_stats.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/resolve.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/runtimes.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/setup_completions.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/core/setup_path.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/data/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/data/asset_index.json +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/data/registry.toml +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/libs/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/libs/constants.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/libs/fmt.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/libs/logger.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/libs/output.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/libs/req_spec.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/libs/semver.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/libs/shell.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/libs/style.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/net/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/net/github_api.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/net/npm.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/net/pypi.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/net/source.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/platform/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/src/ixt/platform/win.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/conftest.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/playground_incus.sh +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/run_e2e_docker.sh +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/e2e/runtime_matrix.sh +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/integration/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/integration/test_policy_runtime.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/__init__.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_asset_index.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_asset_pattern_cache.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_backend.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_bootstrap.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_flags.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_fs_policy.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_github_api.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_hooks.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_inject.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_models.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_namespacing.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_output.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_platform.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_python_backend.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_registry.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_req_spec.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_resolve.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_resolve_cache.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_runtimes.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_semver.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_settings.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_setup_path.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_setup_toml.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_source.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_style.py +0 -0
- {ixt_cli-0.9.0 → ixt_cli-0.10.0}/tests/unit/test_toml.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ixt-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: Universal CLI tool isolator - manage Python, Node, and binary tools with per-tool isolation
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/pytgaen-group/ixt
|
|
6
6
|
Project-URL: Repository, https://gitlab.com/pytgaen-group/ixt.git
|
|
@@ -51,10 +51,10 @@ Description-Content-Type: text/markdown
|
|
|
51
51
|
*Zero third-party deps. Zero system pollution. One command, any ecosystem.*
|
|
52
52
|
|
|
53
53
|
- **Day-one GitHub releases** — install a tool the day it ships, without
|
|
54
|
-
|
|
54
|
+
waiting for a formula, registry entry, or plugin.
|
|
55
55
|
- **No system runtime needed** — ixt is pure Python stdlib; `uv` and `bun` are
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
bootstrapped on first use, so the host does not need Node, npm, or a package
|
|
57
|
+
manager pre-installed.
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
ixt tool install ruff # Python (PyPI)
|
|
@@ -202,40 +202,44 @@ ixt tool apply --locked --strict
|
|
|
202
202
|
|
|
203
203
|
`--locked` replays the exact lock instead of resolving newer versions.
|
|
204
204
|
`--strict` fails before changing the machine if `ixt.toml` and `ixt.lock`
|
|
205
|
-
diverge.
|
|
205
|
+
diverge. The strict preflight is local: it checks the manifest fingerprint,
|
|
206
|
+
declared policy, version constraints, and locked binary metadata without
|
|
207
|
+
contacting PyPI, npm, GitHub, or GitLab. Run `ixt lock write` when you want to
|
|
208
|
+
resolve upstream again and refresh the lock.
|
|
206
209
|
|
|
207
210
|
## 🧰 Commands
|
|
208
211
|
|
|
209
212
|
| Command | Description |
|
|
210
213
|
| ---------------------------------------------- | --------------------------------------------------------------------- |
|
|
211
|
-
| `ixt tool add <pkg>`
|
|
212
|
-
| `ixt tool install <pkg>`
|
|
213
|
-
| `ixt tool install <pkg> --bare`
|
|
214
|
-
| `ixt tool uninstall <pkg>`
|
|
215
|
-
| `ixt tool list`
|
|
216
|
-
| `ixt tool info <pkg>`
|
|
217
|
-
| `ixt tool upgrade <pkg>`
|
|
218
|
-
| `ixt tool upgrade --all`
|
|
219
|
-
| `ixt tool apply`
|
|
220
|
-
| `ixt tool apply --locked --strict`
|
|
221
|
-
| `ixt tool export`
|
|
222
|
-
| `ixt lock write`
|
|
223
|
-
| `ixt lock check`
|
|
224
|
-
| `ixt lock diff`
|
|
214
|
+
| `ixt tool add <pkg>` | Add a tool to ixt.toml without installing |
|
|
215
|
+
| `ixt tool install <pkg>` | Install a tool in isolation |
|
|
216
|
+
| `ixt tool install <pkg> --bare` | Install the env without exposing any binary |
|
|
217
|
+
| `ixt tool uninstall <pkg>` | Remove a tool |
|
|
218
|
+
| `ixt tool list` | List installed tools |
|
|
219
|
+
| `ixt tool info <pkg>` | Show tool details |
|
|
220
|
+
| `ixt tool upgrade <pkg>` | Upgrade a tool |
|
|
221
|
+
| `ixt tool upgrade --all` | Upgrade all tools |
|
|
222
|
+
| `ixt tool apply` | Sync tools from declarative config |
|
|
223
|
+
| `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
|
|
224
|
+
| `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
|
|
225
|
+
| `ixt lock write` | Resolve ixt.toml and write ixt.lock |
|
|
226
|
+
| `ixt lock check` | Validate ixt.lock against ixt.toml locally |
|
|
227
|
+
| `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
|
|
225
228
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
226
|
-
| `ixt tool config <pkg> expose <rules...>`
|
|
227
|
-
| `ixt tool config <pkg> inject <dep>`
|
|
228
|
-
| `ixt tool config <pkg> uninject <dep>`
|
|
229
|
-
| `ixt tool config __all__ expose <rules...>`
|
|
230
|
-
| `ixt tool shell <pkg>`
|
|
231
|
-
| `ixt tool where <pkg>`
|
|
229
|
+
| `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
|
|
230
|
+
| `ixt tool config <pkg> inject <dep>` | Inject a package into the tool's env |
|
|
231
|
+
| `ixt tool config <pkg> uninject <dep>` | Remove an injected package |
|
|
232
|
+
| `ixt tool config __all__ expose <rules...>` | Re-expose every installed tool |
|
|
233
|
+
| `ixt tool shell <pkg>` | Open a subshell in tool's env |
|
|
234
|
+
| `ixt tool where <pkg>` | Print the tool env path |
|
|
235
|
+
| `ixt exec [--sudo] <tool> -- <args...>` | Run an exposed tool by name, without relying on PATH |
|
|
232
236
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
233
237
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
234
238
|
| `ixt cache info` | Show cache paths and sizes |
|
|
235
239
|
| `ixt cache prune` | Keep current + previous indexed download artifacts per repository |
|
|
236
240
|
| `ixt cache clear {downloads,metadata,tmp,all}` | Wipe cache(s); never touches installed tools |
|
|
237
|
-
| `ixt runtime info`
|
|
238
|
-
| `ixt runtime prune [--all]`
|
|
241
|
+
| `ixt runtime info` | Show ixt-managed runtime state |
|
|
242
|
+
| `ixt runtime prune [--all]` | Remove unused or all ixt-managed runtimes |
|
|
239
243
|
| `ixt setup path` | Add `$IXT_HOME/installed/bin` to PATH |
|
|
240
244
|
| `ixt setup completions --shell zsh` | Print shell completion code for ixt |
|
|
241
245
|
|
|
@@ -285,7 +289,7 @@ ixt is **not** a runtime version manager (mise, proto, asdf cover that). It mana
|
|
|
285
289
|
git clone https://gitlab.com/pytgaen-group/ixt.git && cd ixt
|
|
286
290
|
uv sync --group dev
|
|
287
291
|
|
|
288
|
-
uv run pytest #
|
|
292
|
+
uv run pytest # hermetic unit suite
|
|
289
293
|
uv run ruff check src/ tests/ # lint
|
|
290
294
|
uv run ruff format src/ tests/ # format
|
|
291
295
|
```
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
*Zero third-party deps. Zero system pollution. One command, any ecosystem.*
|
|
23
23
|
|
|
24
24
|
- **Day-one GitHub releases** — install a tool the day it ships, without
|
|
25
|
-
|
|
25
|
+
waiting for a formula, registry entry, or plugin.
|
|
26
26
|
- **No system runtime needed** — ixt is pure Python stdlib; `uv` and `bun` are
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
bootstrapped on first use, so the host does not need Node, npm, or a package
|
|
28
|
+
manager pre-installed.
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
31
|
ixt tool install ruff # Python (PyPI)
|
|
@@ -173,40 +173,44 @@ ixt tool apply --locked --strict
|
|
|
173
173
|
|
|
174
174
|
`--locked` replays the exact lock instead of resolving newer versions.
|
|
175
175
|
`--strict` fails before changing the machine if `ixt.toml` and `ixt.lock`
|
|
176
|
-
diverge.
|
|
176
|
+
diverge. The strict preflight is local: it checks the manifest fingerprint,
|
|
177
|
+
declared policy, version constraints, and locked binary metadata without
|
|
178
|
+
contacting PyPI, npm, GitHub, or GitLab. Run `ixt lock write` when you want to
|
|
179
|
+
resolve upstream again and refresh the lock.
|
|
177
180
|
|
|
178
181
|
## 🧰 Commands
|
|
179
182
|
|
|
180
183
|
| Command | Description |
|
|
181
184
|
| ---------------------------------------------- | --------------------------------------------------------------------- |
|
|
182
|
-
| `ixt tool add <pkg>`
|
|
183
|
-
| `ixt tool install <pkg>`
|
|
184
|
-
| `ixt tool install <pkg> --bare`
|
|
185
|
-
| `ixt tool uninstall <pkg>`
|
|
186
|
-
| `ixt tool list`
|
|
187
|
-
| `ixt tool info <pkg>`
|
|
188
|
-
| `ixt tool upgrade <pkg>`
|
|
189
|
-
| `ixt tool upgrade --all`
|
|
190
|
-
| `ixt tool apply`
|
|
191
|
-
| `ixt tool apply --locked --strict`
|
|
192
|
-
| `ixt tool export`
|
|
193
|
-
| `ixt lock write`
|
|
194
|
-
| `ixt lock check`
|
|
195
|
-
| `ixt lock diff`
|
|
185
|
+
| `ixt tool add <pkg>` | Add a tool to ixt.toml without installing |
|
|
186
|
+
| `ixt tool install <pkg>` | Install a tool in isolation |
|
|
187
|
+
| `ixt tool install <pkg> --bare` | Install the env without exposing any binary |
|
|
188
|
+
| `ixt tool uninstall <pkg>` | Remove a tool |
|
|
189
|
+
| `ixt tool list` | List installed tools |
|
|
190
|
+
| `ixt tool info <pkg>` | Show tool details |
|
|
191
|
+
| `ixt tool upgrade <pkg>` | Upgrade a tool |
|
|
192
|
+
| `ixt tool upgrade --all` | Upgrade all tools |
|
|
193
|
+
| `ixt tool apply` | Sync tools from declarative config |
|
|
194
|
+
| `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
|
|
195
|
+
| `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
|
|
196
|
+
| `ixt lock write` | Resolve ixt.toml and write ixt.lock |
|
|
197
|
+
| `ixt lock check` | Validate ixt.lock against ixt.toml locally |
|
|
198
|
+
| `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
|
|
196
199
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
197
|
-
| `ixt tool config <pkg> expose <rules...>`
|
|
198
|
-
| `ixt tool config <pkg> inject <dep>`
|
|
199
|
-
| `ixt tool config <pkg> uninject <dep>`
|
|
200
|
-
| `ixt tool config __all__ expose <rules...>`
|
|
201
|
-
| `ixt tool shell <pkg>`
|
|
202
|
-
| `ixt tool where <pkg>`
|
|
200
|
+
| `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
|
|
201
|
+
| `ixt tool config <pkg> inject <dep>` | Inject a package into the tool's env |
|
|
202
|
+
| `ixt tool config <pkg> uninject <dep>` | Remove an injected package |
|
|
203
|
+
| `ixt tool config __all__ expose <rules...>` | Re-expose every installed tool |
|
|
204
|
+
| `ixt tool shell <pkg>` | Open a subshell in tool's env |
|
|
205
|
+
| `ixt tool where <pkg>` | Print the tool env path |
|
|
206
|
+
| `ixt exec [--sudo] <tool> -- <args...>` | Run an exposed tool by name, without relying on PATH |
|
|
203
207
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
204
208
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
205
209
|
| `ixt cache info` | Show cache paths and sizes |
|
|
206
210
|
| `ixt cache prune` | Keep current + previous indexed download artifacts per repository |
|
|
207
211
|
| `ixt cache clear {downloads,metadata,tmp,all}` | Wipe cache(s); never touches installed tools |
|
|
208
|
-
| `ixt runtime info`
|
|
209
|
-
| `ixt runtime prune [--all]`
|
|
212
|
+
| `ixt runtime info` | Show ixt-managed runtime state |
|
|
213
|
+
| `ixt runtime prune [--all]` | Remove unused or all ixt-managed runtimes |
|
|
210
214
|
| `ixt setup path` | Add `$IXT_HOME/installed/bin` to PATH |
|
|
211
215
|
| `ixt setup completions --shell zsh` | Print shell completion code for ixt |
|
|
212
216
|
|
|
@@ -256,7 +260,7 @@ ixt is **not** a runtime version manager (mise, proto, asdf cover that). It mana
|
|
|
256
260
|
git clone https://gitlab.com/pytgaen-group/ixt.git && cd ixt
|
|
257
261
|
uv sync --group dev
|
|
258
262
|
|
|
259
|
-
uv run pytest #
|
|
263
|
+
uv run pytest # hermetic unit suite
|
|
260
264
|
uv run ruff check src/ tests/ # lint
|
|
261
265
|
uv run ruff format src/ tests/ # format
|
|
262
266
|
```
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ixt-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.10.0" # to bump
|
|
8
8
|
description = "Universal CLI tool isolator - manage Python, Node, and binary tools with per-tool isolation"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -61,14 +61,15 @@ include = [
|
|
|
61
61
|
# Testing
|
|
62
62
|
[tool.pytest.ini_options]
|
|
63
63
|
minversion = "7.0"
|
|
64
|
-
testpaths = ["tests"]
|
|
64
|
+
testpaths = ["tests/unit"]
|
|
65
65
|
python_files = ["test_*.py"]
|
|
66
66
|
python_classes = ["Test*"]
|
|
67
67
|
python_functions = ["test_*"]
|
|
68
|
-
addopts =
|
|
68
|
+
addopts = '-ra --strict-markers -m "not slow and not network"'
|
|
69
69
|
markers = [
|
|
70
70
|
"unit: unit tests",
|
|
71
71
|
"integration: integration tests",
|
|
72
|
+
"e2e: end-to-end tests against an installed ixt CLI",
|
|
72
73
|
"platform: platform-specific tests (linux, macos, windows)",
|
|
73
74
|
"slow: tests that bootstrap external runtimes (uv, bun) — skip by default with -m 'not slow'",
|
|
74
75
|
"network: tests that hit the public internet (e.g. real GitHub releases) — opt in with -m 'network'",
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
5
6
|
import json
|
|
7
|
+
import os
|
|
6
8
|
import re
|
|
7
9
|
from dataclasses import dataclass, field
|
|
8
10
|
from pathlib import Path
|
|
@@ -18,6 +20,7 @@ from ixt.config.flags import is_setup_toml_enabled
|
|
|
18
20
|
from ixt.config.heuristics import (
|
|
19
21
|
get_name_suffix_tokens,
|
|
20
22
|
get_platform_aliases,
|
|
23
|
+
is_asset_arch_compatible,
|
|
21
24
|
load_heuristics,
|
|
22
25
|
select_asset,
|
|
23
26
|
)
|
|
@@ -29,7 +32,10 @@ from ixt.config.settings import Settings, get_settings
|
|
|
29
32
|
from ixt.core.backend import BackendType
|
|
30
33
|
from ixt.core.discover import find_executables
|
|
31
34
|
from ixt.core.extract import extract, is_archive
|
|
35
|
+
from ixt.libs.elf import PREFIX_SIZE as ELF_PREFIX_SIZE
|
|
36
|
+
from ixt.libs.elf import parse_elf, read_elf
|
|
32
37
|
from ixt.libs.logger import get_logger, get_verbosity
|
|
38
|
+
from ixt.net.http import MAX_DOWNLOAD_SIZE, DownloadTooLargeError, get_range
|
|
33
39
|
from ixt.net.source import Asset, Release, ReleaseSource, RepoSpec, parse_spec
|
|
34
40
|
from ixt.platform import get_arch, get_os
|
|
35
41
|
|
|
@@ -45,6 +51,9 @@ __all__ = [
|
|
|
45
51
|
"make_source",
|
|
46
52
|
]
|
|
47
53
|
|
|
54
|
+
BINARY_ASSET_CACHE_SCHEMA = 2
|
|
55
|
+
_CACHE_ARCHIVE_SUFFIXES = (".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".txz", ".zip")
|
|
56
|
+
|
|
48
57
|
|
|
49
58
|
# 3.10 lacks typing.NotRequired (added in 3.11) and the project bans
|
|
50
59
|
# third-party deps (typing_extensions). The required-base + total=False
|
|
@@ -82,14 +91,27 @@ class SetupMetadata(_SetupMetadataRequired, total=False):
|
|
|
82
91
|
class AssetSelectionError(RuntimeError):
|
|
83
92
|
"""Raised when no suitable asset could be selected for the current platform."""
|
|
84
93
|
|
|
85
|
-
def __init__(
|
|
94
|
+
def __init__(
|
|
95
|
+
self,
|
|
96
|
+
spec: str,
|
|
97
|
+
release_tag: str,
|
|
98
|
+
asset_names: list[str],
|
|
99
|
+
unusable: list[tuple[str, str]] | None = None,
|
|
100
|
+
):
|
|
86
101
|
self.spec = spec
|
|
87
102
|
self.release_tag = release_tag
|
|
88
103
|
self.asset_names = asset_names
|
|
104
|
+
self.unusable = unusable or []
|
|
89
105
|
names = ", ".join(asset_names[:10]) or "<none>"
|
|
90
|
-
|
|
106
|
+
message = (
|
|
91
107
|
f"No suitable asset found for '{spec}' (release {release_tag}). Available: {names}"
|
|
92
108
|
)
|
|
109
|
+
if self.unusable:
|
|
110
|
+
skipped = ", ".join(
|
|
111
|
+
f"{name} (needs missing loader {loader})" for name, loader in self.unusable
|
|
112
|
+
)
|
|
113
|
+
message += f". Skipped as unrunnable here: {skipped}"
|
|
114
|
+
super().__init__(message)
|
|
93
115
|
|
|
94
116
|
|
|
95
117
|
class BinaryPlatformError(RuntimeError):
|
|
@@ -267,10 +289,51 @@ def _cache_dir_for(settings: Settings, repo_spec: RepoSpec) -> Path:
|
|
|
267
289
|
return settings.cache_dir / host / f"{repo_spec.owner}-{repo_spec.repo}"
|
|
268
290
|
|
|
269
291
|
|
|
270
|
-
def
|
|
271
|
-
"""Return the
|
|
272
|
-
|
|
273
|
-
|
|
292
|
+
def _asset_cache_key(repo_spec: RepoSpec, asset: Asset, *, tag: str) -> str:
|
|
293
|
+
"""Return the stable cache identity for one exact release asset."""
|
|
294
|
+
identity = json.dumps(
|
|
295
|
+
{
|
|
296
|
+
"schema": BINARY_ASSET_CACHE_SCHEMA,
|
|
297
|
+
"platform": repo_spec.platform,
|
|
298
|
+
"host": repo_spec.host,
|
|
299
|
+
"owner": repo_spec.owner,
|
|
300
|
+
"repo": repo_spec.repo,
|
|
301
|
+
"tag": tag,
|
|
302
|
+
"asset": asset.name,
|
|
303
|
+
"url": asset.url,
|
|
304
|
+
},
|
|
305
|
+
sort_keys=True,
|
|
306
|
+
separators=(",", ":"),
|
|
307
|
+
)
|
|
308
|
+
return hashlib.sha256(identity.encode()).hexdigest()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _asset_cache_path(
|
|
312
|
+
downloads_dir: Path,
|
|
313
|
+
repo_spec: RepoSpec,
|
|
314
|
+
asset: Asset,
|
|
315
|
+
*,
|
|
316
|
+
tag: str,
|
|
317
|
+
) -> Path:
|
|
318
|
+
"""Return a portable path for one exact release asset."""
|
|
319
|
+
lower_name = asset.name.lower()
|
|
320
|
+
suffix = next((item for item in _CACHE_ARCHIVE_SUFFIXES if lower_name.endswith(item)), "")
|
|
321
|
+
if not suffix:
|
|
322
|
+
match = re.search(r"\.[A-Za-z0-9]{1,10}$", asset.name)
|
|
323
|
+
suffix = match.group(0) if match is not None else ""
|
|
324
|
+
key = _asset_cache_key(repo_spec, asset, tag=tag)
|
|
325
|
+
return downloads_dir / f"assets-v{BINARY_ASSET_CACHE_SCHEMA}" / key / f"asset{suffix}"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _cached_asset(path: Path, expected_size: int = 0) -> Path | None:
|
|
329
|
+
"""Return *path* when it is a non-empty cache entry of the expected size."""
|
|
330
|
+
try:
|
|
331
|
+
if not path.is_file():
|
|
332
|
+
return None
|
|
333
|
+
size = path.stat().st_size
|
|
334
|
+
except OSError:
|
|
335
|
+
return None
|
|
336
|
+
if 0 < size <= MAX_DOWNLOAD_SIZE and (expected_size <= 0 or size == expected_size):
|
|
274
337
|
return path
|
|
275
338
|
return None
|
|
276
339
|
|
|
@@ -533,9 +596,30 @@ class BinaryBackend:
|
|
|
533
596
|
log.debug(f"user-forced asset_pattern: {asset_pattern!r}")
|
|
534
597
|
selection_pattern = _selection_pattern(effective_pattern, release.tag)
|
|
535
598
|
if locked_binary is None:
|
|
536
|
-
|
|
599
|
+
try:
|
|
600
|
+
asset = self._select_asset(
|
|
601
|
+
release, selection_pattern, tool_name, spec, repo_spec=repo_spec
|
|
602
|
+
)
|
|
603
|
+
except AssetSelectionError:
|
|
604
|
+
# A synthesized release carries only the asset its pattern named.
|
|
605
|
+
# If that asset cannot run here, the pattern is wrong for this
|
|
606
|
+
# host, not merely stale — relearn it against the full release.
|
|
607
|
+
relearned = self._relearn_release(source, repo_spec, release)
|
|
608
|
+
if relearned is None:
|
|
609
|
+
raise
|
|
610
|
+
release = relearned
|
|
611
|
+
selection_pattern = _selection_pattern(effective_pattern, release.tag)
|
|
612
|
+
asset = self._select_asset(
|
|
613
|
+
release, selection_pattern, tool_name, spec, repo_spec=repo_spec
|
|
614
|
+
)
|
|
537
615
|
else:
|
|
538
616
|
asset = release.assets[0]
|
|
617
|
+
if not is_asset_arch_compatible(
|
|
618
|
+
asset.name,
|
|
619
|
+
heuristics=load_heuristics(),
|
|
620
|
+
declared_platform=locked_binary.platform,
|
|
621
|
+
):
|
|
622
|
+
raise AssetSelectionError(spec, release.tag, [asset.name])
|
|
539
623
|
log.debug(f"selected asset: {asset.name}")
|
|
540
624
|
|
|
541
625
|
archive_path = self._download_asset(
|
|
@@ -680,23 +764,110 @@ class BinaryBackend:
|
|
|
680
764
|
asset_pattern: str | None,
|
|
681
765
|
tool_name: str,
|
|
682
766
|
main_spec: str,
|
|
767
|
+
*,
|
|
768
|
+
repo_spec: RepoSpec | None = None,
|
|
683
769
|
) -> Asset:
|
|
770
|
+
log = get_logger("binary")
|
|
684
771
|
heuristics = load_heuristics()
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
release.tag,
|
|
697
|
-
[a.name for a in release.assets],
|
|
772
|
+
candidates = list(release.assets)
|
|
773
|
+
unusable: list[tuple[str, str]] = []
|
|
774
|
+
|
|
775
|
+
while candidates:
|
|
776
|
+
asset = select_asset(
|
|
777
|
+
candidates,
|
|
778
|
+
tool_name,
|
|
779
|
+
_strip_v_prefix(release.tag),
|
|
780
|
+
heuristics=heuristics,
|
|
781
|
+
asset_pattern=asset_pattern,
|
|
782
|
+
verbose=get_verbosity(),
|
|
698
783
|
)
|
|
699
|
-
|
|
784
|
+
if asset is None:
|
|
785
|
+
break
|
|
786
|
+
|
|
787
|
+
loader = self._unrunnable_loader(asset, repo_spec=repo_spec, tag=release.tag)
|
|
788
|
+
if loader is None:
|
|
789
|
+
return asset
|
|
790
|
+
|
|
791
|
+
log.debug(f"asset skip: {asset.name} needs missing loader {loader}")
|
|
792
|
+
unusable.append((asset.name, loader))
|
|
793
|
+
candidates = [a for a in candidates if a.name != asset.name]
|
|
794
|
+
|
|
795
|
+
raise AssetSelectionError(
|
|
796
|
+
main_spec,
|
|
797
|
+
release.tag,
|
|
798
|
+
[a.name for a in release.assets],
|
|
799
|
+
unusable,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
def _relearn_release(
|
|
803
|
+
self,
|
|
804
|
+
source: ReleaseSource,
|
|
805
|
+
repo_spec: RepoSpec,
|
|
806
|
+
release: Release,
|
|
807
|
+
) -> Release | None:
|
|
808
|
+
"""Re-fetch *release* from the API after its learned pattern proved wrong.
|
|
809
|
+
|
|
810
|
+
The fast path synthesizes a release holding only the asset its pattern
|
|
811
|
+
names, so a pattern pointing at an unrunnable build leaves selection
|
|
812
|
+
with nothing to fall back on. The pattern is dropped and the real asset
|
|
813
|
+
list fetched, which lets scoring reach the runnable siblings.
|
|
814
|
+
|
|
815
|
+
Returns None when the release already came from the API: there is then
|
|
816
|
+
no wider list to fall back to, and the caller should surface the
|
|
817
|
+
original selection error instead.
|
|
818
|
+
"""
|
|
819
|
+
if (release.resolution_source or "").endswith("-api"):
|
|
820
|
+
return None
|
|
821
|
+
|
|
822
|
+
log = get_logger("binary")
|
|
823
|
+
log.debug(
|
|
824
|
+
f"asset pattern for {repo_spec.owner}/{repo_spec.repo} names an unrunnable "
|
|
825
|
+
"asset — invalidating and refetching from the release API"
|
|
826
|
+
)
|
|
827
|
+
from ixt.config import asset_pattern_cache
|
|
828
|
+
|
|
829
|
+
asset_pattern_cache.invalidate(repo_spec.owner, repo_spec.repo)
|
|
830
|
+
self._require_github_api(repo_spec)
|
|
831
|
+
refetched = source.get_release_by_tag(repo_spec.owner, repo_spec.repo, release.tag)
|
|
832
|
+
refetched.resolution_source = f"{repo_spec.platform}-api"
|
|
833
|
+
log.debug(f"release (relearn): {refetched.tag} ({len(refetched.assets)} assets)")
|
|
834
|
+
return refetched
|
|
835
|
+
|
|
836
|
+
def _unrunnable_loader(
|
|
837
|
+
self,
|
|
838
|
+
asset: Asset,
|
|
839
|
+
*,
|
|
840
|
+
repo_spec: RepoSpec | None,
|
|
841
|
+
tag: str,
|
|
842
|
+
) -> str | None:
|
|
843
|
+
"""Return the loader *asset* needs but this host lacks, else None.
|
|
844
|
+
|
|
845
|
+
A cached asset is read from disk; otherwise only the head of the asset
|
|
846
|
+
is fetched over HTTP Range, which keeps a doomed candidate from costing
|
|
847
|
+
a full download. Only bare binaries can be checked this way — an
|
|
848
|
+
archive reveals nothing at offset zero, so those are validated after
|
|
849
|
+
extraction by ``find_executables`` instead.
|
|
850
|
+
|
|
851
|
+
Any doubt resolves to None: a server without Range support, a network
|
|
852
|
+
hiccup, or a truncated prefix all fall through to the normal path.
|
|
853
|
+
"""
|
|
854
|
+
if os.name == "nt" or is_archive(asset.name):
|
|
855
|
+
return None
|
|
856
|
+
if repo_spec is not None:
|
|
857
|
+
cache_path = _asset_cache_path(self.settings.downloads_dir, repo_spec, asset, tag=tag)
|
|
858
|
+
cached = _cached_asset(cache_path, asset.size)
|
|
859
|
+
if cached is not None:
|
|
860
|
+
# Already on disk: judge the real file, no request needed.
|
|
861
|
+
info = read_elf(cached)
|
|
862
|
+
return info.interpreter if info is not None and info.loader_missing() else None
|
|
863
|
+
|
|
864
|
+
head = get_range(asset.url, ELF_PREFIX_SIZE)
|
|
865
|
+
if head is None:
|
|
866
|
+
return None
|
|
867
|
+
info = parse_elf(head)
|
|
868
|
+
if info is None or not info.loader_missing():
|
|
869
|
+
return None
|
|
870
|
+
return info.interpreter
|
|
700
871
|
|
|
701
872
|
def uninstall_package(self, env_dir: Path, package: str) -> None:
|
|
702
873
|
"""No-op — binary tools are atomic (uninstall removes the whole env)."""
|
|
@@ -950,15 +1121,28 @@ class BinaryBackend:
|
|
|
950
1121
|
upgrade: bool = False,
|
|
951
1122
|
) -> Path:
|
|
952
1123
|
"""Download an asset, using cache when available."""
|
|
953
|
-
|
|
1124
|
+
if asset.size > MAX_DOWNLOAD_SIZE:
|
|
1125
|
+
raise DownloadTooLargeError(asset.url, MAX_DOWNLOAD_SIZE, asset.size)
|
|
1126
|
+
|
|
1127
|
+
cache_key = _asset_cache_key(repo_spec, asset, tag=tag)
|
|
1128
|
+
dest = _asset_cache_path(self.settings.downloads_dir, repo_spec, asset, tag=tag)
|
|
1129
|
+
cached = _cached_asset(dest, asset.size)
|
|
954
1130
|
if cached is not None and not upgrade:
|
|
955
|
-
self._record_download(repo_spec, asset, cached, tag=tag)
|
|
1131
|
+
self._record_download(repo_spec, asset, cached, tag=tag, cache_key=cache_key)
|
|
956
1132
|
return cached
|
|
957
1133
|
|
|
958
|
-
|
|
959
|
-
dest
|
|
1134
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
1135
|
+
dest.unlink(missing_ok=True)
|
|
960
1136
|
source.download_asset(asset.url, dest)
|
|
961
|
-
|
|
1137
|
+
if dest.is_file() and dest.stat().st_size > MAX_DOWNLOAD_SIZE:
|
|
1138
|
+
actual_size = dest.stat().st_size
|
|
1139
|
+
dest.unlink(missing_ok=True)
|
|
1140
|
+
raise DownloadTooLargeError(asset.url, MAX_DOWNLOAD_SIZE, actual_size)
|
|
1141
|
+
if _cached_asset(dest, asset.size) is None:
|
|
1142
|
+
dest.unlink(missing_ok=True)
|
|
1143
|
+
expected = f"{asset.size} bytes" if asset.size > 0 else "a non-empty file"
|
|
1144
|
+
raise RuntimeError(f"Downloaded asset {asset.name!r} does not match {expected}")
|
|
1145
|
+
self._record_download(repo_spec, asset, dest, tag=tag, cache_key=cache_key)
|
|
962
1146
|
return dest
|
|
963
1147
|
|
|
964
1148
|
def _record_download(
|
|
@@ -968,6 +1152,7 @@ class BinaryBackend:
|
|
|
968
1152
|
path: Path,
|
|
969
1153
|
*,
|
|
970
1154
|
tag: str,
|
|
1155
|
+
cache_key: str,
|
|
971
1156
|
) -> None:
|
|
972
1157
|
"""Index the downloaded binary asset for later cache pruning."""
|
|
973
1158
|
from ixt.core.cache import record_download
|
|
@@ -984,4 +1169,5 @@ class BinaryBackend:
|
|
|
984
1169
|
asset_url=asset.url,
|
|
985
1170
|
asset_size=asset.size,
|
|
986
1171
|
path=path,
|
|
1172
|
+
cache_key=cache_key,
|
|
987
1173
|
)
|