From e2064a578478e6f6c3aa00f7ef4e7af95d434c21 Mon Sep 17 00:00:00 2001 From: Julian Chang Date: Fri, 7 Aug 2026 22:07:26 +0700 Subject: [PATCH] package visual c++ directly in windows installer --- .github/workflows/oneclient_nightly.yml | 21 +++++++ .github/workflows/oneclient_release.yml | 21 +++++++ distribution/windows/bundle-vcredist.py | 81 +++++++++++++++++++++++++ packages/oneclient_app/Cargo.toml | 17 ++---- packages/oneclient_app/build.rs | 13 +++- 5 files changed, 141 insertions(+), 12 deletions(-) create mode 100644 distribution/windows/bundle-vcredist.py diff --git a/.github/workflows/oneclient_nightly.yml b/.github/workflows/oneclient_nightly.yml index de4dc2ec..242edc38 100644 --- a/.github/workflows/oneclient_nightly.yml +++ b/.github/workflows/oneclient_nightly.yml @@ -132,6 +132,27 @@ jobs: rm -f "$out/OneClient.icns" test -f "$out/Assets.car" || { echo "::error::actool produced no Assets.car"; exit 1; } + - name: (Windows) Bundle VC++ redistributable into the installer + if: matrix.platform == 'windows-latest' + shell: bash + run: python3 distribution/windows/bundle-vcredist.py + + - name: (Windows) Verify bundled redistributable is Microsoft-signed + if: matrix.platform == 'windows-latest' + shell: pwsh + run: | + $sig = Get-AuthenticodeSignature 'target/vcredist/vc_redist.x64.exe' + Write-Host "Authenticode status: $($sig.Status)" + Write-Host "Signer: $($sig.SignerCertificate.Subject)" + if ($sig.Status -ne 'Valid') { + Write-Error "vc_redist.x64.exe signature is $($sig.Status), refusing to bundle it" + exit 1 + } + if ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') { + Write-Error "vc_redist.x64.exe is not signed by Microsoft: $($sig.SignerCertificate.Subject)" + exit 1 + } + - name: Package (unsigned) shell: bash env: diff --git a/.github/workflows/oneclient_release.yml b/.github/workflows/oneclient_release.yml index f6775d7e..1bd43898 100644 --- a/.github/workflows/oneclient_release.yml +++ b/.github/workflows/oneclient_release.yml @@ -345,6 +345,27 @@ jobs: rm -f "$out/OneClient.icns" test -f "$out/Assets.car" || { echo "::error::actool produced no Assets.car"; exit 1; } + - name: (Windows) Bundle VC++ redistributable into the installer + if: matrix.platform == 'windows-latest' + shell: bash + run: python3 distribution/windows/bundle-vcredist.py + + - name: (Windows) Verify bundled redistributable is Microsoft-signed + if: matrix.platform == 'windows-latest' + shell: pwsh + run: | + $sig = Get-AuthenticodeSignature 'target/vcredist/vc_redist.x64.exe' + Write-Host "Authenticode status: $($sig.Status)" + Write-Host "Signer: $($sig.SignerCertificate.Subject)" + if ($sig.Status -ne 'Valid') { + Write-Error "vc_redist.x64.exe signature is $($sig.Status), refusing to bundle it" + exit 1 + } + if ($sig.SignerCertificate.Subject -notmatch 'O=Microsoft Corporation') { + Write-Error "vc_redist.x64.exe is not signed by Microsoft: $($sig.SignerCertificate.Subject)" + exit 1 + } + - name: Package shell: bash env: diff --git a/distribution/windows/bundle-vcredist.py b/distribution/windows/bundle-vcredist.py new file mode 100644 index 00000000..0d040c8b --- /dev/null +++ b/distribution/windows/bundle-vcredist.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +import argparse +import hashlib +import re +import sys +import urllib.request +from pathlib import Path + +REDIST_URL = "https://aka.ms/vs/17/release/vc_redist.x64.exe" +BEGIN = "; >>> VC_REDIST_INSTALL >>>" +END = "; <<< VC_REDIST_INSTALL <<<" + +REPO_ROOT = Path(__file__).resolve().parents[2] +DEFAULT_CARGO_TOML = REPO_ROOT / "packages" / "oneclient_app" / "Cargo.toml" +DEFAULT_REDIST = REPO_ROOT / "target" / "vcredist" / "vc_redist.x64.exe" + +BLOCK = """ {begin} + ; Generated by distribution/windows/bundle-vcredist.py -- do not edit by hand. + DetailPrint "Installing Microsoft Visual C++ Redistributable (x64)..." + InitPluginsDir + File "/oname=$PLUGINSDIR\\\\vc_redist.x64.exe" "{redist}" + ExecWait '"$PLUGINSDIR\\\\vc_redist.x64.exe" /quiet /norestart' $R3 + ${{If}} $R3 == 3010 + SetRebootFlag true + ${{EndIf}} + {end}""" + +REGION = re.compile( + r"^[ \t]*" + re.escape(BEGIN) + r".*?^[ \t]*" + re.escape(END) + r"[ \t]*$", + re.DOTALL | re.MULTILINE, +) + + +def download(dest: Path) -> None: + dest.parent.mkdir(parents=True, exist_ok=True) + print(f"downloading {REDIST_URL} -> {dest}") + with urllib.request.urlopen(REDIST_URL) as resp, dest.open("wb") as out: + while chunk := resp.read(1 << 20): + out.write(chunk) + + data = dest.read_bytes() + if data[:2] != b"MZ": + sys.exit(f"error: {dest} is not a PE image (no MZ header)") + if len(data) < 10 * 1024 * 1024: + sys.exit(f"error: {dest} is only {len(data)} bytes, expected >10MB") + print(f" {len(data)} bytes, sha256={hashlib.sha256(data).hexdigest()}") + + +def patch(cargo_toml: Path, redist: Path) -> None: + text = cargo_toml.read_text(encoding="utf-8") + if not REGION.search(text): + sys.exit(f"error: {BEGIN} .. {END} region not found in {cargo_toml}") + + block = BLOCK.format(begin=BEGIN, end=END, redist=str(redist).replace("\\", "\\\\")) + cargo_toml.write_text(REGION.sub(lambda _: block, text, count=1), encoding="utf-8") + print(f"patched {cargo_toml} to embed {redist}") + + +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument("--cargo-toml", type=Path, default=DEFAULT_CARGO_TOML) + ap.add_argument("--redist", type=Path, default=DEFAULT_REDIST) + ap.add_argument( + "--skip-download", + action="store_true", + help="reuse an already-downloaded --redist instead of fetching it", + ) + args = ap.parse_args() + + redist = args.redist.resolve() + if args.skip_download: + if not redist.is_file(): + sys.exit(f"error: --skip-download given but {redist} does not exist") + else: + download(redist) + + patch(args.cargo_toml.resolve(), redist) + + +if __name__ == "__main__": + main() diff --git a/packages/oneclient_app/Cargo.toml b/packages/oneclient_app/Cargo.toml index c0ee6795..d9869174 100644 --- a/packages/oneclient_app/Cargo.toml +++ b/packages/oneclient_app/Cargo.toml @@ -63,7 +63,7 @@ winresource = "=0.1.31" product-name = "OneClient" identifier = "org.polyfrost.OneClient" category = "Game" -publisher = "Polyfrost" +publisher = "Polyfrost Inc." copyright = "© 2026 Polyfrost Inc." homepage = "https://polyfrost.org/projects/oneclient" long-description = "Next-generation open source Minecraft launcher" @@ -115,17 +115,12 @@ Section "-VCRedist" Goto vcredist_done ${EndIf} - DetailPrint "Installing Microsoft Visual C++ Redistributable (x64)..." - nsis_tauri_utils::Download "https://aka.ms/vs/17/release/vc_redist.x64.exe" "$PLUGINSDIR\\vc_redist.x64.exe" - Pop $R2 - ${If} $R2 == 0 - ExecWait '"$PLUGINSDIR\\vc_redist.x64.exe" /quiet /norestart' $R3 - ${If} $R3 == 3010 - SetRebootFlag true - ${EndIf} - ${ElseIf} $PassiveMode != 1 - MessageBox MB_OK|MB_ICONEXCLAMATION "Could not download the Microsoft Visual C++ Redistributable. OneClient may fail to start until you install it manually from https://aka.ms/vs/17/release/vc_redist.x64.exe" + ; >>> VC_REDIST_INSTALL >>> + ${If} $PassiveMode != 1 + MessageBox MB_OK|MB_ICONEXCLAMATION "OneClient needs the Microsoft Visual C++ Redistributable (x64) 14.40 or newer. Install it from https://aka.ms/vs/17/release/vc_redist.x64.exe, then run this installer again." ${EndIf} + ; <<< VC_REDIST_INSTALL <<< + vcredist_done: SectionEnd """ diff --git a/packages/oneclient_app/build.rs b/packages/oneclient_app/build.rs index 4d3261e9..5918f392 100644 --- a/packages/oneclient_app/build.rs +++ b/packages/oneclient_app/build.rs @@ -1,9 +1,20 @@ fn main() { - // Embeds the app icon into the Windows executable. + // Embeds the app icon and VERSIONINFO into the Windows executable. #[cfg(windows)] { let mut res = winresource::WindowsResource::new(); res.set_icon("icons/icon.ico"); + + let version = env!("CARGO_PKG_VERSION"); + res.set("CompanyName", "Polyfrost Inc."); + res.set("ProductName", "OneClient"); + res.set("FileDescription", "OneClient"); + res.set("LegalCopyright", "© 2026 Polyfrost Inc."); + res.set("OriginalFilename", "oneclient_app.exe"); + res.set("InternalName", "oneclient_app"); + res.set("FileVersion", version); + res.set("ProductVersion", version); + res.compile() .expect("failed to embed Windows icon resource"); }