daedalux 0.7.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.
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: daedalux
3
+ Version: 0.7.1
4
+ Summary: Package any app into a single self-extracting binary (release launcher)
5
+ License: MIT
6
+ Keywords: packaging,binary,self-extracting,static
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: POSIX :: Linux
9
+ Classifier: Operating System :: MacOS
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+
15
+ # daedalus (PyPI)
16
+
17
+ `pip install daedalux` installs the `daedalus` CLI - it packages any app into
18
+ a single self-extracting binary.
19
+
20
+ ```bash
21
+ pip install daedalux
22
+ daedalus build ./my-app -o my-app.de
23
+ ./my-app.de
24
+ ```
25
+
26
+ The wheel ships a thin launcher: on first run it downloads the matching
27
+ release binary from GitHub (SHA-256 verified against the release
28
+ `checksums.txt`), caches it under `~/.cache/daedalus/pip/`, and execs it.
29
+ Subsequent runs are a cache hit. No compilation, no sudo - the `daedalus-stub`
30
+ and `daedalus-crypto` tools ship in the same download, so `daedalus build`
31
+ works immediately.
32
+
33
+ Platforms with release assets: Linux/macOS/Windows × amd64/arm64.
34
+ `pip install daedalux --upgrade` releases the launcher; run any `daedalus`
35
+ command once to pull a newer binary.
@@ -0,0 +1,21 @@
1
+ # daedalus (PyPI)
2
+
3
+ `pip install daedalux` installs the `daedalus` CLI - it packages any app into
4
+ a single self-extracting binary.
5
+
6
+ ```bash
7
+ pip install daedalux
8
+ daedalus build ./my-app -o my-app.de
9
+ ./my-app.de
10
+ ```
11
+
12
+ The wheel ships a thin launcher: on first run it downloads the matching
13
+ release binary from GitHub (SHA-256 verified against the release
14
+ `checksums.txt`), caches it under `~/.cache/daedalus/pip/`, and execs it.
15
+ Subsequent runs are a cache hit. No compilation, no sudo - the `daedalus-stub`
16
+ and `daedalus-crypto` tools ship in the same download, so `daedalus build`
17
+ works immediately.
18
+
19
+ Platforms with release assets: Linux/macOS/Windows × amd64/arm64.
20
+ `pip install daedalux --upgrade` releases the launcher; run any `daedalus`
21
+ command once to pull a newer binary.
@@ -0,0 +1,7 @@
1
+ """daedalus - package any app into a single self-extracting binary."""
2
+
3
+ from .launcher import VERSION, main
4
+
5
+ __version__ = VERSION
6
+
7
+ __all__ = ["VERSION", "main"]
@@ -0,0 +1,158 @@
1
+ # pip release launcher for daedalus.
2
+ #
3
+ # pip/yarn-style native binaries cannot be shipped as wheels from a Rust
4
+ # release flow, so this package installs a thin console script that resolves
5
+ # the matching GitHub release tarball (checksum-verified) into a user cache
6
+ # and execs the real daedalus binary. Every run is a cache hit after the
7
+ # first download. See `daedalus-cli/src/release.rs` for the same logic in the
8
+ # Rust CLI.
9
+ """daedalus release launcher."""
10
+
11
+ import hashlib
12
+ import io
13
+ import os
14
+ import platform
15
+ import stat
16
+ import sys
17
+ import tarfile
18
+ import urllib.request
19
+ from dataclasses import dataclass
20
+ from pathlib import Path
21
+
22
+ VERSION = "0.7.1"
23
+ REPO = "Encapsul/daedalus"
24
+ CACHE_DIR = Path.home() / ".cache" / "daedalus" / "pip"
25
+ MAX_TARBALL_BYTES = 200 * 1024 * 1024
26
+
27
+
28
+ @dataclass
29
+ class Platform:
30
+ os: str
31
+ arch: str
32
+
33
+
34
+ def detect_platform() -> Platform:
35
+ """Map the running host to the (os, arch) tags used in release asset names."""
36
+ system = platform.system().lower()
37
+ machine = platform.machine().lower()
38
+
39
+ os_map = {"linux": "linux", "darwin": "darwin", "windows": "windows"}
40
+ if system not in os_map:
41
+ raise SystemExit(f"daedalus: unsupported platform: {platform.system()}")
42
+
43
+ if machine in ("x86_64", "amd64"):
44
+ arch = "amd64"
45
+ elif machine in ("aarch64", "arm64"):
46
+ arch = "arm64"
47
+ else:
48
+ raise SystemExit(f"daedalus: unsupported architecture: {platform.machine()}")
49
+
50
+ return Platform(os_map[system], arch)
51
+
52
+
53
+ def release_url(p: Platform) -> str:
54
+ """GitHub release download URL for this platform's asset."""
55
+ asset = f"daedalus_{VERSION}_{p.os}_{p.arch}.tar.gz"
56
+ return f"https://github.com/{REPO}/releases/download/v{VERSION}/{asset}"
57
+
58
+
59
+ def sha256_bytes(data: bytes) -> str:
60
+ return hashlib.sha256(data).hexdigest()
61
+
62
+
63
+ def fetch_text(url: str) -> str:
64
+ with urllib.request.urlopen(url, timeout=30) as resp:
65
+ return resp.read().decode("utf-8")
66
+
67
+
68
+ def expected_checksum(p: Platform) -> str:
69
+ """Fetch the sha256 for this asset from the release checksums.txt."""
70
+ checksums_url = f"https://github.com/{REPO}/releases/download/v{VERSION}/checksums.txt"
71
+ asset = f"daedalus_{VERSION}_{p.os}_{p.arch}.tar.gz"
72
+ return parse_checksums(fetch_text(checksums_url), asset)
73
+
74
+
75
+ def parse_checksums(text: str, asset: str) -> str:
76
+ """Return the sha256 for `asset` from a release checksums.txt body."""
77
+ for line in text.splitlines():
78
+ parts = line.split()
79
+ if len(parts) >= 2 and parts[1].lstrip("*") == asset:
80
+ return parts[0]
81
+ raise SystemExit(f"daedalus: no checksum for {asset} in release checksums.txt")
82
+
83
+
84
+ def download_verified(url: str, expected: str) -> bytes:
85
+ """Download a release asset, rejecting oversized or unverified payloads."""
86
+ with urllib.request.urlopen(url, timeout=600) as resp:
87
+ data = resp.read()
88
+ if len(data) > MAX_TARBALL_BYTES:
89
+ raise SystemExit(f"daedalus: release asset too large: {len(data)} bytes")
90
+ got = sha256_bytes(data)
91
+ if got != expected:
92
+ raise SystemExit(
93
+ f"daedalus: checksum mismatch for {url}: expected {expected}, got {got}"
94
+ )
95
+ return data
96
+
97
+
98
+ def extract_daedalus(data: bytes, dest: Path) -> Path:
99
+ """Unpack the release tarball (daedalus + stub + crypto) into dest.
100
+
101
+ The Rust CLI resolves `daedalus-stub` next to the `daedalus` binary, so
102
+ all three release binaries are materialised in the cache directory."""
103
+ dest.mkdir(parents=True, exist_ok=True)
104
+ extracted = None
105
+ with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as tar:
106
+ for member in tar.getmembers():
107
+ name = Path(member.name)
108
+ if ".." in name.parts:
109
+ raise SystemExit(f"daedalus: unsafe path in archive: {member.name}")
110
+ base = name.name
111
+ if not name.parent.name.startswith("daedalus_"):
112
+ continue
113
+ stem = base[:-4] if base.endswith(".exe") else base
114
+ if stem not in ("daedalus", "daedalus-stub", "daedalus-crypto"):
115
+ continue
116
+ if not member.isfile():
117
+ continue
118
+ f = tar.extractfile(member)
119
+ if f is None:
120
+ continue
121
+ target = dest / base
122
+ target.write_bytes(f.read())
123
+ if os.name != "nt":
124
+ target.chmod(target.stat().st_mode | stat.S_IXUSR)
125
+ if stem == "daedalus":
126
+ extracted = target
127
+ if extracted is None:
128
+ raise SystemExit("daedalus: did not find daedalus binary in release archive")
129
+ return extracted
130
+
131
+
132
+ def cache_dir() -> Path:
133
+ return CACHE_DIR / VERSION / f"{detect_platform().os}_{detect_platform().arch}"
134
+
135
+
136
+ def resolve_binary() -> Path:
137
+ """Return the cached daedalus binary, downloading it on first use."""
138
+ p = detect_platform()
139
+ where = cache_dir()
140
+ binary = where / "daedalus.exe" if os.name == "nt" else where / "daedalus"
141
+ if binary.exists():
142
+ return binary
143
+
144
+ url = release_url(p)
145
+ sys.stderr.write(f"[daedalus] downloading {url}...\n")
146
+ expected = expected_checksum(p)
147
+ data = download_verified(url, expected)
148
+ extracted = extract_daedalus(data, where)
149
+ return extracted
150
+
151
+
152
+ def main() -> int:
153
+ binary = resolve_binary()
154
+ os.execv(str(binary), [str(binary)] + sys.argv[1:])
155
+
156
+
157
+ if __name__ == "__main__":
158
+ sys.exit(main())
@@ -0,0 +1,35 @@
1
+ Metadata-Version: 2.4
2
+ Name: daedalux
3
+ Version: 0.7.1
4
+ Summary: Package any app into a single self-extracting binary (release launcher)
5
+ License: MIT
6
+ Keywords: packaging,binary,self-extracting,static
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Operating System :: POSIX :: Linux
9
+ Classifier: Operating System :: MacOS
10
+ Classifier: Operating System :: Microsoft :: Windows
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+
15
+ # daedalus (PyPI)
16
+
17
+ `pip install daedalux` installs the `daedalus` CLI - it packages any app into
18
+ a single self-extracting binary.
19
+
20
+ ```bash
21
+ pip install daedalux
22
+ daedalus build ./my-app -o my-app.de
23
+ ./my-app.de
24
+ ```
25
+
26
+ The wheel ships a thin launcher: on first run it downloads the matching
27
+ release binary from GitHub (SHA-256 verified against the release
28
+ `checksums.txt`), caches it under `~/.cache/daedalus/pip/`, and execs it.
29
+ Subsequent runs are a cache hit. No compilation, no sudo - the `daedalus-stub`
30
+ and `daedalus-crypto` tools ship in the same download, so `daedalus build`
31
+ works immediately.
32
+
33
+ Platforms with release assets: Linux/macOS/Windows × amd64/arm64.
34
+ `pip install daedalux --upgrade` releases the launcher; run any `daedalus`
35
+ command once to pull a newer binary.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ daedalus/__init__.py
4
+ daedalus/launcher.py
5
+ daedalux.egg-info/PKG-INFO
6
+ daedalux.egg-info/SOURCES.txt
7
+ daedalux.egg-info/dependency_links.txt
8
+ daedalux.egg-info/entry_points.txt
9
+ daedalux.egg-info/top_level.txt
10
+ tests/test_launcher.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ daedalus = daedalus.launcher:main
@@ -0,0 +1 @@
1
+ daedalus
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "daedalux"
7
+ version = "0.7.1"
8
+ description = "Package any app into a single self-extracting binary (release launcher)"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.8"
12
+ keywords = ["packaging", "binary", "self-extracting", "static"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Operating System :: POSIX :: Linux",
16
+ "Operating System :: MacOS",
17
+ "Operating System :: Microsoft :: Windows",
18
+ "License :: OSI Approved :: MIT License",
19
+ ]
20
+
21
+ [project.scripts]
22
+ daedalus = "daedalus.launcher:main"
23
+
24
+ [tool.setuptools]
25
+ packages = ["daedalus"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,107 @@
1
+ """Unit tests for the daedalus pip launcher (no network, no pip deps)."""
2
+
3
+ import io
4
+ import os
5
+ import sys
6
+ import tarfile
7
+ import tempfile
8
+ import unittest
9
+ from pathlib import Path
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
+
13
+ from daedalus import launcher
14
+
15
+
16
+ def make_tarball(files: dict[str, bytes]) -> bytes:
17
+ """Build an in-memory gzipped tarball mirroring a release archive layout."""
18
+ buf = io.BytesIO()
19
+ with tarfile.open(fileobj=buf, mode="w:gz") as tar:
20
+ for name, content in files.items():
21
+ info = tarfile.TarInfo(name)
22
+ info.size = len(content)
23
+ tar.addfile(info, io.BytesIO(content))
24
+ return buf.getvalue()
25
+
26
+
27
+ class DetectPlatformTest(unittest.TestCase):
28
+ def test_known_host_maps(self):
29
+ # macOS amd64 shorthand is covered; Windows via sys.platform is hard to
30
+ # fake, so just assert the struct is well-formed on the running host.
31
+ p = launcher.detect_platform()
32
+ self.assertIn(p.os, ("linux", "darwin", "windows"))
33
+ self.assertIn(p.arch, ("amd64", "arm64"))
34
+
35
+ def test_asset_name(self):
36
+ p = launcher.Platform("darwin", "amd64")
37
+ self.assertEqual(
38
+ launcher.release_url(p).rsplit("/", 1)[-1],
39
+ f"daedalus_{launcher.VERSION}_darwin_amd64.tar.gz",
40
+ )
41
+
42
+
43
+ class ChecksumTest(unittest.TestCase):
44
+ def test_sha256(self):
45
+ self.assertEqual(
46
+ launcher.sha256_bytes(b"hello world"),
47
+ "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9",
48
+ )
49
+
50
+ def test_expected_checksum_parse(self):
51
+ lines = "".join([
52
+ f"foo daedalus_{launcher.VERSION}_darwin_amd64.tar.gz\n",
53
+ f"bar daedalus_{launcher.VERSION}_darwin_arm64.tar.gz\n",
54
+ ])
55
+ self.assertEqual(
56
+ launcher.parse_checksums(
57
+ lines, f"daedalus_{launcher.VERSION}_darwin_arm64.tar.gz"
58
+ ),
59
+ "bar",
60
+ )
61
+
62
+ def test_parse_missing_asset_fails(self):
63
+ with self.assertRaises(SystemExit):
64
+ launcher.parse_checksums("x nope.tar.gz\n", "daedalus_missing")
65
+
66
+
67
+ class ExtractTest(unittest.TestCase):
68
+ def test_extract_full_archive(self):
69
+ content = make_tarball({
70
+ "daedalus_0.7.1_darwin_amd64/daedalus": b"\x7fELFfake",
71
+ "daedalus_0.7.1_darwin_amd64/daedalus-stub": b"\x7fELFstub",
72
+ "daedalus_0.7.1_darwin_amd64/daedalus-crypto": b"crypto",
73
+ })
74
+ with tempfile.TemporaryDirectory() as td:
75
+ dest = Path(td)
76
+ binary = launcher.extract_daedalus(content, dest)
77
+ self.assertEqual(binary, dest / "daedalus")
78
+ self.assertEqual((dest / "daedalus").read_bytes(), b"\x7fELFfake")
79
+ self.assertEqual((dest / "daedalus-stub").read_bytes(), b"\x7fELFstub")
80
+ self.assertEqual((dest / "daedalus-crypto").read_bytes(), b"crypto")
81
+
82
+ def test_extract_windows_exe(self):
83
+ content = make_tarball({
84
+ "daedalus_0.7.1_windows_amd64/daedalus.exe": b"MZexe",
85
+ "daedalus_0.7.1_windows_amd64/daedalus-stub.exe": b"MZstub",
86
+ "daedalus_0.7.1_windows_amd64/daedalus-crypto.exe": b"MZcrypto",
87
+ })
88
+ with tempfile.TemporaryDirectory() as td:
89
+ dest = Path(td)
90
+ binary = launcher.extract_daedalus(content, dest)
91
+ self.assertEqual(binary, dest / "daedalus.exe")
92
+
93
+ def test_extract_missing_binary_fails(self):
94
+ content = make_tarball({"daedalus_0.7.1_darwin_amd64/README": b"nope"})
95
+ with tempfile.TemporaryDirectory() as td:
96
+ with self.assertRaises(SystemExit):
97
+ launcher.extract_daedalus(content, Path(td))
98
+
99
+ def test_unsafe_path_rejected(self):
100
+ content = make_tarball({"daedalus_0.7.1_darwin_amd64/../../evil": b"x"})
101
+ with tempfile.TemporaryDirectory() as td:
102
+ with self.assertRaises(SystemExit):
103
+ launcher.extract_daedalus(content, Path(td))
104
+
105
+
106
+ if __name__ == "__main__":
107
+ unittest.main(verbosity=2)