From 832c8b764d2b9f41645f50083d8640bbf22a795a Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:41:01 -0700 Subject: [PATCH 1/3] fix(xtask): embed the CLI binary in the macOS app bundle The macOS release bundle only embedded openlogi-gui, so the openlogi CLI was missing from the .app (the Linux package and the Homebrew cask both expect it under Contents/MacOS/openlogi). Add an embed_cli step mirroring embed_agent_helper, and a verify_bundle_binaries check so a future regression fails the build. --- xtask/src/commands/macos/bundle.rs | 70 ++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/xtask/src/commands/macos/bundle.rs b/xtask/src/commands/macos/bundle.rs index 8c5529a9..a99b7d8b 100644 --- a/xtask/src/commands/macos/bundle.rs +++ b/xtask/src/commands/macos/bundle.rs @@ -99,6 +99,8 @@ pub(crate) fn run() -> Result<()> { let app = root.join("target/release/bundle/osx/OpenLogi.app"); ensure_dir(&app)?; embed_agent_helper(&root, &app, &xcode_env)?; + embed_cli(&root, &app, &xcode_env)?; + verify_bundle_binaries(&app)?; println!(); println!("Bundle ready: {}", app.display()); Ok(()) @@ -150,6 +152,33 @@ fn embed_agent_helper(root: &Path, app: &Path, xcode_env: &[(String, String)]) - Ok(()) } +fn embed_cli(root: &Path, app: &Path, xcode_env: &[(String, String)]) -> Result<()> { + let sh = Shell::new()?; + let _repo = sh.push_dir(root); + println!("==> cli (build)"); + cmd!(sh, "cargo build -p openlogi --release") + .envs(xcode_env.iter().map(|(key, value)| (key, value))) + .run()?; + let cli_bin = root.join("target/release/openlogi"); + ensure_file(&cli_bin)?; + + let macos = app.join("Contents/MacOS"); + fs_err::copy(&cli_bin, macos.join("openlogi")) + .with_context(|| "could not copy the CLI binary into the app bundle".to_string())?; + + println!(" embedded {}", macos.join("openlogi").display()); + Ok(()) +} + +fn verify_bundle_binaries(app: &Path) -> Result<()> { + for binary in ["openlogi", "openlogi-gui"] { + let path = app.join("Contents/MacOS").join(binary); + ensure_file(&path) + .with_context(|| format!("missing required bundle binary {}", path.display()))?; + } + Ok(()) +} + fn stamp_bundle_version(info_plist: &Path, version: &str) -> Result<()> { let mut plist = Value::from_file(info_plist) .with_context(|| format!("could not read {}", info_plist.display()))?; @@ -208,3 +237,44 @@ fn codesign_runtime(identity: &str, target: &Path) -> Result<()> { .run()?; Ok(()) } + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + + fn app_with_binaries(binaries: &[&str]) -> tempfile::TempDir { + let app = tempfile::tempdir().unwrap(); + let macos = app.path().join("Contents/MacOS"); + fs_err::create_dir_all(&macos).unwrap(); + for binary in binaries { + fs_err::write(macos.join(binary), b"").unwrap(); + } + app + } + + #[test] + fn verify_bundle_binaries_accepts_cli_and_gui() { + let app = app_with_binaries(&["openlogi", "openlogi-gui"]); + + verify_bundle_binaries(app.path()).unwrap(); + } + + #[test] + fn verify_bundle_binaries_rejects_missing_cli() { + let app = app_with_binaries(&["openlogi-gui"]); + + let error = verify_bundle_binaries(app.path()).unwrap_err(); + + assert!(error.to_string().contains("openlogi")); + } + + #[test] + fn verify_bundle_binaries_rejects_missing_gui() { + let app = app_with_binaries(&["openlogi"]); + + let error = verify_bundle_binaries(app.path()).unwrap_err(); + + assert!(error.to_string().contains("openlogi-gui")); + } +} From 491d796eb16073e872f538aec1bd79983c1108c8 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:45:58 -0700 Subject: [PATCH 2/3] fix(xtask): sign the embedded CLI in the inside-out codesign flow --- xtask/src/commands/macos/bundle.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/xtask/src/commands/macos/bundle.rs b/xtask/src/commands/macos/bundle.rs index a99b7d8b..a4c1138f 100644 --- a/xtask/src/commands/macos/bundle.rs +++ b/xtask/src/commands/macos/bundle.rs @@ -219,11 +219,21 @@ pub(crate) fn sign_app(identity: &str) -> Result<()> { if helper.exists() { codesign_runtime(identity, &helper)?; } + // The embedded CLI is a second Mach-O under Contents/MacOS; sign it with the + // hardened runtime before the outer app so it carries a Developer ID + // signature (its as-built ad-hoc signature would fail notarization). + let cli = app.join("Contents/MacOS/openlogi"); + if cli.exists() { + codesign_runtime(identity, &cli)?; + } codesign_runtime(identity, &app)?; cmd!(sh, "codesign --verify --strict {app}").run()?; if helper.exists() { cmd!(sh, "codesign --verify --strict {helper}").run()?; } + if cli.exists() { + cmd!(sh, "codesign --verify --strict {cli}").run()?; + } Ok(()) } From d7c357cf1b5054943ff8244b6c4cba31b2dfe208 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:49:11 -0700 Subject: [PATCH 3/3] ci(release): sign the embedded CLI in the macOS release signing flow --- .github/workflows/release.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a695adda..86196258 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,6 +115,7 @@ jobs: set -euo pipefail app="target/release/bundle/osx/OpenLogi.app" helper="$app/Contents/Library/LoginItems/OpenLogiAgent.app" + cli="$app/Contents/MacOS/openlogi" # Inside-out signing: seal the nested agent helper with its own # signature first, then the outer app (which seals the signed helper). # --deep is deprecated and can't give the helper an independent @@ -124,10 +125,18 @@ jobs: codesign --force --options runtime --timestamp \ --sign "$APPLE_SIGNING_IDENTITY" "$helper" fi + # The embedded CLI is a second Mach-O under Contents/MacOS; sign it + # with the hardened runtime before the outer app or notarization + # rejects its as-built ad-hoc signature. + if [ -f "$cli" ]; then + codesign --force --options runtime --timestamp \ + --sign "$APPLE_SIGNING_IDENTITY" "$cli" + fi codesign --force --options runtime --timestamp \ --sign "$APPLE_SIGNING_IDENTITY" "$app" codesign --verify --strict --verbose=2 "$app" [ -d "$helper" ] && codesign --verify --strict --verbose=2 "$helper" + [ -f "$cli" ] && codesign --verify --strict --verbose=2 "$cli" - name: Create and sign DMG env: