Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ jobs:

- name: Validate runtime deps for installer/portable
run: |
python scripts/verify_portable_zip.py --runtime-dir dist/PortkeyDrop_dir --require-runtime-no-data
python scripts/verify_portable_zip.py --runtime-dir dist/PortkeyDrop_dir --pyz-path build/portkeydrop/PYZ-00.pyz --require-runtime-no-data

- name: Create installer
shell: cmd
Expand Down Expand Up @@ -125,7 +125,7 @@ jobs:

- name: Validate portable ZIP contents
run: |
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
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

- uses: actions/upload-artifact@v6
with:
Expand Down
13 changes: 12 additions & 1 deletion installer/portkeydrop.spec
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import sys
from pathlib import Path

import tomllib
from PyInstaller.utils.hooks import collect_all, collect_submodules
from PyInstaller.utils.hooks import collect_all, collect_dynamic_libs, collect_submodules


# Determine paths
Expand Down Expand Up @@ -51,6 +51,14 @@ def collect_optional_submodules(package: str) -> list[str]:
return []


def collect_optional_dynamic_libs(package: str) -> list[tuple[str, str]]:
"""Collect package dynamic libs when available, otherwise return empty list."""
try:
return collect_dynamic_libs(package)
except Exception:
return []


# Determine icon path
ICON_PATH = SPEC_DIR / "app.ico"
ICON_PATH = str(ICON_PATH) if ICON_PATH.exists() else None
Expand All @@ -71,6 +79,9 @@ prism_datas, prism_binaries, prism_hiddenimports = collect_optional_all("prism")
prismatoid_datas, prismatoid_binaries, prismatoid_hiddenimports = collect_optional_all("prismatoid")
datas += prism_datas + prismatoid_datas
binaries += prism_binaries + prismatoid_binaries
# Mirror AccessiWeather strategy: explicitly collect screen-reader dynamic libs.
binaries += collect_optional_dynamic_libs("prism")
binaries += collect_optional_dynamic_libs("prismatoid")

# Hidden imports for wxPython and other dynamic imports
hiddenimports = [
Expand Down
54 changes: 27 additions & 27 deletions scripts/verify_portable_zip.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,18 @@
from pathlib import Path


RUNTIME_PACKAGES = ("prism", "prismatoid")
RUNTIME_MODULE_TOKENS = ("prism", "prismatoid")


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


def _package_matches(entries: list[str]) -> list[str]:
def _contains_runtime_tokens(entries: list[str]) -> list[str]:
matches: list[str] = []
for name in entries:
if any(name.startswith(f"{pkg}/") for pkg in RUNTIME_PACKAGES):
low = name.lower()
if any(token in low for token in RUNTIME_MODULE_TOKENS):
matches.append(name)
return matches

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

for pkg in RUNTIME_PACKAGES:
pkg_dir = runtime_dir / pkg
if not pkg_dir.exists() or not pkg_dir.is_dir():
errors.append(f"missing runtime package directory: {pkg}/")
continue

if not any(path.is_file() for path in pkg_dir.rglob("*")):
errors.append(f"runtime package has no files: {pkg}/")

if require_no_data and (runtime_dir / "data").exists():
errors.append("runtime dir must not contain data/ (portable-only)")

if errors:
return False, errors

print(f"Validated runtime dir: {runtime_dir}")
for pkg in RUNTIME_PACKAGES:
count = sum(1 for p in (runtime_dir / pkg).rglob("*") if p.is_file())
print(f"Found {pkg}/ files: {count}")
return True, []


def verify_pyz_contains_runtime_modules(pyz_path: Path) -> tuple[bool, list[str]]:
if not pyz_path.exists():
return False, [f"missing pyz archive: {pyz_path}"]

data = pyz_path.read_bytes().lower()
found = [token for token in RUNTIME_MODULE_TOKENS if token.encode("utf-8") in data]

if not found:
return False, [f"missing prism/prismatoid markers in PYZ archive ({pyz_path})"]

print(f"Validated runtime modules in PYZ: {pyz_path}")
print(f"Found runtime token markers: {', '.join(found)}")
return True, []


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

runtime_entries = _package_matches(entries)
if not runtime_entries:
errors.append("missing prism/prismatoid runtime files in portable zip")

if errors:
return False, errors

print(f"Validated portable zip: {zip_path}")
print("Found prism/prismatoid entries:")
for name in runtime_entries[:20]:
print(f" {name}")
if len(runtime_entries) > 20:
print(f" ... and {len(runtime_entries) - 20} more")

print("Found data/ directory contents for portable mode")
return True, []


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--runtime-dir", type=Path, help="Path to unpacked runtime directory")
parser.add_argument("--pyz-path", type=Path, help="Path to PyInstaller PYZ archive")
parser.add_argument("--portable-zip", type=Path, help="Path to portable zip")
parser.add_argument(
"--require-runtime-no-data",
Expand All @@ -93,8 +88,8 @@ def main() -> int:
)
args = parser.parse_args()

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

all_errors: list[str] = []

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

if args.pyz_path:
ok, errors = verify_pyz_contains_runtime_modules(args.pyz_path)
if not ok:
all_errors.extend(errors)

if args.portable_zip:
ok, errors = verify_portable_zip(args.portable_zip)
if not ok:
Expand Down