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.
- daedalux-0.7.1/PKG-INFO +35 -0
- daedalux-0.7.1/README.md +21 -0
- daedalux-0.7.1/daedalus/__init__.py +7 -0
- daedalux-0.7.1/daedalus/launcher.py +158 -0
- daedalux-0.7.1/daedalux.egg-info/PKG-INFO +35 -0
- daedalux-0.7.1/daedalux.egg-info/SOURCES.txt +10 -0
- daedalux-0.7.1/daedalux.egg-info/dependency_links.txt +1 -0
- daedalux-0.7.1/daedalux.egg-info/entry_points.txt +2 -0
- daedalux-0.7.1/daedalux.egg-info/top_level.txt +1 -0
- daedalux-0.7.1/pyproject.toml +25 -0
- daedalux-0.7.1/setup.cfg +4 -0
- daedalux-0.7.1/tests/test_launcher.py +107 -0
daedalux-0.7.1/PKG-INFO
ADDED
|
@@ -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.
|
daedalux-0.7.1/README.md
ADDED
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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"]
|
daedalux-0.7.1/setup.cfg
ADDED
|
@@ -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)
|