From acab171bc5eb514bc50342372183878ac87563c8 Mon Sep 17 00:00:00 2001 From: Timon Date: Fri, 6 Feb 2026 14:37:07 +0100 Subject: [PATCH] Desktop: Build and Sign Mac and Windows Bundles in CI (#3728) * Desktop: Build and Sign Mac and Windows Bundles in CI * Desktop: Remove unnecessary CEF files from Windows Bundle * Desktop: Use a temp file for license generation on Windows to avoid PowerShell modifying stdout --- .github/workflows/build-mac-bundle.yml | 153 ++++++++++++++++++++++ .github/workflows/build-win-bundle.yml | 167 +++++++++++++++++++++++++ desktop/bundle/src/win.rs | 25 ++++ desktop/src/app.rs | 2 +- frontend/vite.config.ts | 39 ++++-- 5 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/build-mac-bundle.yml create mode 100644 .github/workflows/build-win-bundle.yml diff --git a/.github/workflows/build-mac-bundle.yml b/.github/workflows/build-mac-bundle.yml new file mode 100644 index 00000000..ee6d1bbb --- /dev/null +++ b/.github/workflows/build-mac-bundle.yml @@ -0,0 +1,153 @@ +name: Build Mac Bundle + +on: + push: + branches: + - master + +jobs: + build: + runs-on: macos-latest + + env: + WASM_BINDGEN_CLI_VERSION: "0.2.100" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + override: true + rustflags: "" + target: wasm32-unknown-unknown + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + frontend/package-lock.json + + - name: Install Native Dependencies + env: + GITHUB_TOKEN: ${{ github.token }} + BINSTALL_DISABLE_TELEMETRY: "true" + run: | + brew update + brew install \ + pkg-config \ + openssl@3 \ + binaryen \ + llvm \ + cargo-binstall + + echo "OPENSSL_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV + echo "PKG_CONFIG_PATH=$(brew --prefix openssl@3)/lib/pkgconfig" >> $GITHUB_ENV + echo "$(brew --prefix llvm)/bin" >> $GITHUB_PATH + + cargo binstall --no-confirm --force wasm-pack + cargo binstall --no-confirm --force cargo-about + cargo binstall --no-confirm --force "wasm-bindgen-cli@${WASM_BINDGEN_CLI_VERSION}" + + - name: Build Mac Bundle + env: + CARGO_TERM_COLOR: always + run: npm run build-desktop + + - name: Stage Artifacts + shell: bash + run: | + rm -rf target/artifacts + mkdir -p target/artifacts + cp -R target/release/Graphite.app target/artifacts/Graphite.app + + - name: Upload Mac Bundle + uses: actions/upload-artifact@v4 + with: + name: graphite-mac-bundle + path: target/artifacts + + - name: Sign and Notarize Mac Bundle Preparation + env: + APPLE_CERT_BASE64: ${{ secrets.APPLE_CERT_BASE64 }} + APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} + run: | + mkdir -p .sign + echo "$APPLE_CERT_BASE64" | base64 --decode > .sign/certificate.p12 + + security create-keychain -p "" .sign/main.keychain + security default-keychain -s .sign/main.keychain + security unlock-keychain -p "" .sign/main.keychain + security set-keychain-settings -t 3600 -u .sign/main.keychain + + security import .sign/certificate.p12 -k .sign/main.keychain -P "$APPLE_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign + security set-key-partition-list -S apple-tool:,apple: -s -k "" .sign/main.keychain + + cat > .sign/entitlements.plist <<'EOF' + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.disable-executable-page-protection + + com.apple.security.cs.disable-library-validation + + + + EOF + + - name: Sign and Notarize Mac Bundle + env: + APPLE_EMAIL: ${{ secrets.APPLE_EMAIL }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_CERT_NAME: ${{ secrets.APPLE_CERT_NAME }} + run: | + CERTIFICATE="$APPLE_CERT_NAME" + ENTITLEMENTS=".sign/entitlements.plist" + APP_PATH="target/artifacts/Graphite.app" + ZIP_PATH=".sign/Graphite.zip" + + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper.app" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (GPU).app" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Graphite Helper (Renderer).app" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libcef_sandbox.dylib" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libEGL.dylib" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH/Contents/Frameworks/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib" + codesign --force --options runtime --entitlements "$ENTITLEMENTS" --sign "$CERTIFICATE" "$APP_PATH" --deep + + codesign --verify --deep --strict --verbose=4 "$APP_PATH" + + ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH" + xcrun notarytool submit "$ZIP_PATH" --wait --apple-id "$APPLE_EMAIL" --team-id "$APPLE_TEAM_ID" --password "$APPLE_PASSWORD" + rm "$ZIP_PATH" + + xcrun stapler staple -v "$APP_PATH" + + spctl -a -vv "$APP_PATH" + + - name: Upload Mac Bundle Signed + uses: actions/upload-artifact@v4 + with: + name: graphite-mac-bundle-signed + path: target/artifacts diff --git a/.github/workflows/build-win-bundle.yml b/.github/workflows/build-win-bundle.yml new file mode 100644 index 00000000..173efbf8 --- /dev/null +++ b/.github/workflows/build-win-bundle.yml @@ -0,0 +1,167 @@ +name: Build Windows Bundle + +on: + push: + branches: + - master + +permissions: + contents: read + id-token: write + +jobs: + build: + runs-on: windows-latest + + env: + WASM_BINDGEN_CLI_VERSION: "0.2.100" + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + override: true + rustflags: "" + target: wasm32-unknown-unknown + + - name: Cache Cargo + uses: actions/cache@v4 + with: + path: | + ${{ env.USERPROFILE }}\.cargo\registry + ${{ env.USERPROFILE }}\.cargo\git + target + key: cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }} + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: | + package-lock.json + frontend/package-lock.json + + - name: Setup Cargo Binstall + uses: cargo-bins/cargo-binstall@main + + - name: Install Native Dependencies + shell: pwsh + env: + GITHUB_TOKEN: ${{ github.token }} + BINSTALL_DISABLE_TELEMETRY: "true" + run: | + winget install --id LLVM.LLVM -e --accept-package-agreements --accept-source-agreements + winget install --id Kitware.CMake -e --accept-package-agreements --accept-source-agreements + winget install --id OpenSSL.OpenSSL -e --accept-package-agreements --accept-source-agreements + winget install --id WebAssembly.Binaryen -e --accept-package-agreements --accept-source-agreements + winget install --id GnuWin32.PkgConfig -e --accept-package-agreements --accept-source-agreements + + "OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" | Out-File -FilePath $env:GITHUB_ENV -Append + "PKG_CONFIG_PATH=C:\Program Files\OpenSSL-Win64\lib\pkgconfig" | Out-File -FilePath $env:GITHUB_ENV -Append + + cargo binstall --no-confirm --force wasm-pack + cargo binstall --no-confirm --force cargo-about + cargo binstall --no-confirm --force "wasm-bindgen-cli@$env:WASM_BINDGEN_CLI_VERSION" + + - name: Build Windows Bundle + env: + CARGO_TERM_COLOR: always + run: npm run build-desktop + + - name: Stage Artifacts + shell: bash + run: | + rm -rf target/artifacts + mkdir -p target/artifacts + cp -R target/release/Graphite target/artifacts/Graphite + + - name: Upload Windows Bundle + uses: actions/upload-artifact@v4 + with: + name: graphite-windows-bundle + path: target/artifacts + + - name: Azure login + uses: azure/login@v1 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + enable-AzPSSession: true + + - name: Sign + uses: azure/artifact-signing-action@v1 + with: + endpoint: https://eus.codesigning.azure.net/ + signing-account-name: Graphite + certificate-profile-name: Graphite + files: | + ${{ github.workspace }}\target\artifacts\Graphite\Graphite.exe + ${{ github.workspace }}\target\artifacts\Graphite\libcef.dll + ${{ github.workspace }}\target\artifacts\Graphite\chrome_elf.dll + ${{ github.workspace }}\target\artifacts\Graphite\vulkan-1.dll + ${{ github.workspace }}\target\artifacts\Graphite\dxcompiler.dll + ${{ github.workspace }}\target\artifacts\Graphite\libEGL.dll + ${{ github.workspace }}\target\artifacts\Graphite\libGLESv2.dll + ${{ github.workspace }}\target\artifacts\Graphite\vk_swiftshader.dll + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + correlation-id: ${{ github.sha }} + + - name: Verify Signatures + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + $TargetDir = "target\artifacts\Graphite" + + if (-not (Test-Path $TargetDir)) { + throw "TargetDir not found: $TargetDir" + } + + $UnsignedOrBad = @() + + Get-ChildItem -Path $TargetDir -Recurse -File -Include *.exe,*.dll | ForEach-Object { + $sig = Get-AuthenticodeSignature -FilePath $_.FullName + + if ($sig.Status -ne 'Valid') { + $UnsignedOrBad += "$($_.FullName) (Status=$($sig.Status))" + } + } + + if ($UnsignedOrBad.Count -gt 0) { + Write-Host "Unsigned or invalid binaries detected:" + $UnsignedOrBad | ForEach-Object { + Write-Host "::error::$_" + } + + if ($env:GITHUB_STEP_SUMMARY) { + "### ❌ Unsigned or invalid binaries detected" | + Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + "" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + $UnsignedOrBad | ForEach-Object { + "* `$_" | Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + } + } + + exit 1 + } + + Write-Host "All binaries are signed and valid." + + if ($env:GITHUB_STEP_SUMMARY) { + "### ✅ All binaries are signed and valid" | + Out-File $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8 + } + + - name: Upload Windows Bundle Signed + uses: actions/upload-artifact@v4 + with: + name: graphite-windows-bundle-signed + path: target/artifacts diff --git a/desktop/bundle/src/win.rs b/desktop/bundle/src/win.rs index 78867d18..1b7a7279 100644 --- a/desktop/bundle/src/win.rs +++ b/desktop/bundle/src/win.rs @@ -27,8 +27,33 @@ fn bundle(out_dir: &Path, app_bin: &Path) -> PathBuf { copy_dir(&cef_path(), &app_dir); + if let Err(e) = remove_unnecessary_cef_files(&app_dir) { + eprintln!("Failed to remove unnecessary CEF files: {}", e); + } + let bin_path = app_dir.join(EXECUTABLE); fs::copy(app_bin, &bin_path).unwrap(); bin_path } + +fn remove_unnecessary_cef_files(app_dir: &Path) -> Result<(), Box> { + fs::remove_dir_all(app_dir.join("cmake"))?; + fs::remove_dir_all(app_dir.join("include"))?; + fs::remove_dir_all(app_dir.join("libcef_dll"))?; + + for entry in fs::read_dir(app_dir.join("locales"))? { + let path = entry?.path(); + if path.is_file() && path.file_name() != Some("en-US.pak".as_ref()) { + fs::remove_file(path)?; + } + } + + fs::remove_file(app_dir.join("archive.json"))?; + fs::remove_file(app_dir.join("CMakeLists.txt"))?; + fs::remove_file(app_dir.join("bootstrapc.exe"))?; + fs::remove_file(app_dir.join("bootstrap.exe"))?; + fs::remove_file(app_dir.join("libcef.lib"))?; + + Ok(()) +} diff --git a/desktop/src/app.rs b/desktop/src/app.rs index 04239583..346f37e5 100644 --- a/desktop/src/app.rs +++ b/desktop/src/app.rs @@ -635,7 +635,7 @@ impl ApplicationHandler for App { } } - fn new_events(&mut self, event_loop: &dyn ActiveEventLoop, cause: winit::event::StartCause) { + fn new_events(&mut self, _event_loop: &dyn ActiveEventLoop, cause: winit::event::StartCause) { if let StartCause::ResumeTimeReached { .. } = cause && let Some(window) = &self.window { diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 5e8203a5..5713812e 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -2,6 +2,7 @@ import { spawnSync } from "child_process"; import fs from "fs"; +import os from "os"; import path from "path"; import { svelte } from "@sveltejs/vite-plugin-svelte"; @@ -298,12 +299,32 @@ function generateRustLicenses(): LicenseInfo[] { try { // Call `cargo about` in the terminal to generate the license information for Rust crates. // The `about.hbs` file is written so it generates a valid JavaScript array expression which we evaluate below. - const { stdout, stderr, status } = spawnSync("cargo", ["about", "generate", "about.hbs"], { - cwd: path.join(__dirname, ".."), - encoding: "utf8", - shell: true, - windowsHide: true, // Hide the terminal on Windows - }); + const { licenses, status, stderr } = (() => { + // On Windows, we have to write the output to a temporary file because of powershell's handling of stdout. + if (os.platform() === "win32") { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "graphite-licenses-")); + const licensesFile = path.join(tmpDir, "licenses.js"); + + const { status, stderr } = spawnSync("cargo", ["about", "generate", "about.hbs", "-o", licensesFile], { + cwd: path.join(__dirname, ".."), + encoding: "utf8", + shell: true, + windowsHide: true, // Hide the terminal on Windows + }); + + const licenses = fs.existsSync(licensesFile) ? fs.readFileSync(licensesFile, "utf8") : ""; + + return { licenses, status, stderr }; + } else { + const { stdout, status, stderr } = spawnSync("cargo", ["about", "generate", "about.hbs"], { + cwd: path.join(__dirname, ".."), + encoding: "utf8", + shell: true, + }); + + return { licenses: stdout, status, stderr }; + } + })(); // If the command failed, print the error message and exit early. if (status !== 0) { @@ -316,8 +337,8 @@ function generateRustLicenses(): LicenseInfo[] { // Make sure the output starts with this expected label, which lets us know the file generated with expected output. // We don't want to eval an error message or something else, so we fail early if that happens. - if (!stdout.trim().startsWith("GENERATED_BY_CARGO_ABOUT:")) { - console.error("Unexpected output from cargo-about", stdout); + if (!licenses.trim().startsWith("GENERATED_BY_CARGO_ABOUT:")) { + console.error("Unexpected output from cargo-about", licenses); return []; } @@ -325,7 +346,7 @@ function generateRustLicenses(): LicenseInfo[] { // Security-wise, eval() isn't any worse than require(), but it's able to work without a temporary file. // We call eval indirectly to avoid a warning as explained here: . const indirectEval = eval; - const licensesArray = indirectEval(stdout) as LicenseInfo[]; + const licensesArray = indirectEval(licenses) as LicenseInfo[]; // Remove the HTML character encoding caused by Handlebars. const rustLicenses = (licensesArray || []).map(