ixt-cli 0.8.1__tar.gz → 0.9.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.
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/.gitignore +1 -0
- ixt_cli-0.9.1/LICENSE +21 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/PKG-INFO +57 -28
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/README.md +55 -27
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/pyproject.toml +4 -7
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/backends/binary.py +148 -28
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/backends/node.py +2 -4
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/cmd_apply.py +54 -7
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/cmd_config.py +69 -51
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/cmd_install.py +5 -2
- ixt_cli-0.9.1/src/ixt/cli/cmd_lock.py +89 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/commands.py +3 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/parser.py +28 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/asset_pattern_cache.py +2 -12
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/env_policy.py +7 -2
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/flags.py +3 -3
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/heuristics.py +129 -1
- ixt_cli-0.9.1/src/ixt/config/lockfile.py +279 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/models.py +3 -12
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/apply.py +176 -7
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/cache.py +45 -25
- ixt_cli-0.9.1/src/ixt/core/discover.py +322 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/expose.py +106 -42
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/extract.py +84 -12
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/inject.py +51 -20
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/install.py +72 -78
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/install_local.py +51 -63
- ixt_cli-0.9.1/src/ixt/core/lockfile.py +698 -0
- ixt_cli-0.9.1/src/ixt/core/locks.py +112 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/pathlink.py +52 -1
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/resolve_cache.py +4 -12
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/save.py +62 -25
- ixt_cli-0.9.1/src/ixt/core/transactions.py +216 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/upgrade.py +22 -106
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/data/heuristics.toml +9 -3
- ixt_cli-0.9.1/src/ixt/libs/atomic.py +56 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/net/gitlab_api.py +8 -8
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/net/http.py +86 -3
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/net/npm.py +18 -2
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/net/pypi.py +12 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/conftest.py +54 -20
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/README.md +6 -4
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/docker/Dockerfile.alpine +1 -1
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/docker/Dockerfile.fedora +1 -1
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/docker/Dockerfile.ubuntu +1 -1
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/run_e2e_incus.sh +1 -1
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/test_e2e.py +18 -8
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/integration/test_lifecycle.py +3 -1
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_apply.py +409 -2
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_argparse_completion.py +1 -1
- ixt_cli-0.9.1/tests/unit/test_atomic.py +61 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_binary_backend.py +467 -40
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_cache.py +49 -2
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_cli.py +49 -1
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_discover.py +186 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_env_policy.py +18 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_export.py +1 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_expose.py +76 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_extract.py +60 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_gitlab_api.py +41 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_heuristics.py +80 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_http.py +127 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_install.py +135 -1
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_install_local.py +30 -4
- ixt_cli-0.9.1/tests/unit/test_lock_write.py +863 -0
- ixt_cli-0.9.1/tests/unit/test_lockfile.py +195 -0
- ixt_cli-0.9.1/tests/unit/test_pathlink.py +145 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_save.py +46 -0
- ixt_cli-0.9.1/tests/unit/test_transactions.py +92 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_upgrade.py +33 -20
- ixt_cli-0.8.1/src/ixt/core/discover.py +0 -150
- ixt_cli-0.8.1/src/ixt/core/locks.py +0 -54
- ixt_cli-0.8.1/tests/unit/test_pathlink.py +0 -62
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/__main__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/backends/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/backends/binary_resolver.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/backends/python.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/argparse_completion.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/cmd_cache.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/cmd_info.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/cmd_misc.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/cmd_registry.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/cmd_upgrade.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/cli/render.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/asset_index.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/fs_policy.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/registry.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/settings.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/setup_toml.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/config/toml.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/apply_actions.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/backend.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/bootstrap.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/doctor.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/export.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/hooks.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/identity.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/resolution_stats.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/resolve.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/runtimes.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/setup_completions.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/core/setup_path.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/data/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/data/asset_index.json +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/data/registry.toml +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/libs/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/libs/constants.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/libs/fmt.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/libs/logger.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/libs/output.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/libs/req_spec.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/libs/semver.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/libs/shell.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/libs/style.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/net/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/net/github_api.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/net/source.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/platform/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/src/ixt/platform/win.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/conftest.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/playground_incus.sh +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/run_e2e_docker.sh +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/e2e/runtime_matrix.sh +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/integration/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/integration/test_policy_runtime.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/__init__.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_asset_index.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_asset_pattern_cache.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_backend.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_bootstrap.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_doctor.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_flags.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_fs_policy.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_github_api.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_hooks.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_inject.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_models.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_namespacing.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_node_backend.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_output.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_platform.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_python_backend.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_registry.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_req_spec.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_resolve.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_resolve_cache.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_runtimes.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_semver.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_settings.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_setup_path.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_setup_toml.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_source.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_style.py +0 -0
- {ixt_cli-0.8.1 → ixt_cli-0.9.1}/tests/unit/test_toml.py +0 -0
ixt_cli-0.9.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gaëtan Montury
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ixt-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
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
|
|
@@ -8,6 +8,7 @@ Project-URL: Issues, https://gitlab.com/pytgaen-group/ixt/-/issues
|
|
|
8
8
|
Project-URL: Documentation, https://ixt-39f604.gitlab.io
|
|
9
9
|
Author-email: Gaëtan Montury <10528250-pytgaen@users.noreply.gitlab.com>
|
|
10
10
|
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
11
12
|
Keywords: cli,github,isolation,npm,package-manager,pypi,tool
|
|
12
13
|
Classifier: Development Status :: 4 - Beta
|
|
13
14
|
Classifier: Environment :: Console
|
|
@@ -33,8 +34,10 @@ Description-Content-Type: text/markdown
|
|
|
33
34
|
<h1 align="center">ixt — Universal CLI Tool Isolator</h1>
|
|
34
35
|
|
|
35
36
|
<p align="center">
|
|
36
|
-
<a href="https://gitlab.com/pytgaen-group/ixt/-/
|
|
37
|
-
<a href="https://gitlab.com/pytgaen-group/ixt/-/
|
|
37
|
+
<a href="https://gitlab.com/pytgaen-group/ixt/-/tags"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F81169049%2Frepository%2Ftags%3Fper_page%3D1&query=%24%5B0%5D.name&label=release" alt="Release"></a>
|
|
38
|
+
<a href="https://gitlab.com/pytgaen-group/ixt/-/blob/main/.gitlab-ci.yml"><img src="https://img.shields.io/badge/build-GitLab%20CI-blue?logo=gitlab" alt="Build: GitLab CI"></a>
|
|
39
|
+
<a href="https://pypi.org/project/ixt-cli/"><img src="https://img.shields.io/pypi/v/ixt-cli?label=pypi" alt="PyPI"></a>
|
|
40
|
+
<a href="https://pepy.tech/projects/ixt-cli"><img src="https://static.pepy.tech/badge/ixt-cli/month" alt="PyPI Downloads"></a>
|
|
38
41
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
|
|
39
42
|
<img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+">
|
|
40
43
|
</p>
|
|
@@ -48,10 +51,10 @@ Description-Content-Type: text/markdown
|
|
|
48
51
|
*Zero third-party deps. Zero system pollution. One command, any ecosystem.*
|
|
49
52
|
|
|
50
53
|
- **Day-one GitHub releases** — install a tool the day it ships, without
|
|
51
|
-
|
|
54
|
+
waiting for a formula, registry entry, or plugin.
|
|
52
55
|
- **No system runtime needed** — ixt is pure Python stdlib; `uv` and `bun` are
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
bootstrapped on first use, so the host does not need Node, npm, or a package
|
|
57
|
+
manager pre-installed.
|
|
55
58
|
|
|
56
59
|
```bash
|
|
57
60
|
ixt tool install ruff # Python (PyPI)
|
|
@@ -183,34 +186,59 @@ ixt tool install sharkdp/fd@10 # latest 10.x.x
|
|
|
183
186
|
ixt tool install ruff>=0.5.0 # PEP 508 (Python)
|
|
184
187
|
```
|
|
185
188
|
|
|
189
|
+
## 🔒 Declarative locks
|
|
190
|
+
|
|
191
|
+
`ixt.toml` is the human-edited intent: which tools you want, with optional
|
|
192
|
+
version bounds, exposure rules, and injections. `ixt.lock` is generated from
|
|
193
|
+
that intent and records the exact resolved versions and binary release assets.
|
|
194
|
+
|
|
195
|
+
For teams and CI, commit both files:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
ixt lock write
|
|
199
|
+
ixt lock check
|
|
200
|
+
ixt tool apply --locked --strict
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`--locked` replays the exact lock instead of resolving newer versions.
|
|
204
|
+
`--strict` fails before changing the machine if `ixt.toml` and `ixt.lock`
|
|
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.
|
|
209
|
+
|
|
186
210
|
## 🧰 Commands
|
|
187
211
|
|
|
188
212
|
| Command | Description |
|
|
189
213
|
| ---------------------------------------------- | --------------------------------------------------------------------- |
|
|
190
|
-
| `ixt tool add <pkg>`
|
|
191
|
-
| `ixt tool install <pkg>`
|
|
192
|
-
| `ixt tool install <pkg> --bare`
|
|
193
|
-
| `ixt tool uninstall <pkg>`
|
|
194
|
-
| `ixt tool list`
|
|
195
|
-
| `ixt tool info <pkg>`
|
|
196
|
-
| `ixt tool upgrade <pkg>`
|
|
197
|
-
| `ixt tool upgrade --all`
|
|
198
|
-
| `ixt tool apply`
|
|
199
|
-
| `ixt tool
|
|
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 |
|
|
200
228
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
201
|
-
| `ixt tool config <pkg> expose <rules...>`
|
|
202
|
-
| `ixt tool config <pkg> inject <dep>`
|
|
203
|
-
| `ixt tool config <pkg> uninject <dep>`
|
|
204
|
-
| `ixt tool config __all__ expose <rules...>`
|
|
205
|
-
| `ixt tool shell <pkg>`
|
|
206
|
-
| `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 |
|
|
207
235
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
208
236
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
209
237
|
| `ixt cache info` | Show cache paths and sizes |
|
|
210
238
|
| `ixt cache prune` | Keep current + previous indexed download artifacts per repository |
|
|
211
239
|
| `ixt cache clear {downloads,metadata,tmp,all}` | Wipe cache(s); never touches installed tools |
|
|
212
|
-
| `ixt runtime info`
|
|
213
|
-
| `ixt runtime prune [--all]`
|
|
240
|
+
| `ixt runtime info` | Show ixt-managed runtime state |
|
|
241
|
+
| `ixt runtime prune [--all]` | Remove unused or all ixt-managed runtimes |
|
|
214
242
|
| `ixt setup path` | Add `$IXT_HOME/installed/bin` to PATH |
|
|
215
243
|
| `ixt setup completions --shell zsh` | Print shell completion code for ixt |
|
|
216
244
|
|
|
@@ -219,7 +247,8 @@ ixt tool install ruff>=0.5.0 # PEP 508 (Python)
|
|
|
219
247
|
```
|
|
220
248
|
$IXT_HOME/ # default ~/.local/share/ixt (respects $XDG_DATA_HOME)
|
|
221
249
|
├── config/
|
|
222
|
-
│ ├── ixt.toml
|
|
250
|
+
│ ├── ixt.toml # global declarative manifest fallback
|
|
251
|
+
│ ├── ixt.lock # global lock when using the global ixt.toml
|
|
223
252
|
│ ├── registry.toml
|
|
224
253
|
│ └── heuristics.toml
|
|
225
254
|
└── installed/
|
|
@@ -259,17 +288,17 @@ ixt is **not** a runtime version manager (mise, proto, asdf cover that). It mana
|
|
|
259
288
|
git clone https://gitlab.com/pytgaen-group/ixt.git && cd ixt
|
|
260
289
|
uv sync --group dev
|
|
261
290
|
|
|
262
|
-
uv run pytest #
|
|
291
|
+
uv run pytest # hermetic unit suite
|
|
263
292
|
uv run ruff check src/ tests/ # lint
|
|
264
293
|
uv run ruff format src/ tests/ # format
|
|
265
294
|
```
|
|
266
295
|
|
|
267
296
|
## 🤝 Contributing
|
|
268
297
|
|
|
269
|
-
Development happens on **GitLab**: <https://gitlab.com/pytgaen-group/ixt>.
|
|
298
|
+
Development happens on **GitLab**: <https://gitlab.com/pytgaen-group/ixt>. Any GitHub repository for this project is a **read-only mirror** of GitLab; it is not the review or issue tracker. Please open issues and merge requests on GitLab.
|
|
270
299
|
|
|
271
300
|
Bug reports and reproduction recipes are welcome. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for details, or open an issue to start a discussion.
|
|
272
301
|
|
|
273
302
|
## 📄 License
|
|
274
303
|
|
|
275
|
-
MIT
|
|
304
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
<h1 align="center">ixt — Universal CLI Tool Isolator</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<a href="https://gitlab.com/pytgaen-group/ixt/-/
|
|
9
|
-
<a href="https://gitlab.com/pytgaen-group/ixt/-/
|
|
8
|
+
<a href="https://gitlab.com/pytgaen-group/ixt/-/tags"><img src="https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgitlab.com%2Fapi%2Fv4%2Fprojects%2F81169049%2Frepository%2Ftags%3Fper_page%3D1&query=%24%5B0%5D.name&label=release" alt="Release"></a>
|
|
9
|
+
<a href="https://gitlab.com/pytgaen-group/ixt/-/blob/main/.gitlab-ci.yml"><img src="https://img.shields.io/badge/build-GitLab%20CI-blue?logo=gitlab" alt="Build: GitLab CI"></a>
|
|
10
|
+
<a href="https://pypi.org/project/ixt-cli/"><img src="https://img.shields.io/pypi/v/ixt-cli?label=pypi" alt="PyPI"></a>
|
|
11
|
+
<a href="https://pepy.tech/projects/ixt-cli"><img src="https://static.pepy.tech/badge/ixt-cli/month" alt="PyPI Downloads"></a>
|
|
10
12
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
|
|
11
13
|
<img src="https://img.shields.io/badge/python-3.10%2B-blue.svg" alt="Python 3.10+">
|
|
12
14
|
</p>
|
|
@@ -20,10 +22,10 @@
|
|
|
20
22
|
*Zero third-party deps. Zero system pollution. One command, any ecosystem.*
|
|
21
23
|
|
|
22
24
|
- **Day-one GitHub releases** — install a tool the day it ships, without
|
|
23
|
-
|
|
25
|
+
waiting for a formula, registry entry, or plugin.
|
|
24
26
|
- **No system runtime needed** — ixt is pure Python stdlib; `uv` and `bun` are
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
bootstrapped on first use, so the host does not need Node, npm, or a package
|
|
28
|
+
manager pre-installed.
|
|
27
29
|
|
|
28
30
|
```bash
|
|
29
31
|
ixt tool install ruff # Python (PyPI)
|
|
@@ -155,34 +157,59 @@ ixt tool install sharkdp/fd@10 # latest 10.x.x
|
|
|
155
157
|
ixt tool install ruff>=0.5.0 # PEP 508 (Python)
|
|
156
158
|
```
|
|
157
159
|
|
|
160
|
+
## 🔒 Declarative locks
|
|
161
|
+
|
|
162
|
+
`ixt.toml` is the human-edited intent: which tools you want, with optional
|
|
163
|
+
version bounds, exposure rules, and injections. `ixt.lock` is generated from
|
|
164
|
+
that intent and records the exact resolved versions and binary release assets.
|
|
165
|
+
|
|
166
|
+
For teams and CI, commit both files:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
ixt lock write
|
|
170
|
+
ixt lock check
|
|
171
|
+
ixt tool apply --locked --strict
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`--locked` replays the exact lock instead of resolving newer versions.
|
|
175
|
+
`--strict` fails before changing the machine if `ixt.toml` and `ixt.lock`
|
|
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.
|
|
180
|
+
|
|
158
181
|
## 🧰 Commands
|
|
159
182
|
|
|
160
183
|
| Command | Description |
|
|
161
184
|
| ---------------------------------------------- | --------------------------------------------------------------------- |
|
|
162
|
-
| `ixt tool add <pkg>`
|
|
163
|
-
| `ixt tool install <pkg>`
|
|
164
|
-
| `ixt tool install <pkg> --bare`
|
|
165
|
-
| `ixt tool uninstall <pkg>`
|
|
166
|
-
| `ixt tool list`
|
|
167
|
-
| `ixt tool info <pkg>`
|
|
168
|
-
| `ixt tool upgrade <pkg>`
|
|
169
|
-
| `ixt tool upgrade --all`
|
|
170
|
-
| `ixt tool apply`
|
|
171
|
-
| `ixt tool
|
|
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 |
|
|
172
199
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
173
|
-
| `ixt tool config <pkg> expose <rules...>`
|
|
174
|
-
| `ixt tool config <pkg> inject <dep>`
|
|
175
|
-
| `ixt tool config <pkg> uninject <dep>`
|
|
176
|
-
| `ixt tool config __all__ expose <rules...>`
|
|
177
|
-
| `ixt tool shell <pkg>`
|
|
178
|
-
| `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 |
|
|
179
206
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
180
207
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
181
208
|
| `ixt cache info` | Show cache paths and sizes |
|
|
182
209
|
| `ixt cache prune` | Keep current + previous indexed download artifacts per repository |
|
|
183
210
|
| `ixt cache clear {downloads,metadata,tmp,all}` | Wipe cache(s); never touches installed tools |
|
|
184
|
-
| `ixt runtime info`
|
|
185
|
-
| `ixt runtime prune [--all]`
|
|
211
|
+
| `ixt runtime info` | Show ixt-managed runtime state |
|
|
212
|
+
| `ixt runtime prune [--all]` | Remove unused or all ixt-managed runtimes |
|
|
186
213
|
| `ixt setup path` | Add `$IXT_HOME/installed/bin` to PATH |
|
|
187
214
|
| `ixt setup completions --shell zsh` | Print shell completion code for ixt |
|
|
188
215
|
|
|
@@ -191,7 +218,8 @@ ixt tool install ruff>=0.5.0 # PEP 508 (Python)
|
|
|
191
218
|
```
|
|
192
219
|
$IXT_HOME/ # default ~/.local/share/ixt (respects $XDG_DATA_HOME)
|
|
193
220
|
├── config/
|
|
194
|
-
│ ├── ixt.toml
|
|
221
|
+
│ ├── ixt.toml # global declarative manifest fallback
|
|
222
|
+
│ ├── ixt.lock # global lock when using the global ixt.toml
|
|
195
223
|
│ ├── registry.toml
|
|
196
224
|
│ └── heuristics.toml
|
|
197
225
|
└── installed/
|
|
@@ -231,17 +259,17 @@ ixt is **not** a runtime version manager (mise, proto, asdf cover that). It mana
|
|
|
231
259
|
git clone https://gitlab.com/pytgaen-group/ixt.git && cd ixt
|
|
232
260
|
uv sync --group dev
|
|
233
261
|
|
|
234
|
-
uv run pytest #
|
|
262
|
+
uv run pytest # hermetic unit suite
|
|
235
263
|
uv run ruff check src/ tests/ # lint
|
|
236
264
|
uv run ruff format src/ tests/ # format
|
|
237
265
|
```
|
|
238
266
|
|
|
239
267
|
## 🤝 Contributing
|
|
240
268
|
|
|
241
|
-
Development happens on **GitLab**: <https://gitlab.com/pytgaen-group/ixt>.
|
|
269
|
+
Development happens on **GitLab**: <https://gitlab.com/pytgaen-group/ixt>. Any GitHub repository for this project is a **read-only mirror** of GitLab; it is not the review or issue tracker. Please open issues and merge requests on GitLab.
|
|
242
270
|
|
|
243
271
|
Bug reports and reproduction recipes are welcome. See [`CONTRIBUTING.md`](CONTRIBUTING.md) for details, or open an issue to start a discussion.
|
|
244
272
|
|
|
245
273
|
## 📄 License
|
|
246
274
|
|
|
247
|
-
MIT
|
|
275
|
+
MIT. See [`LICENSE`](LICENSE).
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ixt-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.1" # 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"
|
|
@@ -37,10 +37,6 @@ dev = [
|
|
|
37
37
|
"pytest-cov>=4.1",
|
|
38
38
|
"pyright>=1.1",
|
|
39
39
|
]
|
|
40
|
-
# Standalone binary build (Nuitka). Not a runtime dependency — install with
|
|
41
|
-
# `uv sync --group build`. [onefile] pulls zstandard for payload compression.
|
|
42
|
-
# See notes/standalone-build.md.
|
|
43
|
-
build = ["nuitka[onefile]>=2.5"]
|
|
44
40
|
|
|
45
41
|
[project.scripts]
|
|
46
42
|
ixt = "ixt.cli:main"
|
|
@@ -65,14 +61,15 @@ include = [
|
|
|
65
61
|
# Testing
|
|
66
62
|
[tool.pytest.ini_options]
|
|
67
63
|
minversion = "7.0"
|
|
68
|
-
testpaths = ["tests"]
|
|
64
|
+
testpaths = ["tests/unit"]
|
|
69
65
|
python_files = ["test_*.py"]
|
|
70
66
|
python_classes = ["Test*"]
|
|
71
67
|
python_functions = ["test_*"]
|
|
72
|
-
addopts =
|
|
68
|
+
addopts = '-ra --strict-markers -m "not slow and not network"'
|
|
73
69
|
markers = [
|
|
74
70
|
"unit: unit tests",
|
|
75
71
|
"integration: integration tests",
|
|
72
|
+
"e2e: end-to-end tests against an installed ixt CLI",
|
|
76
73
|
"platform: platform-specific tests (linux, macos, windows)",
|
|
77
74
|
"slow: tests that bootstrap external runtimes (uv, bun) — skip by default with -m 'not slow'",
|
|
78
75
|
"network: tests that hit the public internet (e.g. real GitHub releases) — opt in with -m 'network'",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import hashlib
|
|
5
6
|
import json
|
|
6
7
|
import re
|
|
7
8
|
from dataclasses import dataclass, field
|
|
@@ -18,22 +19,27 @@ from ixt.config.flags import is_setup_toml_enabled
|
|
|
18
19
|
from ixt.config.heuristics import (
|
|
19
20
|
get_name_suffix_tokens,
|
|
20
21
|
get_platform_aliases,
|
|
22
|
+
is_asset_arch_compatible,
|
|
21
23
|
load_heuristics,
|
|
22
24
|
select_asset,
|
|
23
25
|
)
|
|
24
26
|
|
|
25
27
|
if TYPE_CHECKING:
|
|
28
|
+
from ixt.config.lockfile import BinaryLockMetadata
|
|
26
29
|
from ixt.config.setup_toml import SetupConfig
|
|
27
30
|
from ixt.config.settings import Settings, get_settings
|
|
28
31
|
from ixt.core.backend import BackendType
|
|
29
32
|
from ixt.core.discover import find_executables
|
|
30
33
|
from ixt.core.extract import extract, is_archive
|
|
31
34
|
from ixt.libs.logger import get_logger, get_verbosity
|
|
35
|
+
from ixt.net.http import MAX_DOWNLOAD_SIZE, DownloadTooLargeError
|
|
32
36
|
from ixt.net.source import Asset, Release, ReleaseSource, RepoSpec, parse_spec
|
|
37
|
+
from ixt.platform import get_arch, get_os
|
|
33
38
|
|
|
34
39
|
__all__ = [
|
|
35
40
|
"AssetSelectionError",
|
|
36
41
|
"BinaryBackend",
|
|
42
|
+
"BinaryPlatformError",
|
|
37
43
|
"BinarySpecError",
|
|
38
44
|
"HookEntry",
|
|
39
45
|
"SetupMetadata",
|
|
@@ -42,6 +48,9 @@ __all__ = [
|
|
|
42
48
|
"make_source",
|
|
43
49
|
]
|
|
44
50
|
|
|
51
|
+
BINARY_ASSET_CACHE_SCHEMA = 2
|
|
52
|
+
_CACHE_ARCHIVE_SUFFIXES = (".tar.bz2", ".tar.gz", ".tar.xz", ".tgz", ".txz", ".zip")
|
|
53
|
+
|
|
45
54
|
|
|
46
55
|
# 3.10 lacks typing.NotRequired (added in 3.11) and the project bans
|
|
47
56
|
# third-party deps (typing_extensions). The required-base + total=False
|
|
@@ -89,6 +98,31 @@ class AssetSelectionError(RuntimeError):
|
|
|
89
98
|
)
|
|
90
99
|
|
|
91
100
|
|
|
101
|
+
class BinaryPlatformError(RuntimeError):
|
|
102
|
+
"""Raised when extracted executables are incompatible with the current platform."""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
spec: str,
|
|
107
|
+
release_tag: str,
|
|
108
|
+
asset_name: str,
|
|
109
|
+
rejected: list[tuple[Path, str]],
|
|
110
|
+
):
|
|
111
|
+
self.spec = spec
|
|
112
|
+
self.release_tag = release_tag
|
|
113
|
+
self.asset_name = asset_name
|
|
114
|
+
self.rejected = rejected
|
|
115
|
+
platform = f"{get_os().value}/{get_arch().value}"
|
|
116
|
+
details = ", ".join(
|
|
117
|
+
f"{path.name} ({format_label})"
|
|
118
|
+
for path, format_label in sorted(rejected, key=lambda item: str(item[0]))
|
|
119
|
+
)
|
|
120
|
+
super().__init__(
|
|
121
|
+
f"No executable compatible with {platform} found for '{spec}' "
|
|
122
|
+
f"(release {release_tag}, asset {asset_name}). Rejected: {details}"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
92
126
|
class BinarySpecError(ValueError):
|
|
93
127
|
"""Raised when a spec cannot be parsed as a binary backend spec."""
|
|
94
128
|
|
|
@@ -239,10 +273,51 @@ def _cache_dir_for(settings: Settings, repo_spec: RepoSpec) -> Path:
|
|
|
239
273
|
return settings.cache_dir / host / f"{repo_spec.owner}-{repo_spec.repo}"
|
|
240
274
|
|
|
241
275
|
|
|
242
|
-
def
|
|
243
|
-
"""Return the
|
|
244
|
-
|
|
245
|
-
|
|
276
|
+
def _asset_cache_key(repo_spec: RepoSpec, asset: Asset, *, tag: str) -> str:
|
|
277
|
+
"""Return the stable cache identity for one exact release asset."""
|
|
278
|
+
identity = json.dumps(
|
|
279
|
+
{
|
|
280
|
+
"schema": BINARY_ASSET_CACHE_SCHEMA,
|
|
281
|
+
"platform": repo_spec.platform,
|
|
282
|
+
"host": repo_spec.host,
|
|
283
|
+
"owner": repo_spec.owner,
|
|
284
|
+
"repo": repo_spec.repo,
|
|
285
|
+
"tag": tag,
|
|
286
|
+
"asset": asset.name,
|
|
287
|
+
"url": asset.url,
|
|
288
|
+
},
|
|
289
|
+
sort_keys=True,
|
|
290
|
+
separators=(",", ":"),
|
|
291
|
+
)
|
|
292
|
+
return hashlib.sha256(identity.encode()).hexdigest()
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _asset_cache_path(
|
|
296
|
+
downloads_dir: Path,
|
|
297
|
+
repo_spec: RepoSpec,
|
|
298
|
+
asset: Asset,
|
|
299
|
+
*,
|
|
300
|
+
tag: str,
|
|
301
|
+
) -> Path:
|
|
302
|
+
"""Return a portable path for one exact release asset."""
|
|
303
|
+
lower_name = asset.name.lower()
|
|
304
|
+
suffix = next((item for item in _CACHE_ARCHIVE_SUFFIXES if lower_name.endswith(item)), "")
|
|
305
|
+
if not suffix:
|
|
306
|
+
match = re.search(r"\.[A-Za-z0-9]{1,10}$", asset.name)
|
|
307
|
+
suffix = match.group(0) if match is not None else ""
|
|
308
|
+
key = _asset_cache_key(repo_spec, asset, tag=tag)
|
|
309
|
+
return downloads_dir / f"assets-v{BINARY_ASSET_CACHE_SCHEMA}" / key / f"asset{suffix}"
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _cached_asset(path: Path, expected_size: int = 0) -> Path | None:
|
|
313
|
+
"""Return *path* when it is a non-empty cache entry of the expected size."""
|
|
314
|
+
try:
|
|
315
|
+
if not path.is_file():
|
|
316
|
+
return None
|
|
317
|
+
size = path.stat().st_size
|
|
318
|
+
except OSError:
|
|
319
|
+
return None
|
|
320
|
+
if 0 < size <= MAX_DOWNLOAD_SIZE and (expected_size <= 0 or size == expected_size):
|
|
246
321
|
return path
|
|
247
322
|
return None
|
|
248
323
|
|
|
@@ -396,6 +471,7 @@ class BinaryBackend:
|
|
|
396
471
|
setup_path: Path | None = None,
|
|
397
472
|
asset_pattern: str | None = None,
|
|
398
473
|
known_tag: str | None = None,
|
|
474
|
+
locked_binary: BinaryLockMetadata | None = None,
|
|
399
475
|
use_cache: bool = True,
|
|
400
476
|
) -> dict | None:
|
|
401
477
|
"""Compatibility adapter for the shared backend protocol."""
|
|
@@ -410,6 +486,7 @@ class BinaryBackend:
|
|
|
410
486
|
setup_path=setup_path,
|
|
411
487
|
asset_pattern=asset_pattern,
|
|
412
488
|
known_tag=known_tag,
|
|
489
|
+
locked_binary=locked_binary,
|
|
413
490
|
use_cache=use_cache,
|
|
414
491
|
)
|
|
415
492
|
|
|
@@ -422,6 +499,7 @@ class BinaryBackend:
|
|
|
422
499
|
setup_path: Path | None = None,
|
|
423
500
|
asset_pattern: str | None = None,
|
|
424
501
|
known_tag: str | None = None,
|
|
502
|
+
locked_binary: BinaryLockMetadata | None = None,
|
|
425
503
|
use_cache: bool = True,
|
|
426
504
|
) -> dict | None:
|
|
427
505
|
"""Resolve, download, extract, and discover one atomic binary tool.
|
|
@@ -460,24 +538,34 @@ class BinaryBackend:
|
|
|
460
538
|
)
|
|
461
539
|
release_pattern_name = setup.name if setup and setup.name else None
|
|
462
540
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
else:
|
|
471
|
-
release = self._fetch_release(
|
|
472
|
-
source,
|
|
473
|
-
repo_spec,
|
|
474
|
-
env_dir=env_dir,
|
|
475
|
-
upgrade=upgrade,
|
|
476
|
-
known_tag=known_tag,
|
|
477
|
-
asset_pattern=release_pattern,
|
|
478
|
-
pattern_name=release_pattern_name,
|
|
541
|
+
if locked_binary is not None:
|
|
542
|
+
if not repo_spec.version:
|
|
543
|
+
raise BinarySpecError("locked binary install requires a pinned release tag")
|
|
544
|
+
release = Release(
|
|
545
|
+
tag=repo_spec.version,
|
|
546
|
+
assets=[Asset(name=locked_binary.asset, url=locked_binary.url)],
|
|
547
|
+
resolution_source="lockfile",
|
|
479
548
|
)
|
|
480
|
-
log.debug(f"release: {release.tag} ({
|
|
549
|
+
log.debug(f"release (lockfile): {release.tag} ({locked_binary.asset})")
|
|
550
|
+
else:
|
|
551
|
+
# Reuse the release already fetched during version resolution (e.g. an
|
|
552
|
+
# upgrade or a dry-run within the TTL window). This skips a second API
|
|
553
|
+
# call — the only option for GitLab, whose asset URLs can't be rebuilt
|
|
554
|
+
# from a pattern like GitHub's.
|
|
555
|
+
release = self._cached_release(spec) if use_cache and not repo_spec.version else None
|
|
556
|
+
if release is not None:
|
|
557
|
+
log.debug(f"release (resolve cache): {release.tag} ({len(release.assets)} assets)")
|
|
558
|
+
else:
|
|
559
|
+
release = self._fetch_release(
|
|
560
|
+
source,
|
|
561
|
+
repo_spec,
|
|
562
|
+
env_dir=env_dir,
|
|
563
|
+
upgrade=upgrade,
|
|
564
|
+
known_tag=known_tag,
|
|
565
|
+
asset_pattern=release_pattern,
|
|
566
|
+
pattern_name=release_pattern_name,
|
|
567
|
+
)
|
|
568
|
+
log.debug(f"release: {release.tag} ({len(release.assets)} assets)")
|
|
481
569
|
|
|
482
570
|
if setup is None and not setup_resolved:
|
|
483
571
|
setup = self._resolve_setup(
|
|
@@ -491,7 +579,16 @@ class BinaryBackend:
|
|
|
491
579
|
if asset_pattern:
|
|
492
580
|
log.debug(f"user-forced asset_pattern: {asset_pattern!r}")
|
|
493
581
|
selection_pattern = _selection_pattern(effective_pattern, release.tag)
|
|
494
|
-
|
|
582
|
+
if locked_binary is None:
|
|
583
|
+
asset = self._select_asset(release, selection_pattern, tool_name, spec)
|
|
584
|
+
else:
|
|
585
|
+
asset = release.assets[0]
|
|
586
|
+
if not is_asset_arch_compatible(
|
|
587
|
+
asset.name,
|
|
588
|
+
heuristics=load_heuristics(),
|
|
589
|
+
declared_platform=locked_binary.platform,
|
|
590
|
+
):
|
|
591
|
+
raise AssetSelectionError(spec, release.tag, [asset.name])
|
|
495
592
|
log.debug(f"selected asset: {asset.name}")
|
|
496
593
|
|
|
497
594
|
archive_path = self._download_asset(
|
|
@@ -512,8 +609,16 @@ class BinaryBackend:
|
|
|
512
609
|
log.debug(f"extracted to: {result.root}")
|
|
513
610
|
|
|
514
611
|
heuristics = load_heuristics()
|
|
515
|
-
|
|
612
|
+
rejected_executables: list[tuple[Path, str]] = []
|
|
613
|
+
executables = find_executables(
|
|
614
|
+
result.root,
|
|
615
|
+
expose=heuristics.expose,
|
|
616
|
+
log=log,
|
|
617
|
+
rejected=rejected_executables,
|
|
618
|
+
)
|
|
516
619
|
log.debug(f"executables: {[e.name for e in executables]}")
|
|
620
|
+
if not executables and rejected_executables:
|
|
621
|
+
raise BinaryPlatformError(spec, release.tag, asset.name, rejected_executables)
|
|
517
622
|
|
|
518
623
|
binaries = {exe.name: str(exe) for exe in executables}
|
|
519
624
|
|
|
@@ -898,15 +1003,28 @@ class BinaryBackend:
|
|
|
898
1003
|
upgrade: bool = False,
|
|
899
1004
|
) -> Path:
|
|
900
1005
|
"""Download an asset, using cache when available."""
|
|
901
|
-
|
|
1006
|
+
if asset.size > MAX_DOWNLOAD_SIZE:
|
|
1007
|
+
raise DownloadTooLargeError(asset.url, MAX_DOWNLOAD_SIZE, asset.size)
|
|
1008
|
+
|
|
1009
|
+
cache_key = _asset_cache_key(repo_spec, asset, tag=tag)
|
|
1010
|
+
dest = _asset_cache_path(self.settings.downloads_dir, repo_spec, asset, tag=tag)
|
|
1011
|
+
cached = _cached_asset(dest, asset.size)
|
|
902
1012
|
if cached is not None and not upgrade:
|
|
903
|
-
self._record_download(repo_spec, asset, cached, tag=tag)
|
|
1013
|
+
self._record_download(repo_spec, asset, cached, tag=tag, cache_key=cache_key)
|
|
904
1014
|
return cached
|
|
905
1015
|
|
|
906
|
-
|
|
907
|
-
dest
|
|
1016
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
1017
|
+
dest.unlink(missing_ok=True)
|
|
908
1018
|
source.download_asset(asset.url, dest)
|
|
909
|
-
|
|
1019
|
+
if dest.is_file() and dest.stat().st_size > MAX_DOWNLOAD_SIZE:
|
|
1020
|
+
actual_size = dest.stat().st_size
|
|
1021
|
+
dest.unlink(missing_ok=True)
|
|
1022
|
+
raise DownloadTooLargeError(asset.url, MAX_DOWNLOAD_SIZE, actual_size)
|
|
1023
|
+
if _cached_asset(dest, asset.size) is None:
|
|
1024
|
+
dest.unlink(missing_ok=True)
|
|
1025
|
+
expected = f"{asset.size} bytes" if asset.size > 0 else "a non-empty file"
|
|
1026
|
+
raise RuntimeError(f"Downloaded asset {asset.name!r} does not match {expected}")
|
|
1027
|
+
self._record_download(repo_spec, asset, dest, tag=tag, cache_key=cache_key)
|
|
910
1028
|
return dest
|
|
911
1029
|
|
|
912
1030
|
def _record_download(
|
|
@@ -916,6 +1034,7 @@ class BinaryBackend:
|
|
|
916
1034
|
path: Path,
|
|
917
1035
|
*,
|
|
918
1036
|
tag: str,
|
|
1037
|
+
cache_key: str,
|
|
919
1038
|
) -> None:
|
|
920
1039
|
"""Index the downloaded binary asset for later cache pruning."""
|
|
921
1040
|
from ixt.core.cache import record_download
|
|
@@ -932,4 +1051,5 @@ class BinaryBackend:
|
|
|
932
1051
|
asset_url=asset.url,
|
|
933
1052
|
asset_size=asset.size,
|
|
934
1053
|
path=path,
|
|
1054
|
+
cache_key=cache_key,
|
|
935
1055
|
)
|