Add per-user MSI installer for Windows; strip Mark-of-the-Web in frozen ZIP builds
release / gui-build (linux, bash) (push) Successful in 1m12s
release / gui-build (macos, bash) (push) Successful in 1m14s
release / gui-build (windows, cmd) (push) Failing after 1m31s

Files extracted from a downloaded ZIP carry a Zone.Identifier stream and the
.NET Framework CLR refuses to load such assemblies, so pythonnet failed with
'Failed to resolve Python.Runtime.Loader.Initialize'. MSI-installed files have
no MOTW; the winmain strip fixes the ZIP distribution.
This commit is contained in:
2026-09-22 22:28:16 +00:00
parent b01e8a5c3d
commit 734b7993a2
4 changed files with 175 additions and 4 deletions
+89
View File
@@ -18,6 +18,7 @@ This script:
import io
import platform
import re
import shutil
import stat
import subprocess
@@ -40,6 +41,15 @@ _FFMPEG_STAGING = Path(__file__).parent.parent / "build" / "ffmpeg"
_REPO_ROOT = Path(__file__).parent.parent
_ASSETS_DIR = _REPO_ROOT / "mediahive" / "assets"
# WiX 3.11 standalone binaries (candle/light/heat run on .NET Framework 4.x,
# no .NET SDK needed, unlike WiX v4 which is a dotnet tool).
_WIX_URL = (
"https://github.com/wixtoolset/wix3/releases/download/wix3112rtm"
"/wix311-binaries.zip"
)
_WIX_STAGING = _REPO_ROOT / "build" / "wix311"
_WIX_WXS = _REPO_ROOT / "scripts" / "installer" / "MediaHive.wxs"
def _platform_zip_suffix() -> str:
machine = platform.machine().lower()
@@ -180,6 +190,80 @@ def ensure_macos_icon() -> Path:
return icon_icns
def fetch_wix() -> Path:
"""Download WiX 3.11 standalone binaries into build/wix311/ (cached)."""
candle = _WIX_STAGING / "candle.exe"
if candle.exists():
print(f"WiX already staged at {_WIX_STAGING}, skipping download.")
return _WIX_STAGING
_WIX_STAGING.mkdir(parents=True, exist_ok=True)
print(f"Downloading WiX from {_WIX_URL} ...")
with urllib.request.urlopen(_WIX_URL) as resp:
data = resp.read()
with zipfile.ZipFile(io.BytesIO(data)) as zf:
zf.extractall(_WIX_STAGING)
print(f"WiX staged at {_WIX_STAGING}")
return _WIX_STAGING
def build_msi(version: str) -> Path:
"""Build a per-user MSI installer from the PyInstaller output folder.
MSI-installed files carry no Mark-of-the-Web, so the .NET CLR loads
pythonnet/pywebview assemblies that it refuses from a downloaded ZIP.
"""
dist_folder = _REPO_ROOT / "build" / "MediaHive"
if not dist_folder.exists():
raise FileNotFoundError(f"Distribution folder not found: {dist_folder}")
# MSI versions must be numeric x.y.z[.w]; strip any local/pre-release part.
msi_version = re.match(r"\d+(?:\.\d+){0,3}", version)
if not msi_version:
raise ValueError(f"Cannot derive MSI version from {version!r}")
wix = fetch_wix()
obj_dir = _REPO_ROOT / "build" / "wix-obj"
obj_dir.mkdir(parents=True, exist_ok=True)
files_wxs = _REPO_ROOT / "build" / "wix-files.wxs"
msi_path = _REPO_ROOT / "build" / f"MediaHive-{version}-{_platform_zip_suffix()}.msi"
def run(tool: str, args: list[str]) -> None:
cmd = [str(wix / tool), *args]
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=_REPO_ROOT)
if result.returncode != 0:
raise RuntimeError(f"{tool} failed with exit code {result.returncode}")
run("heat.exe", [
"dir", str(dist_folder),
"-cg", "MediaHiveFiles",
"-dr", "INSTALLFOLDER",
"-gg", "-g1",
"-sfrag", "-sreg", "-srd",
"-var", "var.MediaHiveSourceDir",
"-out", str(files_wxs),
])
run("candle.exe", [
f"-dMediaHiveVersion={msi_version.group(0)}",
f"-dMediaHiveSourceDir={dist_folder}",
"-arch", "x64",
str(_WIX_WXS),
str(files_wxs),
"-out", f"{obj_dir}\\",
])
run("light.exe", [
"-ext", "WixUIExtension",
"-sice:ICE38", "-sice:ICE61", "-sice:ICE64", "-sice:ICE91",
str(obj_dir / "MediaHive.wixobj"),
str(obj_dir / "wix-files.wixobj"),
"-out", str(msi_path),
])
return msi_path
def read_version() -> str:
"""Read version via setuptools_scm (same logic as hatch-vcs)."""
return setuptools_scm.get_version(root=str(_REPO_ROOT))
@@ -292,6 +376,11 @@ def main() -> None:
print(f"✓ Built successfully: {artifact_path}")
print(f" Size: {artifact_path.stat().st_size / (1024 * 1024):.1f} MB")
if sys.platform == "win32":
msi_path = build_msi(version)
print(f"✓ Built successfully: {msi_path}")
print(f" Size: {msi_path.stat().st_size / (1024 * 1024):.1f} MB")
except (FileNotFoundError, OSError, RuntimeError, ValueError) as e:
print(f"✗ Build failed: {e}", file=sys.stderr)
sys.exit(1)