ixt-cli 0.10.0__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.10.0 → ixt_cli-0.10.1}/PKG-INFO +2 -2
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/pyproject.toml +1 -1
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/__init__.py +1 -3
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/backends/binary.py +288 -50
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/__init__.py +3 -2
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/argparse_completion.py +10 -1
- ixt_cli-0.10.1/src/ixt/cli/commands.py +89 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/parser.py +2 -9
- {ixt_cli-0.10.0 → 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.10.0 → ixt_cli-0.10.1}/src/ixt/data/heuristics.toml +37 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_argparse_completion.py +15 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_binary_backend.py +210 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_cli.py +35 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_heuristics.py +125 -0
- ixt_cli-0.10.0/src/ixt/cli/commands.py +0 -76
- ixt_cli-0.10.0/src/ixt/core/__init__.py +0 -16
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/.gitignore +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/LICENSE +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/README.md +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/__main__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/backends/__init__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/backends/binary_resolver.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/backends/node.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/backends/python.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_apply.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_cache.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_config.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_exec.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_info.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_install.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_lock.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_misc.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_registry.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/cmd_upgrade.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/cli/render.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/__init__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/asset_index.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/asset_pattern_cache.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/env_policy.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/flags.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/fs_policy.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/lockfile.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/models.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/registry.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/settings.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/setup_toml.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/config/toml.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/apply.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/apply_actions.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/backend.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/bootstrap.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/cache.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/discover.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/doctor.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/exec_tool.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/export.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/expose.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/extract.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/hooks.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/identity.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/inject.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/install.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/install_local.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/lockfile.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/locks.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/pathlink.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/resolution_stats.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/resolve.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/resolve_cache.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/runtimes.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/save.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/setup_completions.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/setup_path.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/transactions.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/core/upgrade.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/data/__init__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/data/asset_index.json +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/data/registry.toml +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/__init__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/atomic.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/constants.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/elf.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/fmt.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/logger.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/output.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/req_spec.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/semver.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/shell.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/libs/style.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/net/__init__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/net/github_api.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/net/gitlab_api.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/net/http.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/net/npm.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/net/pypi.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/net/source.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/platform/__init__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/src/ixt/platform/win.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/__init__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/conftest.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/README.md +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/conftest.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/docker/Dockerfile.alpine +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/docker/Dockerfile.fedora +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/docker/Dockerfile.ubuntu +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/playground_incus.sh +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/run_e2e_docker.sh +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/run_e2e_incus.sh +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/runtime_matrix.sh +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/e2e/test_e2e.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/integration/__init__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/integration/test_lifecycle.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/integration/test_policy_runtime.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/__init__.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_apply.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_asset_index.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_asset_pattern_cache.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_atomic.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_backend.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_bootstrap.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_cache.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_discover.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_doctor.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_elf.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_env_policy.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_exec.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_export.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_expose.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_extract.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_flags.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_fs_policy.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_github_api.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_gitlab_api.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_hooks.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_http.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_inject.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_install.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_install_local.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_lock_write.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_lockfile.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_models.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_namespacing.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_node_backend.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_output.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_pathlink.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_platform.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_python_backend.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_registry.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_req_spec.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_resolve.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_resolve_cache.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_runtimes.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_save.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_semver.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_settings.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_setup_path.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_setup_toml.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_source.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_style.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_toml.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_transactions.py +0 -0
- {ixt_cli-0.10.0 → ixt_cli-0.10.1}/tests/unit/test_upgrade.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: ixt-cli
|
|
3
|
-
Version: 0.10.
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "ixt-cli"
|
|
7
|
-
version = "0.10.
|
|
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"
|
|
@@ -6,6 +6,7 @@ import hashlib
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import re
|
|
9
|
+
import shutil
|
|
9
10
|
from dataclasses import dataclass, field
|
|
10
11
|
from pathlib import Path
|
|
11
12
|
from typing import TYPE_CHECKING, TypedDict
|
|
@@ -20,7 +21,9 @@ from ixt.config.flags import is_setup_toml_enabled
|
|
|
20
21
|
from ixt.config.heuristics import (
|
|
21
22
|
get_name_suffix_tokens,
|
|
22
23
|
get_platform_aliases,
|
|
24
|
+
has_any_token,
|
|
23
25
|
is_asset_arch_compatible,
|
|
26
|
+
libc_tokens,
|
|
24
27
|
load_heuristics,
|
|
25
28
|
select_asset,
|
|
26
29
|
)
|
|
@@ -42,6 +45,7 @@ from ixt.platform import get_arch, get_os
|
|
|
42
45
|
__all__ = [
|
|
43
46
|
"AssetSelectionError",
|
|
44
47
|
"BinaryBackend",
|
|
48
|
+
"BinaryNoExecutableError",
|
|
45
49
|
"BinaryPlatformError",
|
|
46
50
|
"BinarySpecError",
|
|
47
51
|
"HookEntry",
|
|
@@ -139,6 +143,78 @@ class BinaryPlatformError(RuntimeError):
|
|
|
139
143
|
)
|
|
140
144
|
|
|
141
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
|
+
|
|
142
218
|
class BinarySpecError(ValueError):
|
|
143
219
|
"""Raised when a spec cannot be parsed as a binary backend spec."""
|
|
144
220
|
|
|
@@ -594,25 +670,7 @@ class BinaryBackend:
|
|
|
594
670
|
)
|
|
595
671
|
if asset_pattern:
|
|
596
672
|
log.debug(f"user-forced asset_pattern: {asset_pattern!r}")
|
|
597
|
-
|
|
598
|
-
if locked_binary is None:
|
|
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
|
-
)
|
|
615
|
-
else:
|
|
673
|
+
if locked_binary is not None:
|
|
616
674
|
asset = release.assets[0]
|
|
617
675
|
if not is_asset_arch_compatible(
|
|
618
676
|
asset.name,
|
|
@@ -620,36 +678,33 @@ class BinaryBackend:
|
|
|
620
678
|
declared_platform=locked_binary.platform,
|
|
621
679
|
):
|
|
622
680
|
raise AssetSelectionError(spec, release.tag, [asset.name])
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
log.debug(f"executables: {[e.name for e in executables]}")
|
|
651
|
-
if not executables and rejected_executables:
|
|
652
|
-
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
|
+
)
|
|
653
708
|
|
|
654
709
|
binaries = {exe.name: str(exe) for exe in executables}
|
|
655
710
|
|
|
@@ -758,6 +813,187 @@ class BinaryBackend:
|
|
|
758
813
|
)
|
|
759
814
|
return setup
|
|
760
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
|
+
|
|
761
997
|
def _select_asset(
|
|
762
998
|
self,
|
|
763
999
|
release: Release,
|
|
@@ -766,10 +1002,12 @@ class BinaryBackend:
|
|
|
766
1002
|
main_spec: str,
|
|
767
1003
|
*,
|
|
768
1004
|
repo_spec: RepoSpec | None = None,
|
|
1005
|
+
exclude: set[str] | None = None,
|
|
769
1006
|
) -> Asset:
|
|
770
1007
|
log = get_logger("binary")
|
|
771
1008
|
heuristics = load_heuristics()
|
|
772
|
-
|
|
1009
|
+
excluded = exclude or set()
|
|
1010
|
+
candidates = [a for a in release.assets if a.name not in excluded]
|
|
773
1011
|
unusable: list[tuple[str, str]] = []
|
|
774
1012
|
|
|
775
1013
|
while candidates:
|
|
@@ -9,7 +9,6 @@ def main() -> int:
|
|
|
9
9
|
"""Entry point for ixt CLI."""
|
|
10
10
|
import sys
|
|
11
11
|
|
|
12
|
-
from ixt.cli.commands import COMMANDS
|
|
13
12
|
from ixt.cli.parser import build_parser, normalize_verbosity
|
|
14
13
|
|
|
15
14
|
# 'exec' forwards every argument after the tool name to that tool, including
|
|
@@ -32,7 +31,9 @@ def main() -> int:
|
|
|
32
31
|
parser.print_help()
|
|
33
32
|
return 0
|
|
34
33
|
|
|
35
|
-
|
|
34
|
+
from ixt.cli.commands import get_command
|
|
35
|
+
|
|
36
|
+
handler = get_command(args.command)
|
|
36
37
|
if handler is None:
|
|
37
38
|
parser.print_help()
|
|
38
39
|
return 1
|
|
@@ -451,7 +451,16 @@ def _render_zsh(root: CompletionCommand) -> str:
|
|
|
451
451
|
" esac",
|
|
452
452
|
"}",
|
|
453
453
|
"",
|
|
454
|
-
|
|
454
|
+
# The trailer must serve both installation modes. Sourced through
|
|
455
|
+
# eval, it registers the function. Dropped in an fpath, the file
|
|
456
|
+
# body *is* _ixt under autoload, so redefining the function is all
|
|
457
|
+
# a first call would do and the first TAB would complete nothing;
|
|
458
|
+
# calling it right away is what makes that mode work.
|
|
459
|
+
'if [ "$funcstack[1]" = "_ixt" ]; then',
|
|
460
|
+
' _ixt "$@"',
|
|
461
|
+
"else",
|
|
462
|
+
" compdef _ixt ixt",
|
|
463
|
+
"fi",
|
|
455
464
|
"",
|
|
456
465
|
]
|
|
457
466
|
)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Command dispatch — resolves each ``cmd_*`` from its dedicated module.
|
|
2
|
+
|
|
3
|
+
The actual implementations live in ``cli/cmd_install.py``, ``cli/cmd_upgrade.py``,
|
|
4
|
+
``cli/cmd_info.py``, ``cli/cmd_config.py``, ``cli/cmd_apply.py`` and
|
|
5
|
+
``cli/cmd_misc.py``. This module maps command names to them for ``cli.main``.
|
|
6
|
+
|
|
7
|
+
Resolution is deferred to first use: importing every handler eagerly pulled in
|
|
8
|
+
the whole CLI on each invocation, including the ones that never run a command
|
|
9
|
+
(``--version``, ``--help``). Module-level ``__getattr__`` keeps
|
|
10
|
+
``from ixt.cli.commands import cmd_install`` working all the same.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from importlib import import_module
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
__all__ = ["get_command"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Exported symbol -> module under ``ixt.cli`` that defines it.
|
|
23
|
+
_SYMBOL_MODULES: dict[str, str] = {
|
|
24
|
+
"_log_not_found_hint": "cmd_install",
|
|
25
|
+
"cmd_add": "cmd_install",
|
|
26
|
+
"cmd_apply": "cmd_apply",
|
|
27
|
+
"cmd_cache": "cmd_cache",
|
|
28
|
+
"cmd_config": "cmd_config",
|
|
29
|
+
"cmd_doctor": "cmd_misc",
|
|
30
|
+
"cmd_environment": "cmd_misc",
|
|
31
|
+
"cmd_exec": "cmd_exec",
|
|
32
|
+
"cmd_export": "cmd_apply",
|
|
33
|
+
"cmd_info": "cmd_info",
|
|
34
|
+
"cmd_install": "cmd_install",
|
|
35
|
+
"cmd_list": "cmd_info",
|
|
36
|
+
"cmd_lock": "cmd_lock",
|
|
37
|
+
"cmd_registry": "cmd_registry",
|
|
38
|
+
"cmd_runtimes": "cmd_misc",
|
|
39
|
+
"cmd_setup": "cmd_misc",
|
|
40
|
+
"cmd_shell": "cmd_misc",
|
|
41
|
+
"cmd_uninstall": "cmd_install",
|
|
42
|
+
"cmd_upgrade": "cmd_upgrade",
|
|
43
|
+
"cmd_where": "cmd_misc",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# CLI command name -> handler symbol.
|
|
47
|
+
_COMMANDS: dict[str, str] = {
|
|
48
|
+
"add": "cmd_add",
|
|
49
|
+
"install": "cmd_install",
|
|
50
|
+
"uninstall": "cmd_uninstall",
|
|
51
|
+
"upgrade": "cmd_upgrade",
|
|
52
|
+
"list": "cmd_list",
|
|
53
|
+
"info": "cmd_info",
|
|
54
|
+
"exec": "cmd_exec",
|
|
55
|
+
"config": "cmd_config",
|
|
56
|
+
"shell": "cmd_shell",
|
|
57
|
+
"where": "cmd_where",
|
|
58
|
+
"apply": "cmd_apply",
|
|
59
|
+
"export": "cmd_export",
|
|
60
|
+
"lock": "cmd_lock",
|
|
61
|
+
"cache": "cmd_cache",
|
|
62
|
+
"registry": "cmd_registry",
|
|
63
|
+
"runtime": "cmd_runtimes",
|
|
64
|
+
"setup": "cmd_setup",
|
|
65
|
+
"environment": "cmd_environment",
|
|
66
|
+
"doctor": "cmd_doctor",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve(symbol: str) -> Any:
|
|
71
|
+
module = _SYMBOL_MODULES.get(symbol)
|
|
72
|
+
if module is None:
|
|
73
|
+
raise AttributeError(f"module {__name__!r} has no attribute {symbol!r}")
|
|
74
|
+
return getattr(import_module(f"ixt.cli.{module}"), symbol)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def __getattr__(name: str) -> Any:
|
|
78
|
+
"""Resolve a re-exported ``cmd_*`` on first access (PEP 562)."""
|
|
79
|
+
return _resolve(name)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def __dir__() -> list[str]:
|
|
83
|
+
return sorted([*__all__, *_SYMBOL_MODULES])
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def get_command(name: str) -> Callable[..., int] | None:
|
|
87
|
+
"""Return the handler for CLI command *name*, or ``None`` if unknown."""
|
|
88
|
+
symbol = _COMMANDS.get(name)
|
|
89
|
+
return None if symbol is None else _resolve(symbol)
|
|
@@ -3,18 +3,11 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import argparse
|
|
6
|
-
from importlib.metadata import version
|
|
7
6
|
|
|
7
|
+
from ixt import __version__
|
|
8
8
|
from ixt.cli.argparse_completion import SUPPORTED_COMPLETION_SHELLS, complete_with
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def _get_version() -> str:
|
|
12
|
-
try:
|
|
13
|
-
return version("ixt-cli")
|
|
14
|
-
except Exception:
|
|
15
|
-
return "unknown"
|
|
16
|
-
|
|
17
|
-
|
|
18
11
|
def effective_verbosity(args: argparse.Namespace) -> int:
|
|
19
12
|
"""Return global verbosity, including aliases accepted after a subcommand."""
|
|
20
13
|
return (getattr(args, "verbose", 0) or 0) + (getattr(args, "_verbose_after", 0) or 0)
|
|
@@ -57,7 +50,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
57
50
|
"--version",
|
|
58
51
|
"-V",
|
|
59
52
|
action="version",
|
|
60
|
-
version=f"%(prog)s {
|
|
53
|
+
version=f"%(prog)s {__version__}",
|
|
61
54
|
)
|
|
62
55
|
parser.add_argument(
|
|
63
56
|
"--verbose",
|