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(