tbx-cli 0.0.1__tar.gz → 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tbx_cli-0.0.1 → tbx_cli-0.1.0}/.gitignore +1 -0
- tbx_cli-0.1.0/PKG-INFO +7 -0
- {tbx_cli-0.0.1 → tbx_cli-0.1.0}/pyproject.toml +5 -3
- tbx_cli-0.1.0/src/tbx_cli/__init__.py +125 -0
- tbx_cli-0.1.0/src/tbx_cli/__main__.py +3 -0
- tbx_cli-0.1.0/src/tbx_cli/_checksums.py +7 -0
- tbx_cli-0.0.1/PKG-INFO +0 -12
- tbx_cli-0.0.1/README.md +0 -3
- tbx_cli-0.0.1/src/tbx_cli/__init__.py +0 -1
tbx_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tbx-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Launcher for the tbx CLI: downloads and runs the checksum-verified release binary.
|
|
5
|
+
Project-URL: Repository, https://github.com/joeblackwaslike/toolbox
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tbx-cli"
|
|
3
|
-
version = "0.0
|
|
4
|
-
description = "
|
|
5
|
-
readme = "README.md"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Launcher for the tbx CLI: downloads and runs the checksum-verified release binary."
|
|
6
5
|
license = "MIT"
|
|
7
6
|
requires-python = ">=3.10"
|
|
8
7
|
|
|
9
8
|
[project.urls]
|
|
10
9
|
Repository = "https://github.com/joeblackwaslike/toolbox"
|
|
11
10
|
|
|
11
|
+
[project.scripts]
|
|
12
|
+
tbx = "tbx_cli:main"
|
|
13
|
+
|
|
12
14
|
[build-system]
|
|
13
15
|
requires = ["hatchling==1.27.0"]
|
|
14
16
|
build-backend = "hatchling.build"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Launcher that downloads, verifies and runs the tbx release binary for this package version."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import hashlib
|
|
5
|
+
import http.client
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
import urllib.error
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import urllib.request
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import NoReturn
|
|
15
|
+
|
|
16
|
+
from tbx_cli._checksums import CHECKSUMS, VERSION
|
|
17
|
+
|
|
18
|
+
DEFAULT_BASE = "https://github.com/joeblackwaslike/toolbox/releases/download"
|
|
19
|
+
OS_NAMES = {"darwin": "darwin", "linux": "linux"}
|
|
20
|
+
ARCH_NAMES = {"arm64": "arm64", "aarch64": "arm64", "x86_64": "x64", "amd64": "x64"}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fail(message: str) -> NoReturn:
|
|
24
|
+
sys.stderr.write(f"tbx: {message}\n")
|
|
25
|
+
sys.exit(1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def asset_name(os_name: str, machine: str, libc: str) -> str:
|
|
29
|
+
"""Map the platform to a release asset name, exiting with a clear message when unsupported."""
|
|
30
|
+
os_part = OS_NAMES.get(os_name)
|
|
31
|
+
arch = ARCH_NAMES.get(machine.lower())
|
|
32
|
+
if not os_part or not arch:
|
|
33
|
+
_fail(f"unsupported platform {os_name}/{machine}; need macOS or Linux on arm64 or x64")
|
|
34
|
+
if os_part == "linux" and libc != "glibc":
|
|
35
|
+
_fail("musl-based Linux (such as Alpine) is not supported; the tbx binaries need glibc")
|
|
36
|
+
return f"tbx-{os_part}-{arch}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_allowed_url(url: str) -> bool:
|
|
40
|
+
"""Allow https anywhere, and http only for loopback hosts."""
|
|
41
|
+
try:
|
|
42
|
+
parsed = urllib.parse.urlparse(url)
|
|
43
|
+
loopback = parsed.hostname in ("127.0.0.1", "localhost")
|
|
44
|
+
except ValueError:
|
|
45
|
+
return False
|
|
46
|
+
return parsed.scheme == "https" or (parsed.scheme == "http" and loopback)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _download_base() -> str:
|
|
50
|
+
base = os.environ.get("TBX_DOWNLOAD_BASE", DEFAULT_BASE).rstrip("/")
|
|
51
|
+
if not is_allowed_url(base):
|
|
52
|
+
_fail("the download base must use https (http is allowed only for localhost)")
|
|
53
|
+
return base
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _sha256(data: bytes) -> str:
|
|
57
|
+
return hashlib.sha256(data).hexdigest()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _RedirectHandler(urllib.request.HTTPRedirectHandler):
|
|
61
|
+
"""Validates each redirect hop before following it, with a bounded hop count."""
|
|
62
|
+
|
|
63
|
+
max_redirections = 5
|
|
64
|
+
|
|
65
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl):
|
|
66
|
+
if not is_allowed_url(newurl):
|
|
67
|
+
raise urllib.error.HTTPError(
|
|
68
|
+
newurl, code, f"refusing a redirect to {newurl}", headers, fp
|
|
69
|
+
)
|
|
70
|
+
return super().redirect_request(req, fp, code, msg, headers, newurl)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _fetch(url: str) -> bytes:
|
|
74
|
+
opener = urllib.request.build_opener(_RedirectHandler)
|
|
75
|
+
try:
|
|
76
|
+
with opener.open(url, timeout=60) as res:
|
|
77
|
+
if not is_allowed_url(res.geturl()):
|
|
78
|
+
_fail(f"download failed: {url}: refusing a redirect to {res.geturl()}")
|
|
79
|
+
return res.read()
|
|
80
|
+
except (OSError, http.client.HTTPException, ValueError) as err:
|
|
81
|
+
_fail(f"download failed: {url}: {err}")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _install(asset: str, expected: str, target: Path) -> None:
|
|
85
|
+
url = f"{_download_base()}/tbx-cli-v{VERSION}/{asset}"
|
|
86
|
+
body = _fetch(url)
|
|
87
|
+
if _sha256(body) != expected:
|
|
88
|
+
_fail(f"checksum mismatch for {asset}; refusing to run the download")
|
|
89
|
+
try:
|
|
90
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
91
|
+
fd, tmp = tempfile.mkstemp(dir=target.parent, prefix=f".{asset}.", suffix=".tmp")
|
|
92
|
+
except OSError as err:
|
|
93
|
+
_fail(f"cannot install cached binary {target}: {err}")
|
|
94
|
+
try:
|
|
95
|
+
with os.fdopen(fd, "wb") as f:
|
|
96
|
+
f.write(body)
|
|
97
|
+
Path(tmp).chmod(0o755)
|
|
98
|
+
Path(tmp).replace(target)
|
|
99
|
+
except OSError as err:
|
|
100
|
+
_fail(f"cannot install cached binary {target}: {err}")
|
|
101
|
+
finally:
|
|
102
|
+
with contextlib.suppress(FileNotFoundError):
|
|
103
|
+
Path(tmp).unlink()
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main() -> None:
|
|
107
|
+
"""Run the pinned tbx binary with the original arguments, downloading it first if needed."""
|
|
108
|
+
asset = asset_name(sys.platform, platform.machine(), platform.libc_ver()[0])
|
|
109
|
+
expected = CHECKSUMS.get(asset)
|
|
110
|
+
if not expected:
|
|
111
|
+
_fail(f"no checksum for {asset} in this package")
|
|
112
|
+
cache_root = Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache")
|
|
113
|
+
target = cache_root / "tbx" / VERSION / asset
|
|
114
|
+
try:
|
|
115
|
+
cached_ok = _sha256(target.read_bytes()) == expected
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
cached_ok = False
|
|
118
|
+
except OSError as err:
|
|
119
|
+
_fail(f"cannot read cached binary {target}: {err}")
|
|
120
|
+
if not cached_ok:
|
|
121
|
+
_install(asset, expected, target)
|
|
122
|
+
try:
|
|
123
|
+
os.execv(target, [str(target), *sys.argv[1:]]) # noqa: S606
|
|
124
|
+
except OSError as err:
|
|
125
|
+
_fail(f"cannot run {target}: {err}")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
VERSION = "0.1.0"
|
|
2
|
+
CHECKSUMS = {
|
|
3
|
+
"tbx-darwin-arm64": "eb504c1edba20abd74b2baa117e647494a7d582a05b8d653e8eabd6d5e4c79c1",
|
|
4
|
+
"tbx-darwin-x64": "c3b0c60c08cb046334924d59bb2b9d71cff43db321af878c959f255c60438e17",
|
|
5
|
+
"tbx-linux-arm64": "09d8c179eb30c57740403a33db7bf450cea9f3d00278de6ceaf6a5cf23e5f05b",
|
|
6
|
+
"tbx-linux-x64": "acd9c50163ef6f89ee5b83b7358dfdd76be11844894d511a9ffc6600755e8cbc",
|
|
7
|
+
}
|
tbx_cli-0.0.1/PKG-INFO
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: tbx-cli
|
|
3
|
-
Version: 0.0.1
|
|
4
|
-
Summary: Placeholder for the tbx CLI toolbox. The real release follows.
|
|
5
|
-
Project-URL: Repository, https://github.com/joeblackwaslike/toolbox
|
|
6
|
-
License-Expression: MIT
|
|
7
|
-
Requires-Python: >=3.10
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
|
|
10
|
-
# tbx-cli
|
|
11
|
-
|
|
12
|
-
Reserved name for the `tbx` CLI toolbox. See https://github.com/joeblackwaslike/toolbox.
|
tbx_cli-0.0.1/README.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Placeholder for tbx-cli."""
|