diff --git a/crates/agentdesktop/macos/distribution.xml b/crates/agentdesktop/macos/distribution.xml
new file mode 100644
index 00000000..6118ddab
--- /dev/null
+++ b/crates/agentdesktop/macos/distribution.xml
@@ -0,0 +1,14 @@
+
+
This installer adds:
+Administrator permission is required. The command-line tool is not added to your shell's PATH.
+agentdesktop.dev | + Documentation
+ + diff --git a/frontend/desktop/scripts/package-macos.mjs b/frontend/desktop/scripts/package-macos.mjs index 0e24a0a3..6b75556f 100644 --- a/frontend/desktop/scripts/package-macos.mjs +++ b/frontend/desktop/scripts/package-macos.mjs @@ -2,11 +2,13 @@ import { spawnSync } from "node:child_process"; import { chmodSync, copyFileSync, + cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, + writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; @@ -108,6 +110,7 @@ const packagePath = path.join( const stagingDirectory = mkdtempSync(path.join(tmpdir(), "agentdesktop-pkg-")); const payloadRoot = path.join(stagingDirectory, "root"); const installerScriptsDirectory = path.join(stagingDirectory, "scripts"); +const installerResourcesDirectory = path.join(stagingDirectory, "resources"); try { const applicationsDirectory = path.join(payloadRoot, "Applications"); @@ -158,17 +161,45 @@ try { "--ownership", "recommended", ]; + const componentPath = path.join( + stagingDirectory, + "agentdesktop-component.pkg", + ); + packageArguments.push(componentPath); + run("/usr/bin/pkgbuild", packageArguments); + + cpSync(path.join(macosDirectory, "resources"), installerResourcesDirectory, { + recursive: true, + }); + const welcomePath = path.join(installerResourcesDirectory, "welcome.html"); + writeFileSync( + welcomePath, + readFileSync(welcomePath, "utf8").replace( + "{{AGENTDESKTOP_LOGO}}", + readFileSync(path.join(nativeDirectory, "icons", "128x128.png")).toString( + "base64", + ), + ), + ); + const productArguments = [ + "--distribution", + path.join(macosDirectory, "distribution.xml"), + "--resources", + installerResourcesDirectory, + "--package-path", + stagingDirectory, + ]; if (process.env.APPLE_INSTALLER_SIGNING_IDENTITY) { - packageArguments.push( + productArguments.push( "--sign", process.env.APPLE_INSTALLER_SIGNING_IDENTITY, ); if (process.env.APPLE_INSTALLER_KEYCHAIN) { - packageArguments.push("--keychain", process.env.APPLE_INSTALLER_KEYCHAIN); + productArguments.push("--keychain", process.env.APPLE_INSTALLER_KEYCHAIN); } } - packageArguments.push(packagePath); - run("/usr/bin/pkgbuild", packageArguments); + productArguments.push(packagePath); + run("/usr/bin/productbuild", productArguments); } finally { rmSync(stagingDirectory, { force: true, recursive: true }); } diff --git a/frontend/desktop/scripts/package-macos.test.mjs b/frontend/desktop/scripts/package-macos.test.mjs index 80d28503..dc8d0f79 100644 --- a/frontend/desktop/scripts/package-macos.test.mjs +++ b/frontend/desktop/scripts/package-macos.test.mjs @@ -1,7 +1,9 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { + cpSync, existsSync, + mkdirSync, mkdtempSync, readFileSync, rmSync, @@ -64,6 +66,123 @@ test("package installs the app at a fixed location", () => { ); }); +test("signs the branded product installer rather than the intermediate component", () => { + const script = readFileSync( + path.join(import.meta.dirname, "package-macos.mjs"), + "utf8", + ); + + assert.match(script, /run\("\/usr\/bin\/pkgbuild", packageArguments\)/); + assert.match(script, /productArguments\.push\(\s*"--sign",/); + assert.match(script, /productArguments\.push\("--keychain",/); + assert.match(script, /productArguments\.push\(packagePath\)/); + assert.match(script, /run\("\/usr\/bin\/productbuild", productArguments\)/); + assert.doesNotMatch(script, /packageArguments\.push\(\s*"--sign"/); +}); + +test("builds a branded installer with the original component identity and scripts", { + skip: process.platform !== "darwin", +}, (t) => { + const directory = mkdtempSync(path.join(tmpdir(), "agentdesktop product-")); + t.after(() => rmSync(directory, { recursive: true, force: true })); + const payload = path.join(directory, "payload"); + mkdirSync(payload); + writeFileSync(path.join(payload, "fixture.txt"), "installer fixture\n"); + + function run(command, arguments_) { + const result = spawnSync(command, arguments_, { encoding: "utf8" }); + assert.ifError(result.error); + assert.equal(result.status, 0, result.stderr); + } + + run("/usr/bin/pkgbuild", [ + "--root", + payload, + "--scripts", + path.join(macosDirectory, "scripts"), + "--identifier", + "dev.agentdesktop.installer", + "--version", + "9.8.7", + "--install-location", + "/", + path.join(directory, "agentdesktop-component.pkg"), + ]); + const product = path.join(directory, "agentdesktop.pkg"); + const resources = path.join(directory, "resources"); + cpSync(path.join(macosDirectory, "resources"), resources, { + recursive: true, + }); + const logo = path.join(nativeDirectory, "icons", "128x128.png"); + const welcomePath = path.join(resources, "welcome.html"); + writeFileSync( + welcomePath, + readFileSync(welcomePath, "utf8").replace( + "{{AGENTDESKTOP_LOGO}}", + readFileSync(logo).toString("base64"), + ), + ); + run("/usr/bin/productbuild", [ + "--distribution", + path.join(macosDirectory, "distribution.xml"), + "--resources", + resources, + "--package-path", + directory, + product, + ]); + const expanded = path.join(directory, "expanded"); + run("/usr/sbin/pkgutil", ["--expand-full", product, expanded]); + + const distribution = readFileSync( + path.join(expanded, "Distribution"), + "utf8", + ); + assert.match(distribution, /