ixt-cli 0.9.1__tar.gz → 0.10.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.9.1 → ixt_cli-0.10.1}/PKG-INFO +4 -3
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/README.md +2 -1
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/pyproject.toml +1 -1
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/__init__.py +1 -3
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/backends/binary.py +407 -51
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/backends/node.py +49 -14
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/__init__.py +12 -2
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/argparse_completion.py +10 -1
- ixt_cli-0.10.1/src/ixt/cli/cmd_exec.py +71 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/cmd_info.py +5 -1
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/cmd_misc.py +24 -1
- ixt_cli-0.10.1/src/ixt/cli/commands.py +89 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/parser.py +39 -9
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/heuristics.py +143 -16
- ixt_cli-0.10.1/src/ixt/core/__init__.py +51 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/discover.py +27 -4
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/doctor.py +56 -0
- ixt_cli-0.10.1/src/ixt/core/exec_tool.py +165 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/upgrade.py +19 -8
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/data/heuristics.toml +37 -0
- ixt_cli-0.10.1/src/ixt/libs/elf.py +174 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/net/http.py +38 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_argparse_completion.py +16 -1
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_binary_backend.py +466 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_cli.py +125 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_discover.py +63 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_doctor.py +94 -0
- ixt_cli-0.10.1/tests/unit/test_elf.py +230 -0
- ixt_cli-0.10.1/tests/unit/test_exec.py +289 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_heuristics.py +125 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_http.py +49 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_node_backend.py +111 -9
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_upgrade.py +74 -28
- ixt_cli-0.9.1/src/ixt/cli/commands.py +0 -73
- ixt_cli-0.9.1/src/ixt/core/__init__.py +0 -16
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/.gitignore +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/LICENSE +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/__main__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/backends/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/backends/binary_resolver.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/backends/python.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/cmd_apply.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/cmd_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/cmd_config.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/cmd_install.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/cmd_lock.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/cmd_registry.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/cmd_upgrade.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/cli/render.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/asset_index.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/asset_pattern_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/env_policy.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/flags.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/fs_policy.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/lockfile.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/models.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/registry.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/settings.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/setup_toml.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/config/toml.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/apply.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/apply_actions.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/backend.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/bootstrap.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/export.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/expose.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/extract.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/hooks.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/identity.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/inject.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/install.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/install_local.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/lockfile.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/locks.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/pathlink.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/resolution_stats.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/resolve.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/resolve_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/runtimes.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/save.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/setup_completions.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/setup_path.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/core/transactions.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/data/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/data/asset_index.json +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/data/registry.toml +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/atomic.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/constants.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/fmt.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/logger.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/output.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/req_spec.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/semver.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/shell.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/libs/style.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/net/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/net/github_api.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/net/gitlab_api.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/net/npm.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/net/pypi.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/net/source.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/platform/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/src/ixt/platform/win.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/conftest.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/README.md +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/conftest.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/docker/Dockerfile.alpine +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/docker/Dockerfile.fedora +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/docker/Dockerfile.ubuntu +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/playground_incus.sh +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/run_e2e_docker.sh +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/run_e2e_incus.sh +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/runtime_matrix.sh +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/e2e/test_e2e.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/integration/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/integration/test_lifecycle.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/integration/test_policy_runtime.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/__init__.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_apply.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_asset_index.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_asset_pattern_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_atomic.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_backend.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_bootstrap.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_env_policy.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_export.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_expose.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_extract.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_flags.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_fs_policy.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_github_api.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_gitlab_api.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_hooks.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_inject.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_install.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_install_local.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_lock_write.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_lockfile.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_models.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_namespacing.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_output.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_pathlink.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_platform.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_python_backend.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_registry.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_req_spec.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_resolve.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_resolve_cache.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_runtimes.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_save.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_semver.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_settings.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_setup_path.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_setup_toml.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_source.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_style.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_toml.py +0 -0
- {ixt_cli-0.9.1 → ixt_cli-0.10.1}/tests/unit/test_transactions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: ixt-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.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
|
|
@@ -223,7 +223,7 @@ resolve upstream again and refresh the lock.
|
|
|
223
223
|
| `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
|
|
224
224
|
| `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
|
|
225
225
|
| `ixt lock write` | Resolve ixt.toml and write ixt.lock |
|
|
226
|
-
| `ixt lock check` | Validate ixt.lock against ixt.toml locally
|
|
226
|
+
| `ixt lock check` | Validate ixt.lock against ixt.toml locally |
|
|
227
227
|
| `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
|
|
228
228
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
229
229
|
| `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
|
|
@@ -232,6 +232,7 @@ resolve upstream again and refresh the lock.
|
|
|
232
232
|
| `ixt tool config __all__ expose <rules...>` | Re-expose every installed tool |
|
|
233
233
|
| `ixt tool shell <pkg>` | Open a subshell in tool's env |
|
|
234
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 |
|
|
235
236
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
236
237
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
237
238
|
| `ixt cache info` | Show cache paths and sizes |
|
|
@@ -194,7 +194,7 @@ resolve upstream again and refresh the lock.
|
|
|
194
194
|
| `ixt tool apply --locked --strict` | Sync using exact versions and assets from ixt.lock |
|
|
195
195
|
| `ixt tool export` | Export installed tools as ixt.toml (stdout), preserves install intent |
|
|
196
196
|
| `ixt lock write` | Resolve ixt.toml and write ixt.lock |
|
|
197
|
-
| `ixt lock check` | Validate ixt.lock against ixt.toml locally
|
|
197
|
+
| `ixt lock check` | Validate ixt.lock against ixt.toml locally |
|
|
198
198
|
| `ixt lock diff` | Show semantic differences between ixt.toml and ixt.lock |
|
|
199
199
|
| `ixt asset-index export` | Export portable binary-resolution metadata as JSON |
|
|
200
200
|
| `ixt tool config <pkg> expose <rules...>` | Change exposed binaries |
|
|
@@ -203,6 +203,7 @@ resolve upstream again and refresh the lock.
|
|
|
203
203
|
| `ixt tool config __all__ expose <rules...>` | Re-expose every installed tool |
|
|
204
204
|
| `ixt tool shell <pkg>` | Open a subshell in tool's env |
|
|
205
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 |
|
|
206
207
|
| `ixt environment [--sizes]` | Show ixt configuration, paths, and optional storage sizes |
|
|
207
208
|
| `ixt doctor` | Check ixt health (runtimes, PATH, caches) |
|
|
208
209
|
| `ixt cache info` | Show cache paths and sizes |
|
|
@@ -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.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"
|
|
@@ -4,7 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import hashlib
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
7
8
|
import re
|
|
9
|
+
import shutil
|
|
8
10
|
from dataclasses import dataclass, field
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import TYPE_CHECKING, TypedDict
|
|
@@ -19,7 +21,9 @@ from ixt.config.flags import is_setup_toml_enabled
|
|
|
19
21
|
from ixt.config.heuristics import (
|
|
20
22
|
get_name_suffix_tokens,
|
|
21
23
|
get_platform_aliases,
|
|
24
|
+
has_any_token,
|
|
22
25
|
is_asset_arch_compatible,
|
|
26
|
+
libc_tokens,
|
|
23
27
|
load_heuristics,
|
|
24
28
|
select_asset,
|
|
25
29
|
)
|
|
@@ -31,14 +35,17 @@ from ixt.config.settings import Settings, get_settings
|
|
|
31
35
|
from ixt.core.backend import BackendType
|
|
32
36
|
from ixt.core.discover import find_executables
|
|
33
37
|
from ixt.core.extract import extract, is_archive
|
|
38
|
+
from ixt.libs.elf import PREFIX_SIZE as ELF_PREFIX_SIZE
|
|
39
|
+
from ixt.libs.elf import parse_elf, read_elf
|
|
34
40
|
from ixt.libs.logger import get_logger, get_verbosity
|
|
35
|
-
from ixt.net.http import MAX_DOWNLOAD_SIZE, DownloadTooLargeError
|
|
41
|
+
from ixt.net.http import MAX_DOWNLOAD_SIZE, DownloadTooLargeError, get_range
|
|
36
42
|
from ixt.net.source import Asset, Release, ReleaseSource, RepoSpec, parse_spec
|
|
37
43
|
from ixt.platform import get_arch, get_os
|
|
38
44
|
|
|
39
45
|
__all__ = [
|
|
40
46
|
"AssetSelectionError",
|
|
41
47
|
"BinaryBackend",
|
|
48
|
+
"BinaryNoExecutableError",
|
|
42
49
|
"BinaryPlatformError",
|
|
43
50
|
"BinarySpecError",
|
|
44
51
|
"HookEntry",
|
|
@@ -88,14 +95,27 @@ class SetupMetadata(_SetupMetadataRequired, total=False):
|
|
|
88
95
|
class AssetSelectionError(RuntimeError):
|
|
89
96
|
"""Raised when no suitable asset could be selected for the current platform."""
|
|
90
97
|
|
|
91
|
-
def __init__(
|
|
98
|
+
def __init__(
|
|
99
|
+
self,
|
|
100
|
+
spec: str,
|
|
101
|
+
release_tag: str,
|
|
102
|
+
asset_names: list[str],
|
|
103
|
+
unusable: list[tuple[str, str]] | None = None,
|
|
104
|
+
):
|
|
92
105
|
self.spec = spec
|
|
93
106
|
self.release_tag = release_tag
|
|
94
107
|
self.asset_names = asset_names
|
|
108
|
+
self.unusable = unusable or []
|
|
95
109
|
names = ", ".join(asset_names[:10]) or "<none>"
|
|
96
|
-
|
|
110
|
+
message = (
|
|
97
111
|
f"No suitable asset found for '{spec}' (release {release_tag}). Available: {names}"
|
|
98
112
|
)
|
|
113
|
+
if self.unusable:
|
|
114
|
+
skipped = ", ".join(
|
|
115
|
+
f"{name} (needs missing loader {loader})" for name, loader in self.unusable
|
|
116
|
+
)
|
|
117
|
+
message += f". Skipped as unrunnable here: {skipped}"
|
|
118
|
+
super().__init__(message)
|
|
99
119
|
|
|
100
120
|
|
|
101
121
|
class BinaryPlatformError(RuntimeError):
|
|
@@ -123,6 +143,78 @@ class BinaryPlatformError(RuntimeError):
|
|
|
123
143
|
)
|
|
124
144
|
|
|
125
145
|
|
|
146
|
+
def _empty_dir(path: Path) -> None:
|
|
147
|
+
"""Remove everything inside *path*, keeping the directory itself."""
|
|
148
|
+
if not path.is_dir():
|
|
149
|
+
return
|
|
150
|
+
for entry in path.iterdir():
|
|
151
|
+
if entry.is_dir() and not entry.is_symlink():
|
|
152
|
+
shutil.rmtree(entry, ignore_errors=True)
|
|
153
|
+
else:
|
|
154
|
+
entry.unlink(missing_ok=True)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _doomed_by_libc(release: Release, rejected: list[tuple[Path, str]], log) -> set[str]:
|
|
158
|
+
"""Return the sibling assets built against a libc this host cannot load.
|
|
159
|
+
|
|
160
|
+
A candidate turned down for a missing loader condemns every asset built
|
|
161
|
+
against the same libc: downloading them to reach the very same verdict is
|
|
162
|
+
pure waste. Must be called while the rejected files are still on disk.
|
|
163
|
+
"""
|
|
164
|
+
loaders = set()
|
|
165
|
+
for path, _label in rejected:
|
|
166
|
+
info = read_elf(path)
|
|
167
|
+
if info is not None and info.interpreter and info.loader_missing():
|
|
168
|
+
loaders.add(info.interpreter)
|
|
169
|
+
if not loaders:
|
|
170
|
+
return set()
|
|
171
|
+
|
|
172
|
+
cfg = load_heuristics()
|
|
173
|
+
tokens = sorted({token for loader in loaders for token in libc_tokens(loader, cfg)})
|
|
174
|
+
if not tokens:
|
|
175
|
+
return set()
|
|
176
|
+
|
|
177
|
+
doomed = {asset.name for asset in release.assets if has_any_token(asset.name, tokens)}
|
|
178
|
+
if doomed:
|
|
179
|
+
log.debug(f"skip (needs {'/'.join(tokens)} loader): {sorted(doomed)}")
|
|
180
|
+
return doomed
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _rejection_reason(rejected: list[tuple[Path, str]]) -> str:
|
|
184
|
+
"""Describe why an unpacked asset yielded nothing runnable."""
|
|
185
|
+
if not rejected:
|
|
186
|
+
return "no executable inside"
|
|
187
|
+
return ", ".join(
|
|
188
|
+
f"{path.name} ({label})" for path, label in sorted(rejected, key=lambda item: str(item[0]))
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class BinaryNoExecutableError(RuntimeError):
|
|
193
|
+
"""Raised when the selected asset extracts without a single runnable file."""
|
|
194
|
+
|
|
195
|
+
def __init__(
|
|
196
|
+
self,
|
|
197
|
+
spec: str,
|
|
198
|
+
release_tag: str,
|
|
199
|
+
asset_name: str,
|
|
200
|
+
alternatives: list[str],
|
|
201
|
+
):
|
|
202
|
+
self.spec = spec
|
|
203
|
+
self.release_tag = release_tag
|
|
204
|
+
self.asset_name = asset_name
|
|
205
|
+
self.alternatives = alternatives
|
|
206
|
+
message = (
|
|
207
|
+
f"No executable found in '{asset_name}' for '{spec}' (release {release_tag}). "
|
|
208
|
+
"The asset carries no runnable file — a shared library, headers or a "
|
|
209
|
+
"source archive rather than the tool itself."
|
|
210
|
+
)
|
|
211
|
+
if alternatives:
|
|
212
|
+
others = ", ".join(alternatives[:10])
|
|
213
|
+
message += f" Other assets in this release: {others}."
|
|
214
|
+
message += " Point --asset-pattern at another asset."
|
|
215
|
+
super().__init__(message)
|
|
216
|
+
|
|
217
|
+
|
|
126
218
|
class BinarySpecError(ValueError):
|
|
127
219
|
"""Raised when a spec cannot be parsed as a binary backend spec."""
|
|
128
220
|
|
|
@@ -578,10 +670,7 @@ class BinaryBackend:
|
|
|
578
670
|
)
|
|
579
671
|
if asset_pattern:
|
|
580
672
|
log.debug(f"user-forced asset_pattern: {asset_pattern!r}")
|
|
581
|
-
|
|
582
|
-
if locked_binary is None:
|
|
583
|
-
asset = self._select_asset(release, selection_pattern, tool_name, spec)
|
|
584
|
-
else:
|
|
673
|
+
if locked_binary is not None:
|
|
585
674
|
asset = release.assets[0]
|
|
586
675
|
if not is_asset_arch_compatible(
|
|
587
676
|
asset.name,
|
|
@@ -589,36 +678,33 @@ class BinaryBackend:
|
|
|
589
678
|
declared_platform=locked_binary.platform,
|
|
590
679
|
):
|
|
591
680
|
raise AssetSelectionError(spec, release.tag, [asset.name])
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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)
|
|
681
|
+
executables = self._unpack(
|
|
682
|
+
source,
|
|
683
|
+
repo_spec,
|
|
684
|
+
release,
|
|
685
|
+
asset,
|
|
686
|
+
env_dir,
|
|
687
|
+
cache_dir,
|
|
688
|
+
spec=spec,
|
|
689
|
+
tool_name=tool_name,
|
|
690
|
+
setup=setup,
|
|
691
|
+
upgrade=upgrade,
|
|
692
|
+
log=log,
|
|
693
|
+
)
|
|
694
|
+
else:
|
|
695
|
+
asset, release, executables = self._unpack_first_runnable(
|
|
696
|
+
source,
|
|
697
|
+
repo_spec,
|
|
698
|
+
release,
|
|
699
|
+
env_dir,
|
|
700
|
+
cache_dir,
|
|
701
|
+
spec=spec,
|
|
702
|
+
tool_name=tool_name,
|
|
703
|
+
setup=setup,
|
|
704
|
+
effective_pattern=effective_pattern,
|
|
705
|
+
upgrade=upgrade,
|
|
706
|
+
log=log,
|
|
707
|
+
)
|
|
622
708
|
|
|
623
709
|
binaries = {exe.name: str(exe) for exe in executables}
|
|
624
710
|
|
|
@@ -727,29 +813,299 @@ class BinaryBackend:
|
|
|
727
813
|
)
|
|
728
814
|
return setup
|
|
729
815
|
|
|
816
|
+
def _unpack_candidate(
|
|
817
|
+
self,
|
|
818
|
+
source: ReleaseSource,
|
|
819
|
+
repo_spec: RepoSpec,
|
|
820
|
+
release: Release,
|
|
821
|
+
asset: Asset,
|
|
822
|
+
env_dir: Path,
|
|
823
|
+
cache_dir: Path,
|
|
824
|
+
*,
|
|
825
|
+
tool_name: str,
|
|
826
|
+
setup: SetupConfig | None,
|
|
827
|
+
upgrade: bool,
|
|
828
|
+
log,
|
|
829
|
+
) -> tuple[list[Path], list[tuple[Path, str]]]:
|
|
830
|
+
"""Download *asset* into *env_dir* and report what can run on this host.
|
|
831
|
+
|
|
832
|
+
Returns the runnable executables and, when there are none, the files
|
|
833
|
+
that were found but rejected as incompatible.
|
|
834
|
+
"""
|
|
835
|
+
log.debug(f"selected asset: {asset.name}")
|
|
836
|
+
archive_path = self._download_asset(
|
|
837
|
+
source,
|
|
838
|
+
repo_spec,
|
|
839
|
+
asset,
|
|
840
|
+
cache_dir,
|
|
841
|
+
tag=release.tag,
|
|
842
|
+
upgrade=upgrade,
|
|
843
|
+
)
|
|
844
|
+
log.debug(f"archive: {archive_path}")
|
|
845
|
+
|
|
846
|
+
extract_tool_name = tool_name
|
|
847
|
+
if not is_archive(asset.name) and not (setup and setup.name):
|
|
848
|
+
extract_tool_name = _derive_bare_binary_name(asset.name, tool_name, release.tag)
|
|
849
|
+
|
|
850
|
+
result = extract(archive_path, env_dir, tool_name=extract_tool_name)
|
|
851
|
+
log.debug(f"extracted to: {result.root}")
|
|
852
|
+
|
|
853
|
+
rejected: list[tuple[Path, str]] = []
|
|
854
|
+
executables = find_executables(
|
|
855
|
+
result.root,
|
|
856
|
+
expose=load_heuristics().expose,
|
|
857
|
+
log=log,
|
|
858
|
+
rejected=rejected,
|
|
859
|
+
)
|
|
860
|
+
log.debug(f"executables: {[e.name for e in executables]}")
|
|
861
|
+
return executables, rejected
|
|
862
|
+
|
|
863
|
+
def _unpack(
|
|
864
|
+
self,
|
|
865
|
+
source: ReleaseSource,
|
|
866
|
+
repo_spec: RepoSpec,
|
|
867
|
+
release: Release,
|
|
868
|
+
asset: Asset,
|
|
869
|
+
env_dir: Path,
|
|
870
|
+
cache_dir: Path,
|
|
871
|
+
*,
|
|
872
|
+
spec: str,
|
|
873
|
+
tool_name: str,
|
|
874
|
+
setup: SetupConfig | None,
|
|
875
|
+
upgrade: bool,
|
|
876
|
+
log,
|
|
877
|
+
) -> list[Path]:
|
|
878
|
+
"""Unpack the one asset a lockfile names, which has no fallback."""
|
|
879
|
+
executables, rejected = self._unpack_candidate(
|
|
880
|
+
source,
|
|
881
|
+
repo_spec,
|
|
882
|
+
release,
|
|
883
|
+
asset,
|
|
884
|
+
env_dir,
|
|
885
|
+
cache_dir,
|
|
886
|
+
tool_name=tool_name,
|
|
887
|
+
setup=setup,
|
|
888
|
+
upgrade=upgrade,
|
|
889
|
+
log=log,
|
|
890
|
+
)
|
|
891
|
+
if executables:
|
|
892
|
+
return executables
|
|
893
|
+
if rejected:
|
|
894
|
+
raise BinaryPlatformError(spec, release.tag, asset.name, rejected)
|
|
895
|
+
raise BinaryNoExecutableError(spec, release.tag, asset.name, [])
|
|
896
|
+
|
|
897
|
+
def _unpack_first_runnable(
|
|
898
|
+
self,
|
|
899
|
+
source: ReleaseSource,
|
|
900
|
+
repo_spec: RepoSpec,
|
|
901
|
+
release: Release,
|
|
902
|
+
env_dir: Path,
|
|
903
|
+
cache_dir: Path,
|
|
904
|
+
*,
|
|
905
|
+
spec: str,
|
|
906
|
+
tool_name: str,
|
|
907
|
+
setup: SetupConfig | None,
|
|
908
|
+
effective_pattern: str | None,
|
|
909
|
+
upgrade: bool,
|
|
910
|
+
log,
|
|
911
|
+
) -> tuple[Asset, Release, list[Path]]:
|
|
912
|
+
"""Try candidates in scoring order until one unpacks to something runnable.
|
|
913
|
+
|
|
914
|
+
An archive only says what it holds once unpacked: the tool, a shared
|
|
915
|
+
library shipped under the same os/arch tokens, or a build this host has
|
|
916
|
+
no loader for. Rather than fail on an unlucky first pick, a candidate
|
|
917
|
+
that unpacks to nothing usable is dropped and its siblings get their
|
|
918
|
+
turn — releases that ship a library beside the binary, or a musl build
|
|
919
|
+
beside the glibc one, resolve on the second pass.
|
|
920
|
+
"""
|
|
921
|
+
excluded: set[str] = set()
|
|
922
|
+
tried: list[str] = []
|
|
923
|
+
first_rejection: tuple[str, list[tuple[Path, str]]] | None = None
|
|
924
|
+
relearned = False
|
|
925
|
+
|
|
926
|
+
while True:
|
|
927
|
+
selection_pattern = _selection_pattern(effective_pattern, release.tag)
|
|
928
|
+
try:
|
|
929
|
+
asset = self._select_asset(
|
|
930
|
+
release,
|
|
931
|
+
selection_pattern,
|
|
932
|
+
tool_name,
|
|
933
|
+
spec,
|
|
934
|
+
repo_spec=repo_spec,
|
|
935
|
+
exclude=excluded,
|
|
936
|
+
)
|
|
937
|
+
except AssetSelectionError:
|
|
938
|
+
# A synthesized release carries only the asset its pattern named.
|
|
939
|
+
# If that asset cannot run here, the pattern is wrong for this
|
|
940
|
+
# host, not merely stale — relearn it against the full release.
|
|
941
|
+
wider = None if relearned else self._relearn_release(source, repo_spec, release)
|
|
942
|
+
if wider is not None:
|
|
943
|
+
relearned = True
|
|
944
|
+
release = wider
|
|
945
|
+
continue
|
|
946
|
+
if first_rejection is not None:
|
|
947
|
+
name, rejected = first_rejection
|
|
948
|
+
raise BinaryPlatformError(spec, release.tag, name, rejected) from None
|
|
949
|
+
if tried:
|
|
950
|
+
# Nothing in the release unpacked to a runnable file. Name the
|
|
951
|
+
# best candidate and let the user point at another asset: an
|
|
952
|
+
# install that exposes nothing must never look successful.
|
|
953
|
+
raise BinaryNoExecutableError(
|
|
954
|
+
spec,
|
|
955
|
+
release.tag,
|
|
956
|
+
tried[0],
|
|
957
|
+
[a.name for a in release.assets if a.name not in tried],
|
|
958
|
+
) from None
|
|
959
|
+
raise
|
|
960
|
+
|
|
961
|
+
executables, rejected = self._unpack_candidate(
|
|
962
|
+
source,
|
|
963
|
+
repo_spec,
|
|
964
|
+
release,
|
|
965
|
+
asset,
|
|
966
|
+
env_dir,
|
|
967
|
+
cache_dir,
|
|
968
|
+
tool_name=tool_name,
|
|
969
|
+
setup=setup,
|
|
970
|
+
upgrade=upgrade,
|
|
971
|
+
log=log,
|
|
972
|
+
)
|
|
973
|
+
if executables:
|
|
974
|
+
return asset, release, executables
|
|
975
|
+
|
|
976
|
+
log.debug(f"asset skip: {asset.name} — {_rejection_reason(rejected)}")
|
|
977
|
+
doomed = _doomed_by_libc(release, rejected, log)
|
|
978
|
+
_empty_dir(env_dir)
|
|
979
|
+
if effective_pattern is not None:
|
|
980
|
+
# The asset was named, by the user or by the tool's setup file.
|
|
981
|
+
# Quietly installing a different one would answer a question
|
|
982
|
+
# nobody asked: report what the named asset turned out to be.
|
|
983
|
+
if rejected:
|
|
984
|
+
raise BinaryPlatformError(spec, release.tag, asset.name, rejected)
|
|
985
|
+
raise BinaryNoExecutableError(
|
|
986
|
+
spec,
|
|
987
|
+
release.tag,
|
|
988
|
+
asset.name,
|
|
989
|
+
[a.name for a in release.assets if a.name != asset.name],
|
|
990
|
+
)
|
|
991
|
+
tried.append(asset.name)
|
|
992
|
+
if rejected and first_rejection is None:
|
|
993
|
+
first_rejection = (asset.name, rejected)
|
|
994
|
+
excluded.add(asset.name)
|
|
995
|
+
excluded |= doomed
|
|
996
|
+
|
|
730
997
|
def _select_asset(
|
|
731
998
|
self,
|
|
732
999
|
release: Release,
|
|
733
1000
|
asset_pattern: str | None,
|
|
734
1001
|
tool_name: str,
|
|
735
1002
|
main_spec: str,
|
|
1003
|
+
*,
|
|
1004
|
+
repo_spec: RepoSpec | None = None,
|
|
1005
|
+
exclude: set[str] | None = None,
|
|
736
1006
|
) -> Asset:
|
|
1007
|
+
log = get_logger("binary")
|
|
737
1008
|
heuristics = load_heuristics()
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
[a.name for a in release.assets],
|
|
1009
|
+
excluded = exclude or set()
|
|
1010
|
+
candidates = [a for a in release.assets if a.name not in excluded]
|
|
1011
|
+
unusable: list[tuple[str, str]] = []
|
|
1012
|
+
|
|
1013
|
+
while candidates:
|
|
1014
|
+
asset = select_asset(
|
|
1015
|
+
candidates,
|
|
1016
|
+
tool_name,
|
|
1017
|
+
_strip_v_prefix(release.tag),
|
|
1018
|
+
heuristics=heuristics,
|
|
1019
|
+
asset_pattern=asset_pattern,
|
|
1020
|
+
verbose=get_verbosity(),
|
|
751
1021
|
)
|
|
752
|
-
|
|
1022
|
+
if asset is None:
|
|
1023
|
+
break
|
|
1024
|
+
|
|
1025
|
+
loader = self._unrunnable_loader(asset, repo_spec=repo_spec, tag=release.tag)
|
|
1026
|
+
if loader is None:
|
|
1027
|
+
return asset
|
|
1028
|
+
|
|
1029
|
+
log.debug(f"asset skip: {asset.name} needs missing loader {loader}")
|
|
1030
|
+
unusable.append((asset.name, loader))
|
|
1031
|
+
candidates = [a for a in candidates if a.name != asset.name]
|
|
1032
|
+
|
|
1033
|
+
raise AssetSelectionError(
|
|
1034
|
+
main_spec,
|
|
1035
|
+
release.tag,
|
|
1036
|
+
[a.name for a in release.assets],
|
|
1037
|
+
unusable,
|
|
1038
|
+
)
|
|
1039
|
+
|
|
1040
|
+
def _relearn_release(
|
|
1041
|
+
self,
|
|
1042
|
+
source: ReleaseSource,
|
|
1043
|
+
repo_spec: RepoSpec,
|
|
1044
|
+
release: Release,
|
|
1045
|
+
) -> Release | None:
|
|
1046
|
+
"""Re-fetch *release* from the API after its learned pattern proved wrong.
|
|
1047
|
+
|
|
1048
|
+
The fast path synthesizes a release holding only the asset its pattern
|
|
1049
|
+
names, so a pattern pointing at an unrunnable build leaves selection
|
|
1050
|
+
with nothing to fall back on. The pattern is dropped and the real asset
|
|
1051
|
+
list fetched, which lets scoring reach the runnable siblings.
|
|
1052
|
+
|
|
1053
|
+
Returns None when the release already came from the API: there is then
|
|
1054
|
+
no wider list to fall back to, and the caller should surface the
|
|
1055
|
+
original selection error instead.
|
|
1056
|
+
"""
|
|
1057
|
+
if (release.resolution_source or "").endswith("-api"):
|
|
1058
|
+
return None
|
|
1059
|
+
|
|
1060
|
+
log = get_logger("binary")
|
|
1061
|
+
log.debug(
|
|
1062
|
+
f"asset pattern for {repo_spec.owner}/{repo_spec.repo} names an unrunnable "
|
|
1063
|
+
"asset — invalidating and refetching from the release API"
|
|
1064
|
+
)
|
|
1065
|
+
from ixt.config import asset_pattern_cache
|
|
1066
|
+
|
|
1067
|
+
asset_pattern_cache.invalidate(repo_spec.owner, repo_spec.repo)
|
|
1068
|
+
self._require_github_api(repo_spec)
|
|
1069
|
+
refetched = source.get_release_by_tag(repo_spec.owner, repo_spec.repo, release.tag)
|
|
1070
|
+
refetched.resolution_source = f"{repo_spec.platform}-api"
|
|
1071
|
+
log.debug(f"release (relearn): {refetched.tag} ({len(refetched.assets)} assets)")
|
|
1072
|
+
return refetched
|
|
1073
|
+
|
|
1074
|
+
def _unrunnable_loader(
|
|
1075
|
+
self,
|
|
1076
|
+
asset: Asset,
|
|
1077
|
+
*,
|
|
1078
|
+
repo_spec: RepoSpec | None,
|
|
1079
|
+
tag: str,
|
|
1080
|
+
) -> str | None:
|
|
1081
|
+
"""Return the loader *asset* needs but this host lacks, else None.
|
|
1082
|
+
|
|
1083
|
+
A cached asset is read from disk; otherwise only the head of the asset
|
|
1084
|
+
is fetched over HTTP Range, which keeps a doomed candidate from costing
|
|
1085
|
+
a full download. Only bare binaries can be checked this way — an
|
|
1086
|
+
archive reveals nothing at offset zero, so those are validated after
|
|
1087
|
+
extraction by ``find_executables`` instead.
|
|
1088
|
+
|
|
1089
|
+
Any doubt resolves to None: a server without Range support, a network
|
|
1090
|
+
hiccup, or a truncated prefix all fall through to the normal path.
|
|
1091
|
+
"""
|
|
1092
|
+
if os.name == "nt" or is_archive(asset.name):
|
|
1093
|
+
return None
|
|
1094
|
+
if repo_spec is not None:
|
|
1095
|
+
cache_path = _asset_cache_path(self.settings.downloads_dir, repo_spec, asset, tag=tag)
|
|
1096
|
+
cached = _cached_asset(cache_path, asset.size)
|
|
1097
|
+
if cached is not None:
|
|
1098
|
+
# Already on disk: judge the real file, no request needed.
|
|
1099
|
+
info = read_elf(cached)
|
|
1100
|
+
return info.interpreter if info is not None and info.loader_missing() else None
|
|
1101
|
+
|
|
1102
|
+
head = get_range(asset.url, ELF_PREFIX_SIZE)
|
|
1103
|
+
if head is None:
|
|
1104
|
+
return None
|
|
1105
|
+
info = parse_elf(head)
|
|
1106
|
+
if info is None or not info.loader_missing():
|
|
1107
|
+
return None
|
|
1108
|
+
return info.interpreter
|
|
753
1109
|
|
|
754
1110
|
def uninstall_package(self, env_dir: Path, package: str) -> None:
|
|
755
1111
|
"""No-op — binary tools are atomic (uninstall removes the whole env)."""
|