mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-09 22:25:02 +03:00
feat(desktop): add a custom macOS DMG background
The build job packs the disk image layout (background, icon positions and volume icon) around an empty app folder, and sign-desktop-macos.sh puts the stapled app in its place instead of making a plain hdiutil image.
This commit is contained in:
@@ -1,15 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# sign-desktop-macos.sh <out dir> <app tarball>...: signs each unsigned .app inside-out with hardened runtime,
|
||||
# notarizes them in one submission, staples them, and only then builds opencode-desktop-mac-<arch>.{zip,dmg,app.tar.gz}
|
||||
# from the stapled apps. Dry runs sign ad-hoc and skip notarization. Dev and beta trade larger files for faster
|
||||
# packaging (zip and dmg at zlib level 1) and skip the .app.tar.gz, which only the retired Tauri updater feed read.
|
||||
# from the stapled apps, putting each app into the disk image layout packed next to its tarball. Dry runs sign ad-hoc
|
||||
# and skip notarization. Dev and beta trade larger files for faster packaging (zip and dmg at zlib level 1) and skip
|
||||
# the .app.tar.gz, which only the retired Tauri updater feed read.
|
||||
# Notarization reads APPLE_NOTARY_KEY (the .p8 contents), APPLE_NOTARY_KEY_ID and APPLE_NOTARY_ISSUER_ID.
|
||||
. "$(dirname "$0")/lib.sh"
|
||||
|
||||
out=$1
|
||||
shift
|
||||
mkdir -p "$out"
|
||||
apps=$(mktemp -d)
|
||||
apps=$(mktemp -d) layouts=$(mktemp -d)
|
||||
identity=- timestamp=--timestamp=none
|
||||
zip_level=() dmg_level=() preview=false
|
||||
case "$OPENCODE_CHANNEL" in dev | beta) preview=true ;; esac
|
||||
@@ -36,6 +37,7 @@ for tarball in "$@"; do
|
||||
die "Expected exactly one .app bundle in $tarball"
|
||||
fi
|
||||
app=$(bundle "$arch")
|
||||
cp "${tarball%.app.tar}.layout.dmg" "$layouts/$arch.dmg"
|
||||
|
||||
# Every nested Mach-O file and bundle, deepest first, then the app itself.
|
||||
list=$(mktemp)
|
||||
@@ -79,10 +81,20 @@ for dir in "$apps"/*; do
|
||||
spctl --assess --type execute --verbose=2 "$app"
|
||||
fi
|
||||
ditto -c -k ${zip_level[@]+"${zip_level[@]}"} --sequesterRsrc --keepParent "$app" "$out/opencode-desktop-mac-$arch.zip"
|
||||
staging=$(mktemp -d)
|
||||
ditto "$app" "$staging/$name"
|
||||
ln -s /Applications "$staging/Applications"
|
||||
hdiutil create -volname "${name%.app}" -srcfolder "$staging" -fs HFS+ -format UDZO ${dmg_level[@]+"${dmg_level[@]}"} -ov "$out/opencode-desktop-mac-$arch.dmg"
|
||||
# The layout holds the background, icon positions and volume icon around an empty app folder. Only that folder
|
||||
# and the Applications link may be visible, so the image ships nothing but the stapled app beside the link.
|
||||
hdiutil convert "$layouts/$arch.dmg" -format UDRW -o "$layouts/$arch-rw.dmg"
|
||||
volume=$(mktemp -d)
|
||||
hdiutil attach "$layouts/$arch-rw.dmg" -nobrowse -noautoopen -mountpoint "$volume"
|
||||
if [ "$(ls "$volume")" != "$(printf 'Applications\n%s' "$name")" ] || [ "$(readlink "$volume/Applications")" != /Applications ] ||
|
||||
[ -n "$(ls -A "$volume/$name")" ]; then
|
||||
die "Unexpected disk image layout: $(ls -A "$volume")"
|
||||
fi
|
||||
rmdir "$volume/$name"
|
||||
ditto "$app" "$volume/$name"
|
||||
hdiutil detach "$volume"
|
||||
hdiutil resize -sectors min "$layouts/$arch-rw.dmg"
|
||||
hdiutil convert "$layouts/$arch-rw.dmg" -format UDZO ${dmg_level[@]+"${dmg_level[@]}"} -ov -o "$out/opencode-desktop-mac-$arch.dmg"
|
||||
if [ "$preview" = true ]; then continue; fi
|
||||
COPYFILE_DISABLE=1 tar --no-mac-metadata -czf "$out/opencode-desktop-mac-$arch.app.tar.gz" -C "$dir" "$name"
|
||||
done
|
||||
|
||||
@@ -88,12 +88,24 @@ test("legacy Linux launcher stays hidden", async () => {
|
||||
expect(desktop).toContain("NoDisplay=true")
|
||||
})
|
||||
|
||||
test.each(channels)("macOS bundle for $channel", ({ channel }) => {
|
||||
test.each(channels)("macOS bundle for $channel", async ({ channel }) => {
|
||||
const config = load(channel)
|
||||
expect(config.mac?.icon).toBe(channel === "prod" ? "resources/icons/icon.icon" : "resources/icons/icon.icns")
|
||||
expect(config.dmg?.icon).toBe(channel === "prod" ? "resources/icons/icon.icns" : undefined)
|
||||
// The prod override must keep the rest of the shared macOS settings.
|
||||
// The prod overrides must keep the rest of the shared macOS settings and disk image layout.
|
||||
expect(config.mac?.target).toEqual(["dmg", "zip"])
|
||||
expect(config.dmg).toMatchObject({
|
||||
title: "${productName}",
|
||||
background: "icons/dmg/background.png",
|
||||
contents: [
|
||||
{ x: 180, y: 210, type: "file" },
|
||||
{ x: 480, y: 210, type: "link", path: "/Applications" },
|
||||
],
|
||||
})
|
||||
|
||||
// electron-builder combines the background with its @2x image only when both exist; otherwise Retina blurs it.
|
||||
for (const file of ["icons/dmg/background.png", "icons/dmg/background@2x.png"])
|
||||
expect(await Bun.file(path.join(import.meta.dirname, file)).exists(), file).toBe(true)
|
||||
|
||||
for (const key of [
|
||||
"NSMicrophoneUsageDescription",
|
||||
|
||||
@@ -172,6 +172,19 @@ const getBase = (appId: string): Configuration => {
|
||||
},
|
||||
target: ["dmg", "zip"],
|
||||
},
|
||||
// script/pack.ts builds this layout around an empty app folder, and sign-desktop-macos.sh puts the notarized app
|
||||
// in its place. Finder draws the icons and their labels, always black, over the background, so it stays light.
|
||||
dmg: {
|
||||
title: "${productName}",
|
||||
background: "icons/dmg/background.png",
|
||||
iconSize: 128,
|
||||
contents: [
|
||||
{ x: 180, y: 210, type: "file" },
|
||||
{ x: 480, y: 210, type: "link", path: "/Applications" },
|
||||
],
|
||||
// publish-desktop.sh computes the update feeds from the final files, so no blockmap.
|
||||
writeUpdateInfo: false,
|
||||
},
|
||||
protocols: {
|
||||
name: "OpenCode",
|
||||
schemes: ["opencode"],
|
||||
@@ -261,7 +274,7 @@ export function getConfig() {
|
||||
// (Dock, Finder, system dialogs and the system icon styles). Older macOS keeps using the ICNS.
|
||||
mac: { ...base.mac, icon: "resources/icons/icon.icon" },
|
||||
// The DMG volume icon would otherwise use the same actool-generated ICNS.
|
||||
dmg: { icon: "resources/icons/icon.icns" },
|
||||
dmg: { ...base.dmg, icon: "resources/icons/icon.icns" },
|
||||
publish: {
|
||||
provider: "generic",
|
||||
url: "https://opencode.ai/update/api/latest/desktop/opencode/",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -6,7 +6,7 @@ import { getConfig } from "../electron-builder.config"
|
||||
import { getCurrentCli, resolveChannel } from "./utils"
|
||||
|
||||
// Packages the built app (out/) for OPENCODE_TARGET into dist/desktop at the repository root, unsigned:
|
||||
// macOS: the .app as a tarball. Windows: the unpacked app in app/, or with OPENCODE_DESKTOP_APP, the NSIS
|
||||
// macOS: the .app as a tarball and the disk image layout. Windows: the unpacked app in app/, or with OPENCODE_DESKTOP_APP, the NSIS
|
||||
// installer around that (signed) app. Linux: AppImage, deb and rpm. The release workflow signs afterwards.
|
||||
const dir = path.resolve(import.meta.dir, "..")
|
||||
|
||||
@@ -53,6 +53,15 @@ if (!app && os === "mac") {
|
||||
if (bundles.length !== 1) throw new Error(`Expected one .app bundle in ${parent}, found ${bundles.join(", ")}`)
|
||||
await writeUpdateConfig(path.join(parent, bundles[0], "Contents", "Resources"))
|
||||
await $`tar -cf ${path.join(out, `opencode-desktop-mac-${cli.cpu}.app.tar`)} -C ${parent} ${bundles[0]}`
|
||||
// The disk image layout (background, icon positions and volume icon) around an empty app folder, with room
|
||||
// for sign-desktop-macos.sh to put the notarized app in its place.
|
||||
const placeholder = path.join("dist", "dmg", bundles[0])
|
||||
await mkdir(placeholder, { recursive: true })
|
||||
await $`bunx electron-builder ${flags} -c.mac.target=dmg -c.dmg.size=2g -c.dmg.shrink=false --prepackaged ${placeholder}`
|
||||
await cp(
|
||||
path.join("dist", `opencode-desktop-mac-${cli.cpu}.dmg`),
|
||||
path.join(out, `opencode-desktop-mac-${cli.cpu}.layout.dmg`),
|
||||
)
|
||||
}
|
||||
|
||||
if (!app && os === "win") {
|
||||
|
||||
Reference in New Issue
Block a user