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.
@@ -12,3 +12,4 @@ __pycache__/
12
12
  *.egg-info/
13
13
  .clawhip/
14
14
  .serena/
15
+ .*.bun-build
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.1"
4
- description = "Placeholder for the tbx CLI toolbox. The real release follows."
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,3 @@
1
+ from tbx_cli import main
2
+
3
+ main()
@@ -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,3 +0,0 @@
1
- # tbx-cli
2
-
3
- Reserved name for the `tbx` CLI toolbox. See https://github.com/joeblackwaslike/toolbox.
@@ -1 +0,0 @@
1
- """Placeholder for tbx-cli."""