Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ jobs:
run: cargo test --manifest-path src-tauri/Cargo.toml
env:
NIXMAC_ENV: production
# Packaged builds hard-fail without the packaged source revision
# (build.rs embeds it as the build identity).
NIXMAC_BUILD_ID: ${{ github.sha }}
build:
needs: rust-tests
runs-on: [self-hosted, macOS]
Expand Down Expand Up @@ -170,6 +173,26 @@ jobs:
run: cargo clippy --workspace --all-targets --features nixmac/codegen -- -D warnings
env:
NIXMAC_ENV: production
# Packaged builds hard-fail without the packaged source revision.
NIXMAC_BUILD_ID: ${{ github.sha }}

# Same test set as the Linux `rust-tests` job, run again here because a
# large share of this crate's behavioral coverage is
# `cfg(target_os = "macos")` and compiles away to nothing on Linux — the
# privileged helper's socket serving, peer authentication, and launchd
# paths among it. Without this step CI can be green while none of it has
# ever executed.
#
# Placed with clippy: after the toolchain and passkey patch are in place,
# before the sccache setup and the expensive release build, so a failure
# is fast and cheap. `NIXMAC_ENV`/`NIXMAC_BUILD_ID` match the clippy and
# build steps — build.rs reruns on those, so a different value here would
# force a rebuild of everything that follows.
- name: Rust unit tests (macOS)
run: cargo test -p nixmac
env:
NIXMAC_ENV: production
NIXMAC_BUILD_ID: ${{ github.sha }}

# Decide what kind of build this is:
# - tag: push of refs/tags/v* → ship that exact version on stable
Expand Down Expand Up @@ -252,6 +275,9 @@ jobs:
# Pass DSNs and build metadata into the tauri action step so build.rs can read them
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
NIXMAC_ENV: production
# Build identity compiled into the GUI, helper, and sync agent; the
# sidecar build inherits it through the environment.
NIXMAC_BUILD_ID: ${{ github.sha }}
NIXMAC_VERSION: ${{ steps.sync-version.outputs.build_version }}
VITE_SERVER_URL: ${{ secrets.VITE_SERVER_URL }}
SUBMITTED_FEEDBACK_DSN: ${{ secrets.SUBMITTED_FEEDBACK_DSN }}
Expand Down
3 changes: 3 additions & 0 deletions apps/native/scripts/build-tauri-sidecars.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const tauriConf = JSON.parse(
await readFile(path.join(root, "src-tauri", "tauri.conf.json"), "utf8"),
);
const minimumSystemVersion = tauriConf.bundle?.macOS?.minimumSystemVersion;
// execa extends process.env by default, so NIXMAC_ENV and NIXMAC_BUILD_ID
// reach build.rs unchanged: the helper and sync agent must compile in the same
// build identity as the GUI built from this environment.
const cargoEnv =
process.platform === "darwin" && minimumSystemVersion
? { MACOSX_DEPLOYMENT_TARGET: minimumSystemVersion }
Expand Down
52 changes: 52 additions & 0 deletions apps/native/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ mod env_keys {
include!("src/env_keys.rs");
}

mod build_id {
#![allow(dead_code)]
include!("src/build_id.rs");
}

use std::path::Path;
use std::process::Command;

Expand Down Expand Up @@ -88,6 +93,52 @@ fn embed_signing_team_id() {
}
}

/// Embed the build identity (`NIXMAC_BUILD_ID`, supplied by CI from the
/// packaged source revision) into every target of this crate — the GUI, the
/// helper, and the sync agent — and stamp the same string into the plist the
/// macOS bundler merges into the app's `Info.plist`. Packaged builds
/// (`NIXMAC_ENV` = `production`) hard-fail on a missing or empty value;
/// development builds fall back to a fixed literal. Git is deliberately never
/// run here: the value must describe the packaged source, which only the build
/// orchestrator knows.
///
/// One resolution feeds both the compiled constant and the on-disk stamp, so a
/// GUI comparing itself against the bundle it was built from always matches.
fn embed_build_id() {
println!("cargo:rerun-if-env-changed=NIXMAC_BUILD_ID");
println!("cargo:rerun-if-env-changed=NIXMAC_ENV");

let packaged = matches!(std::env::var("NIXMAC_ENV").as_deref(), Ok("production"));
let raw = std::env::var("NIXMAC_BUILD_ID").ok();
let build_id = match build_id::resolve_build_id(raw.as_deref(), packaged) {
Ok(build_id) => build_id,
Err(error) => panic!("{error}"),
};
println!("cargo:rustc-env=NIXMAC_BUILD_ID={build_id}");
stamp_bundle_build_id(&build_id);
}

/// Write the stamped copy of the tracked `Info.plist` that
/// `bundle > macOS > infoPlist` points at.
///
/// The stamp has to live in the bundle rather than only in the executables: a
/// running GUI reads it to notice that its own bundle was replaced on disk. The
/// tracked template stays the source of every other key; this copy is generated
/// output.
fn stamp_bundle_build_id(build_id: &str) {
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
let template = crate_dir.join(build_id::INFO_PLIST_TEMPLATE_PATH);
let stamped = crate_dir.join(build_id::STAMPED_INFO_PLIST_PATH);
println!("cargo:rerun-if-changed={}", template.display());
// Regenerate when the output is missing (a cleaned checkout): a bundle
// without the stamp reads as somebody else's build to every GUI.
println!("cargo:rerun-if-changed={}", stamped.display());

if let Err(error) = build_id::write_stamped_info_plist(&template, &stamped, build_id) {
panic!("{error}");
}
}

fn add_debug_swift_runtime_rpaths() {
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() != Ok("macos")
|| std::env::var("PROFILE").as_deref() != Ok("debug")
Expand Down Expand Up @@ -127,6 +178,7 @@ fn add_debug_swift_runtime_rpaths() {
fn main() {
embed_build_profile();
embed_signing_team_id();
embed_build_id();
add_debug_swift_runtime_rpaths();

// Set up passthrough for relevant environment variables.
Expand Down
Loading
Loading