|
31 | 31 | """ |
32 | 32 |
|
33 | 33 | import argparse |
34 | | -import hashlib |
35 | 34 | import json |
36 | | -import re |
37 | 35 | import shutil |
38 | 36 | import subprocess |
39 | 37 | import sys |
|
44 | 42 |
|
45 | 43 | REPO_DIR = Path(__file__).resolve().parent.parent |
46 | 44 | sys.path.insert(0, str(REPO_DIR)) |
| 45 | +sys.path.insert(0, str(Path(__file__).resolve().parent)) |
47 | 46 |
|
48 | | -import scenedetect # noqa: E402 |
49 | | - |
50 | | -CHUNK = 1 << 20 # 1 MiB |
51 | | - |
52 | | - |
53 | | -def msi_version(raw: str) -> str: |
54 | | - # Mirror scripts/update_installer.py / generate_manifest.py - artifact |
55 | | - # filenames use the normalized X.Y.Z form, not the Python __version__. |
56 | | - parts = [re.split(r"[^\d]", p, maxsplit=1)[0] for p in raw.split(".")] |
57 | | - while len(parts) < 3: |
58 | | - parts.append("0") |
59 | | - return ".".join(parts[:4]) |
| 47 | +import validate_release # noqa: E402 |
| 48 | +from _release_common import ( # noqa: E402 |
| 49 | + find_7zip, |
| 50 | + hash_zip_contents, |
| 51 | + msi_version, |
| 52 | + sha256_file, |
| 53 | + verify_authenticode, |
| 54 | +) |
60 | 55 |
|
| 56 | +import scenedetect # noqa: E402 |
61 | 57 |
|
62 | 58 | VERSION = msi_version(scenedetect.__version__) |
63 | 59 |
|
64 | 60 |
|
65 | | -def find_7zip() -> Path: |
66 | | - for candidate in ( |
67 | | - Path(r"C:\Program Files\7-Zip\7z.exe"), |
68 | | - Path(r"C:\Program Files (x86)\7-Zip\7z.exe"), |
69 | | - ): |
70 | | - if candidate.exists(): |
71 | | - return candidate |
72 | | - on_path = shutil.which("7z") or shutil.which("7z.exe") |
73 | | - if on_path: |
74 | | - return Path(on_path) |
75 | | - sys.exit("7-Zip not found. Install from https://www.7-zip.org/.") |
76 | | - |
77 | | - |
78 | | -def sha256_file(path: Path) -> str: |
79 | | - h = hashlib.sha256() |
80 | | - with path.open("rb") as f: |
81 | | - for block in iter(lambda: f.read(CHUNK), b""): |
82 | | - h.update(block) |
83 | | - return h.hexdigest() |
84 | | - |
85 | | - |
86 | | -def hash_zip_contents(zip_path: Path) -> list[dict]: |
87 | | - entries = [] |
88 | | - with zipfile.ZipFile(zip_path) as zf: |
89 | | - for info in sorted(zf.infolist(), key=lambda i: i.filename): |
90 | | - if info.is_dir(): |
91 | | - continue |
92 | | - h = hashlib.sha256() |
93 | | - with zf.open(info) as f: |
94 | | - for block in iter(lambda: f.read(CHUNK), b""): |
95 | | - h.update(block) |
96 | | - entries.append( |
97 | | - { |
98 | | - "path": info.filename, |
99 | | - "size": info.file_size, |
100 | | - "sha256": h.hexdigest(), |
101 | | - } |
102 | | - ) |
103 | | - return entries |
104 | | - |
105 | | - |
106 | | -def verify_authenticode(path: Path) -> None: |
107 | | - """Bail unless `path` carries a Valid Authenticode signature. |
108 | | -
|
109 | | - Catches the wrong-artifact case: e.g. someone drops the AppVeyor |
110 | | - pre-signing bundle into dist/signed/ instead of the SignPath output. |
111 | | - PowerShell's Get-AuthenticodeSignature works on both .exe and .msi. |
112 | | - """ |
113 | | - if sys.platform != "win32": |
114 | | - print(f" (skipping Authenticode check for {path.name} on non-Windows)") |
115 | | - return |
116 | | - ps_cmd = ( |
117 | | - f"$sig = Get-AuthenticodeSignature -FilePath '{path}'; " |
118 | | - "Write-Output $sig.Status; " |
119 | | - "if ($sig.SignerCertificate) { Write-Output $sig.SignerCertificate.Subject }" |
120 | | - ) |
121 | | - result = subprocess.run( |
122 | | - ["powershell", "-NoProfile", "-Command", ps_cmd], |
123 | | - capture_output=True, |
124 | | - text=True, |
125 | | - check=False, |
126 | | - ) |
127 | | - lines = [line.strip() for line in result.stdout.splitlines() if line.strip()] |
128 | | - if result.returncode != 0 or not lines: |
129 | | - sys.exit( |
130 | | - f"Authenticode check for {path.name} failed to run.\n stderr: {result.stderr.strip()}" |
131 | | - ) |
132 | | - status = lines[0] |
133 | | - subject = lines[1] if len(lines) > 1 else "<no certificate>" |
134 | | - print(f" Authenticode: {status} ({subject})") |
135 | | - if status != "Valid": |
136 | | - sys.exit( |
137 | | - f"Authenticode check FAILED for {path.name}: status={status!r}. " |
138 | | - "Verify scenedetect-signed.zip is the SignPath output, not an " |
139 | | - "unsigned AppVeyor artifact." |
140 | | - ) |
141 | | - |
142 | | - |
143 | 61 | def extract_signed_bundle(signed_zip: Path, dest: Path) -> tuple[Path, Path]: |
144 | 62 | print(f"Extracting {signed_zip.name}...") |
145 | 63 | with zipfile.ZipFile(signed_zip) as zf: |
@@ -274,6 +192,10 @@ def main() -> None: |
274 | 192 | print(f"Copied signed MSI -> {msi_dest.name}") |
275 | 193 | write_manifests(staging, portable_zip, msi_dest) |
276 | 194 |
|
| 195 | + print() |
| 196 | + print("Validating finalized artifacts...") |
| 197 | + validate_release.run_all_checks(staging) |
| 198 | + |
277 | 199 |
|
278 | 200 | if __name__ == "__main__": |
279 | 201 | main() |
0 commit comments