Skip to content

Commit 217de5d

Browse files
committed
[build] Allow building development .msi artifacts
1 parent bbcf187 commit 217de5d

5 files changed

Lines changed: 79 additions & 24 deletions

File tree

RELEASE-PLAN.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Version referenced below as `X.Y[.Z]` - replace with the real version throughout
4343
- [ ] `python scripts/pre_release.py --release` passes (enforces `.aip``__version__` parity, writes `packaging/windows/.version_info`).
4444
- [ ] `pyinstaller packaging/windows/scenedetect.spec` produces a working `scenedetect.exe` - run it against a sample video.
4545
- [ ] `python scripts/stage_windows_dist.py --ffmpeg-dir <dir> --portable-zip` populates `dist/scenedetect/` with ffmpeg, third-party licenses, sphinx docs, and emits the portable `.zip`. Pass `--ffmpeg-dir` pointing at a recent extracted [GyanD codexffmpeg](https://github.com/GyanD/codexffmpeg/releases) build; omit it only for offline builds (uses the bundled `packaging/windows/thirdparty.7z` with a stub `LICENSE-FFMPEG`).
46+
- [ ] `python scripts/bump_installer.py --sync-files` and commit the .aip diff (refreshes the APPDIR baseline so CI's per-build `--sync-only` diff stays small).
4647
- [ ] Build the MSI via Advanced Installer (`packaging/windows/installer/PySceneDetect.aip`); install into a clean Windows VM and run the CLI.
4748
- [ ] After both `pyinstaller` and the MSI build are done (and the portable `.zip` is staged at `dist/PySceneDetect-X.Y.Z-portable.zip`), run `python scripts/generate_manifest.py` to produce `dist/PySceneDetect-X.Y.Z.manifest.json` (per-file SHA256 audit of every artifact) and `dist/SHA256SUMS` (flat `sha256sum -c` compatible). Both are attached to the GitHub release in step 7.
4849

appveyor.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,26 @@ install:
6464
- appveyor-tools\secure-file -decrypt license65.dat.enc -secret %ai_license_secret% -salt %ai_license_salt%
6565
- appveyor DownloadFile https://www.advancedinstaller.com/downloads/advinst.msi
6666
- msiexec /i advinst.msi /qn
67-
- 'SET PATH=%PATH%;C:\\Program Files (x86)\\Caphyon\\Advanced Installer 22.9.1\\bin\\x86'
67+
# Resolve the installed Advanced Installer bin path dynamically - the upstream
68+
# MSI is unversioned so the directory name (Advanced Installer X.Y.Z) drifts.
69+
- ps: $aiBin = (Get-ChildItem 'C:\Program Files (x86)\Caphyon\Advanced Installer*\bin\x86' | Sort-Object FullName -Descending | Select-Object -First 1).FullName; Add-Content $env:APPVEYOR_BUILD_FOLDER\ai_path.txt $aiBin
70+
- set /p AI_BIN=<%APPVEYOR_BUILD_FOLDER%\ai_path.txt
71+
- 'SET PATH=%PATH%;%AI_BIN%'
6872
# License path must be absolute
6973
- AdvancedInstaller.com /RegisterOffline "%cd%\license65.dat"
7074
- cd ../../..
7175
# Re-sync APPDIR from CI's dist/scenedetect (handles drift between local and
7276
# CI pyinstaller output - new transitive deps, Python patch updates, etc.).
7377
# Does not touch version/GUID fields - those are committed to the .aip on the
7478
# release tag and must stay stable across rebuilds for upgrade-chain integrity.
75-
- python scripts/bump_installer.py --sync-only
79+
# On non-tag builds, also pass --dev so the MSI is named PySceneDetect-{ver}-dev-win64.msi
80+
# (keeps dev artifacts distinguishable from signed releases).
81+
- if "%APPVEYOR_REPO_TAG%"=="true" (python scripts/bump_installer.py --sync-only) else (python scripts/bump_installer.py --sync-only --dev)
82+
# Snapshot the post-sync .aip and the actual payload tree as build artifacts.
83+
# The committed .aip is a baseline; CI adapts it to its own pyinstaller output
84+
# and we never write back to git, so these snapshots are the authoritative
85+
# record of what each MSI was built from (for audit / release forensics).
86+
- copy packaging\windows\installer\PySceneDetect.aip dist\PySceneDetect.aip
7687
# Create MSI installer
7788
- AdvancedInstaller.com /build packaging/windows/installer/PySceneDetect.aip
7889

@@ -112,3 +123,8 @@ artifacts:
112123
# MSI Installer + .EXE Bundle for Signing
113124
- path: dist/scenedetect-signed.zip
114125
name: PySceneDetect-win64_installer
126+
# Build provenance: post-sync .aip and the portable payload manifest.
127+
- path: dist/PySceneDetect.aip
128+
name: PySceneDetect-build-manifest-aip
129+
- path: dist/PySceneDetect-*-portable.manifest.txt
130+
name: PySceneDetect-build-manifest-payload

scripts/bump_installer.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,18 @@
1212
"""Bump the AdvancedInstaller .aip project for a release.
1313
1414
Usage:
15-
python scripts/bump_installer.py # version bump only
16-
python scripts/bump_installer.py --sync-files # bump + re-sync APPDIR
17-
python scripts/bump_installer.py --sync-only # re-sync APPDIR only (CI)
18-
python scripts/bump_installer.py --version 0.7.0 # explicit version override
19-
20-
The version-bump path rewrites ProductVersion / ProductCode / PackageFileName.
21-
--sync-files additionally walks dist/scenedetect/ (pyinstaller output) and
22-
rewrites the project's directory + component + file tables to match, which
23-
is needed when bundled dependencies change. --sync-only does the resync
24-
without touching version/identity fields - intended for CI, where the .aip
25-
is already at the release version and we just want the file list to match
26-
CI's pyinstaller output (rather than the developer's local one).
27-
28-
All paths shell out to AdvancedInstaller.com so the .aip's invariants
29-
(line endings, attribute ordering, GUID casing) stay intact. The CLI lives
30-
under "C:\\Program Files (x86)\\Caphyon\\Advanced Installer ..\\bin\\x86\\".
31-
Override discovery with the ADVINST environment variable.
15+
python scripts/bump_installer.py # version bump only
16+
python scripts/bump_installer.py --sync-files # bump + re-sync APPDIR
17+
python scripts/bump_installer.py --sync-only # re-sync APPDIR only (CI)
18+
python scripts/bump_installer.py --sync-only --dev # CI dev build (renames MSI)
19+
python scripts/bump_installer.py --version 0.7.0 # explicit version override
20+
21+
The committed .aip is a baseline; CI's --sync-only adapts it per build and is
22+
never written back to git. Refresh locally with --sync-files before each release.
23+
24+
All paths shell out to AdvancedInstaller.com to preserve .aip invariants
25+
(line endings, attribute ordering, GUID casing). Override CLI discovery with
26+
the ADVINST environment variable.
3227
"""
3328

3429
import argparse
@@ -109,19 +104,35 @@ def main() -> None:
109104
action="store_true",
110105
help="Re-sync APPDIR only; leave version/GUID fields untouched (CI use).",
111106
)
107+
parser.add_argument(
108+
"--dev",
109+
action="store_true",
110+
help=(
111+
"Rename the MSI to PySceneDetect-{ver}-dev-win64.msi so dev-build artifacts "
112+
"are distinguishable from release artifacts. Only valid with --sync-only."
113+
),
114+
)
112115
parser.add_argument(
113116
"--version",
114117
dest="version_override",
115118
help="MSI version override (default: derived from scenedetect.__version__).",
116119
)
117120
args = parser.parse_args()
118121

122+
if args.dev and not args.sync_only:
123+
sys.exit("--dev is only valid in combination with --sync-only.")
124+
119125
advinst = find_advinst()
120126
print(f"Using {advinst}")
121127

122128
if args.sync_only:
123129
print(f"Re-syncing APPDIR in {INSTALLER_AIP.name}")
124130
resync_appdir(advinst)
131+
if args.dev:
132+
version = msi_version(args.version_override or scenedetect.__version__)
133+
dev_name = f"PySceneDetect-{version}-dev-win64.msi"
134+
print(f"Renaming MSI package to {dev_name} (dev build)")
135+
run(advinst, "/SetPackageName", dev_name, "-buildname", "DefaultBuild")
125136
return
126137

127138
raw_version = args.version_override or scenedetect.__version__

scripts/pre_release.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@
1818
import sys
1919
from pathlib import Path
2020

21-
REPO_DIR = Path(__file__).resolve().parent.parent
21+
SCRIPTS_DIR = Path(__file__).resolve().parent
22+
REPO_DIR = SCRIPTS_DIR.parent
2223
sys.path.insert(0, str(REPO_DIR))
24+
sys.path.insert(0, str(SCRIPTS_DIR))
25+
26+
from bump_installer import msi_version # noqa: E402
2327

2428
import scenedetect # noqa: E402
2529

@@ -34,8 +38,15 @@
3438

3539
if run_version_check:
3640
installer_aip = INSTALLER_AIP.read_text()
37-
aip_version = f'<ROW Property="ProductVersion" Value="{VERSION}" Options="32"/>'
38-
assert aip_version in installer_aip, f"Installer project version does not match {VERSION}."
41+
# The .aip stores the numeric MSI form (e.g. "0.7.0"), not the Python __version__
42+
# (which may be "0.7-dev0", "0.7", "0.7.1", ...). Normalize through the same
43+
# function bump_installer.py uses to write the .aip so the comparison is apples-to-apples.
44+
expected = msi_version(VERSION)
45+
aip_row = f'<ROW Property="ProductVersion" Value="{expected}" Options="32"/>'
46+
assert aip_row in installer_aip, (
47+
f"Installer ProductVersion does not match normalized {VERSION!r} ({expected!r}). "
48+
f"Run `python scripts/bump_installer.py` to refresh the .aip."
49+
)
3950

4051
with VERSION_INFO.open("wb") as f:
4152
v = VERSION.split(".")

scripts/stage_windows_dist.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,22 @@ def find_7zip() -> Path:
6969
sys.exit("7-Zip not found. Install from https://www.7-zip.org/.")
7070

7171

72+
def _rel(p: Path) -> str:
73+
# Display paths relative to the repo when possible, else fall back to the
74+
# absolute path (e.g. --ffmpeg-dir pointing outside the repo on CI).
75+
try:
76+
return str(p.relative_to(REPO_DIR))
77+
except ValueError:
78+
return str(p)
79+
80+
7281
def copy_file(src: Path, dst: Path) -> None:
7382
if not src.exists():
7483
print(f"WARNING: {src} missing - skipping {dst.name}")
7584
return
7685
dst.parent.mkdir(parents=True, exist_ok=True)
7786
shutil.copy2(src, dst)
78-
print(f" {src.relative_to(REPO_DIR)} -> {dst.relative_to(REPO_DIR)}")
87+
print(f" {_rel(src)} -> {_rel(dst)}")
7988

8089

8190
def stage_ffmpeg(ffmpeg_dir: Path | None) -> None:
@@ -141,13 +150,20 @@ def stage_thirdparty_licenses() -> None:
141150

142151
def make_portable_zip(version: str) -> None:
143152
zip_path = DIST_DIR / f"PySceneDetect-{version}-portable.zip"
153+
manifest_path = DIST_DIR / f"PySceneDetect-{version}-portable.manifest.txt"
144154
if zip_path.exists():
145155
zip_path.unlink()
146156
print(f"Creating {zip_path.relative_to(REPO_DIR)}...")
157+
files = sorted(p for p in DIST_TREE.rglob("*") if p.is_file())
147158
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
148-
for path in sorted(p for p in DIST_TREE.rglob("*") if p.is_file()):
159+
for path in files:
149160
zf.write(path, path.relative_to(DIST_TREE))
150161
print(f" {zip_path.stat().st_size / (1024 * 1024):.1f} MB")
162+
manifest_path.write_text(
163+
"\n".join(str(p.relative_to(DIST_TREE)) for p in files) + "\n",
164+
encoding="utf-8",
165+
)
166+
print(f" manifest -> {manifest_path.relative_to(REPO_DIR)}")
151167

152168

153169
def main() -> None:

0 commit comments

Comments
 (0)