Skip to content

Commit 6188ee0

Browse files
authored
fix(ci): validate prismatoid bundling via pyinstaller archive (#76)
* fix(ci): verify prism modules from pyinstaller archive * fix(ci): validate pyz markers instead of zip parsing * fix(build): collect prism dynamic libs like AccessiWeather * style(ci): apply ruff formatting for verifier
1 parent e846f16 commit 6188ee0

3 files changed

Lines changed: 41 additions & 30 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ jobs:
9797
9898
- name: Validate runtime deps for installer/portable
9999
run: |
100-
python scripts/verify_portable_zip.py --runtime-dir dist/PortkeyDrop_dir --require-runtime-no-data
100+
python scripts/verify_portable_zip.py --runtime-dir dist/PortkeyDrop_dir --pyz-path build/portkeydrop/PYZ-00.pyz --require-runtime-no-data
101101
102102
- name: Create installer
103103
shell: cmd
@@ -125,7 +125,7 @@ jobs:
125125
126126
- name: Validate portable ZIP contents
127127
run: |
128-
python scripts/verify_portable_zip.py --runtime-dir dist/PortkeyDrop_dir --require-runtime-no-data --portable-zip dist/PortkeyDrop_Portable_v${{ needs.prepare.outputs.version }}.zip
128+
python scripts/verify_portable_zip.py --runtime-dir dist/PortkeyDrop_dir --pyz-path build/portkeydrop/PYZ-00.pyz --require-runtime-no-data --portable-zip dist/PortkeyDrop_Portable_v${{ needs.prepare.outputs.version }}.zip
129129
130130
- uses: actions/upload-artifact@v6
131131
with:

installer/portkeydrop.spec

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import sys
1212
from pathlib import Path
1313

1414
import tomllib
15-
from PyInstaller.utils.hooks import collect_all, collect_submodules
15+
from PyInstaller.utils.hooks import collect_all, collect_dynamic_libs, collect_submodules
1616

1717

1818
# Determine paths
@@ -51,6 +51,14 @@ def collect_optional_submodules(package: str) -> list[str]:
5151
return []
5252

5353

54+
def collect_optional_dynamic_libs(package: str) -> list[tuple[str, str]]:
55+
"""Collect package dynamic libs when available, otherwise return empty list."""
56+
try:
57+
return collect_dynamic_libs(package)
58+
except Exception:
59+
return []
60+
61+
5462
# Determine icon path
5563
ICON_PATH = SPEC_DIR / "app.ico"
5664
ICON_PATH = str(ICON_PATH) if ICON_PATH.exists() else None
@@ -71,6 +79,9 @@ prism_datas, prism_binaries, prism_hiddenimports = collect_optional_all("prism")
7179
prismatoid_datas, prismatoid_binaries, prismatoid_hiddenimports = collect_optional_all("prismatoid")
7280
datas += prism_datas + prismatoid_datas
7381
binaries += prism_binaries + prismatoid_binaries
82+
# Mirror AccessiWeather strategy: explicitly collect screen-reader dynamic libs.
83+
binaries += collect_optional_dynamic_libs("prism")
84+
binaries += collect_optional_dynamic_libs("prismatoid")
7485

7586
# Hidden imports for wxPython and other dynamic imports
7687
hiddenimports = [

scripts/verify_portable_zip.py

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,18 @@
99
from pathlib import Path
1010

1111

12-
RUNTIME_PACKAGES = ("prism", "prismatoid")
12+
RUNTIME_MODULE_TOKENS = ("prism", "prismatoid")
1313

1414

1515
def _normalize(path: str) -> str:
1616
return path.replace("\\", "/").lstrip("./")
1717

1818

19-
def _package_matches(entries: list[str]) -> list[str]:
19+
def _contains_runtime_tokens(entries: list[str]) -> list[str]:
2020
matches: list[str] = []
2121
for name in entries:
22-
if any(name.startswith(f"{pkg}/") for pkg in RUNTIME_PACKAGES):
22+
low = name.lower()
23+
if any(token in low for token in RUNTIME_MODULE_TOKENS):
2324
matches.append(name)
2425
return matches
2526

@@ -30,26 +31,28 @@ def verify_runtime_dir(runtime_dir: Path, require_no_data: bool) -> tuple[bool,
3031
if not runtime_dir.exists():
3132
return False, [f"missing runtime dir: {runtime_dir}"]
3233

33-
for pkg in RUNTIME_PACKAGES:
34-
pkg_dir = runtime_dir / pkg
35-
if not pkg_dir.exists() or not pkg_dir.is_dir():
36-
errors.append(f"missing runtime package directory: {pkg}/")
37-
continue
38-
39-
if not any(path.is_file() for path in pkg_dir.rglob("*")):
40-
errors.append(f"runtime package has no files: {pkg}/")
41-
4234
if require_no_data and (runtime_dir / "data").exists():
4335
errors.append("runtime dir must not contain data/ (portable-only)")
4436

4537
if errors:
4638
return False, errors
4739

4840
print(f"Validated runtime dir: {runtime_dir}")
49-
for pkg in RUNTIME_PACKAGES:
50-
count = sum(1 for p in (runtime_dir / pkg).rglob("*") if p.is_file())
51-
print(f"Found {pkg}/ files: {count}")
41+
return True, []
42+
43+
44+
def verify_pyz_contains_runtime_modules(pyz_path: Path) -> tuple[bool, list[str]]:
45+
if not pyz_path.exists():
46+
return False, [f"missing pyz archive: {pyz_path}"]
47+
48+
data = pyz_path.read_bytes().lower()
49+
found = [token for token in RUNTIME_MODULE_TOKENS if token.encode("utf-8") in data]
5250

51+
if not found:
52+
return False, [f"missing prism/prismatoid markers in PYZ archive ({pyz_path})"]
53+
54+
print(f"Validated runtime modules in PYZ: {pyz_path}")
55+
print(f"Found runtime token markers: {', '.join(found)}")
5356
return True, []
5457

5558

@@ -65,26 +68,18 @@ def verify_portable_zip(zip_path: Path) -> tuple[bool, list[str]]:
6568
if not any(name == "data/" or name.startswith("data/") for name in entries):
6669
errors.append("missing required data/ directory contents")
6770

68-
runtime_entries = _package_matches(entries)
69-
if not runtime_entries:
70-
errors.append("missing prism/prismatoid runtime files in portable zip")
71-
7271
if errors:
7372
return False, errors
7473

7574
print(f"Validated portable zip: {zip_path}")
76-
print("Found prism/prismatoid entries:")
77-
for name in runtime_entries[:20]:
78-
print(f" {name}")
79-
if len(runtime_entries) > 20:
80-
print(f" ... and {len(runtime_entries) - 20} more")
81-
75+
print("Found data/ directory contents for portable mode")
8276
return True, []
8377

8478

8579
def main() -> int:
8680
parser = argparse.ArgumentParser(description=__doc__)
8781
parser.add_argument("--runtime-dir", type=Path, help="Path to unpacked runtime directory")
82+
parser.add_argument("--pyz-path", type=Path, help="Path to PyInstaller PYZ archive")
8883
parser.add_argument("--portable-zip", type=Path, help="Path to portable zip")
8984
parser.add_argument(
9085
"--require-runtime-no-data",
@@ -93,8 +88,8 @@ def main() -> int:
9388
)
9489
args = parser.parse_args()
9590

96-
if not args.runtime_dir and not args.portable_zip:
97-
parser.error("Provide at least one of --runtime-dir or --portable-zip")
91+
if not args.runtime_dir and not args.pyz_path and not args.portable_zip:
92+
parser.error("Provide at least one of --runtime-dir, --pyz-path, or --portable-zip")
9893

9994
all_errors: list[str] = []
10095

@@ -103,6 +98,11 @@ def main() -> int:
10398
if not ok:
10499
all_errors.extend(errors)
105100

101+
if args.pyz_path:
102+
ok, errors = verify_pyz_contains_runtime_modules(args.pyz_path)
103+
if not ok:
104+
all_errors.extend(errors)
105+
106106
if args.portable_zip:
107107
ok, errors = verify_portable_zip(args.portable_zip)
108108
if not ok:

0 commit comments

Comments
 (0)