Installers for all platforms and related fixes #1
@@ -1126,8 +1126,26 @@ def _configure_windows_event_loop_policy() -> None:
|
|||||||
asyncio.set_event_loop_policy(policy_cls())
|
asyncio.set_event_loop_policy(policy_cls())
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_mark_of_the_web() -> None:
|
||||||
|
"""Remove Zone.Identifier streams from bundled DLLs (frozen Windows only).
|
||||||
|
|
||||||
|
Files extracted from a downloaded ZIP carry the Mark-of-the-Web, and the
|
||||||
|
.NET Framework CLR refuses to load such assemblies — pythonnet then fails
|
||||||
|
with "Failed to resolve Python.Runtime.Loader.Initialize from
|
||||||
|
.../Python.Runtime.dll". Strip the mark from the bundled DLLs before
|
||||||
|
pywebview loads the CLR.
|
||||||
|
"""
|
||||||
|
if not getattr(sys, "frozen", False) or sys.platform != "win32":
|
||||||
|
return
|
||||||
|
meipass = Path(sys._MEIPASS) # type: ignore[attr-defined]
|
||||||
|
for dll in meipass.rglob("*.dll"):
|
||||||
|
with contextlib.suppress(OSError):
|
||||||
|
os.remove(f"{dll}:Zone.Identifier")
|
||||||
|
|
||||||
|
|
||||||
def winmain() -> None:
|
def winmain() -> None:
|
||||||
_configure_windows_event_loop_policy()
|
_configure_windows_event_loop_policy()
|
||||||
|
_strip_mark_of_the_web()
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="MediaHive")
|
parser = argparse.ArgumentParser(description="MediaHive")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ This script:
|
|||||||
|
|
||||||
import io
|
import io
|
||||||
import platform
|
import platform
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -40,6 +41,15 @@ _FFMPEG_STAGING = Path(__file__).parent.parent / "build" / "ffmpeg"
|
|||||||
_REPO_ROOT = Path(__file__).parent.parent
|
_REPO_ROOT = Path(__file__).parent.parent
|
||||||
_ASSETS_DIR = _REPO_ROOT / "mediahive" / "assets"
|
_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:
|
def _platform_zip_suffix() -> str:
|
||||||
machine = platform.machine().lower()
|
machine = platform.machine().lower()
|
||||||
@@ -180,6 +190,80 @@ def ensure_macos_icon() -> Path:
|
|||||||
return icon_icns
|
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:
|
def read_version() -> str:
|
||||||
"""Read version via setuptools_scm (same logic as hatch-vcs)."""
|
"""Read version via setuptools_scm (same logic as hatch-vcs)."""
|
||||||
return setuptools_scm.get_version(root=str(_REPO_ROOT))
|
return setuptools_scm.get_version(root=str(_REPO_ROOT))
|
||||||
@@ -292,6 +376,11 @@ def main() -> None:
|
|||||||
|
|
||||||
print(f"✓ Built successfully: {artifact_path}")
|
print(f"✓ Built successfully: {artifact_path}")
|
||||||
print(f" Size: {artifact_path.stat().st_size / (1024 * 1024):.1f} MB")
|
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:
|
except (FileNotFoundError, OSError, RuntimeError, ValueError) as e:
|
||||||
print(f"✗ Build failed: {e}", file=sys.stderr)
|
print(f"✗ Build failed: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- MediaHive per-user MSI installer.
|
||||||
|
|
||||||
|
Build defines (passed via candle -d):
|
||||||
|
MediaHiveVersion e.g. 1.2.3 (must be numeric x.y.z[.w] for MSI)
|
||||||
|
MediaHiveSourceDir path to the PyInstaller output folder (build/MediaHive)
|
||||||
|
-->
|
||||||
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||||
|
<Product Id="*"
|
||||||
|
Name="MediaHive"
|
||||||
|
Language="1033"
|
||||||
|
Version="$(var.MediaHiveVersion)"
|
||||||
|
Manufacturer="MediaHive"
|
||||||
|
UpgradeCode="8f3a2c1e-9b6d-4a7e-b5c4-2d1f0e9a8b7c">
|
||||||
|
|
||||||
|
<Package InstallerVersion="500"
|
||||||
|
Compressed="yes"
|
||||||
|
InstallScope="perUser"
|
||||||
|
InstallPrivileges="limited"
|
||||||
|
Description="MediaHive media library"
|
||||||
|
Comments="MediaHive desktop application" />
|
||||||
|
|
||||||
|
<MajorUpgrade DowngradeErrorMessage="A newer version of MediaHive is already installed."
|
||||||
|
AllowSameVersionUpgrades="yes" />
|
||||||
|
<MediaTemplate EmbedCab="yes" />
|
||||||
|
|
||||||
|
<Icon Id="MediaHiveIcon" SourceFile="$(var.MediaHiveSourceDir)\_internal\mediahive\assets\mediahive.ico" />
|
||||||
|
<Property Id="ARPPRODUCTICON" Value="MediaHiveIcon" />
|
||||||
|
|
||||||
|
<Feature Id="MainApplication" Title="MediaHive" Level="1">
|
||||||
|
<ComponentGroupRef Id="MediaHiveFiles" />
|
||||||
|
<ComponentRef Id="StartMenuShortcut" />
|
||||||
|
</Feature>
|
||||||
|
|
||||||
|
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||||
|
<Directory Id="LocalAppDataFolder">
|
||||||
|
<Directory Id="ProgramsFolder" Name="Programs">
|
||||||
|
<Directory Id="INSTALLFOLDER" Name="MediaHive" />
|
||||||
|
</Directory>
|
||||||
|
</Directory>
|
||||||
|
<Directory Id="ProgramMenuFolder" />
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
<!-- Start Menu shortcut (needs its own component; per-user keypath is a
|
||||||
|
registry entry under HKCU). -->
|
||||||
|
<DirectoryRef Id="ProgramMenuFolder">
|
||||||
|
<Component Id="StartMenuShortcut" Guid="5c1e9a2b-4d7f-4e6b-8a3c-1d0f2e9b7a65">
|
||||||
|
<Shortcut Id="MediaHiveStartMenuShortcut"
|
||||||
|
Name="MediaHive"
|
||||||
|
Target="[INSTALLFOLDER]MediaHive.exe"
|
||||||
|
WorkingDirectory="INSTALLFOLDER"
|
||||||
|
Icon="MediaHiveIcon" />
|
||||||
|
<RegistryValue Root="HKCU"
|
||||||
|
Key="Software\MediaHive"
|
||||||
|
Name="StartMenuShortcut"
|
||||||
|
Type="integer"
|
||||||
|
Value="1"
|
||||||
|
KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
</DirectoryRef>
|
||||||
|
</Product>
|
||||||
|
</Wix>
|
||||||
+6
-4
@@ -67,13 +67,14 @@ def load_token() -> str:
|
|||||||
# ZIP + dist helpers
|
# ZIP + dist helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Matches MediaHive-1.2.3-win64.zip, MediaHive-1.2.3-macos-arm64.dmg, etc.
|
# Matches MediaHive-1.2.3-win64.zip, MediaHive-1.2.3-win64.msi,
|
||||||
|
# MediaHive-1.2.3-macos-arm64.dmg, etc.
|
||||||
# Rejects dev/dirty versions like MediaHive-1.2.3.dev0+gabcd-win64.zip
|
# Rejects dev/dirty versions like MediaHive-1.2.3.dev0+gabcd-win64.zip
|
||||||
_CLEAN_ARTIFACT_RE = re.compile(r"^MediaHive-(\d+(?:\.\d+)*)-([A-Za-z0-9._-]+)\.(?:zip|dmg)$")
|
_CLEAN_ARTIFACT_RE = re.compile(r"^MediaHive-(\d+(?:\.\d+)*)-([A-Za-z0-9._-]+)\.(?:zip|dmg|msi)$")
|
||||||
|
|
||||||
|
|
||||||
def find_releasable_artifacts() -> list[tuple[Path, str, str]]:
|
def find_releasable_artifacts() -> list[tuple[Path, str, str]]:
|
||||||
"""Return (path, version, platform_tag) for clean-versioned ZIPs/DMGs in build/."""
|
"""Return (path, version, platform_tag) for clean-versioned ZIPs/DMGs/MSIs in build/."""
|
||||||
build_dir = REPO_ROOT / "build"
|
build_dir = REPO_ROOT / "build"
|
||||||
results = []
|
results = []
|
||||||
for p in sorted(build_dir.glob("MediaHive-*")):
|
for p in sorted(build_dir.glob("MediaHive-*")):
|
||||||
@@ -183,6 +184,7 @@ def upload_asset(
|
|||||||
mime = {
|
mime = {
|
||||||
".zip": "application/zip",
|
".zip": "application/zip",
|
||||||
".dmg": "application/x-apple-diskimage",
|
".dmg": "application/x-apple-diskimage",
|
||||||
|
".msi": "application/x-msi",
|
||||||
}.get(path.suffix, "application/octet-stream")
|
}.get(path.suffix, "application/octet-stream")
|
||||||
print(f"Uploading {path.name} ({size_mb:.1f} MB) ...")
|
print(f"Uploading {path.name} ({size_mb:.1f} MB) ...")
|
||||||
with Path(path).open("rb") as fh:
|
with Path(path).open("rb") as fh:
|
||||||
@@ -228,7 +230,7 @@ def main() -> None:
|
|||||||
artifacts = find_releasable_artifacts()
|
artifacts = find_releasable_artifacts()
|
||||||
if not artifacts:
|
if not artifacts:
|
||||||
print(
|
print(
|
||||||
"No clean-versioned ZIPs/DMGs found in build/.\n"
|
"No clean-versioned ZIPs/DMGs/MSIs found in build/.\n"
|
||||||
"Run scripts/guibuild.py first.",
|
"Run scripts/guibuild.py first.",
|
||||||
file=sys.stderr,
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user