Add signed, notarized releases with the server inside the app

This commit is contained in:
Ian Gulliver
2026-10-18 15:19:57 -07:00
parent 10a80ccb22
commit 6b86a7d5a9
20 changed files with 388 additions and 66 deletions
+6
View File
@@ -16,6 +16,12 @@
<string>27.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>SUFeedURL</key>
<string>https://g.fc.run/misc/byre/releases/download/latest/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>350+qbxFCpm+LhbRlCxtBHST+FTz6o19jhGOSs8D0XU=</string>
<key>SUEnableAutomaticChecks</key>
<true/>
<key>NSAppleEventsUsageDescription</key>
<string>byre's agents drive Orion on this Mac through AppleScript when a task asks them to.</string>
</dict>
+10 -1
View File
@@ -1,5 +1,5 @@
{
"originHash" : "c8eddc113e1728df643cb0e886195a9376637e4c97c431bbf2201b07be34a7c6",
"originHash" : "1e64f8e620bfd60399ac1276949a869cf223612c3875be1481772d36a8a2181d",
"pins" : [
{
"identity" : "h",
@@ -19,6 +19,15 @@
"version" : "6.4.0"
}
},
{
"identity" : "sparkle",
"kind" : "remoteSourceControl",
"location" : "https://github.com/sparkle-project/Sparkle",
"state" : {
"revision" : "eef1a539a373c1f1a320624b1130fc5de7b2e100",
"version" : "2.10.0"
}
},
{
"identity" : "swift-algorithms",
"kind" : "remoteSourceControl",
+4 -1
View File
@@ -9,6 +9,7 @@ let package = Package(
.package(url: "https://github.com/apple/swift-openapi-generator", from: "1.10.0"),
.package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.8.0"),
.package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.1.0"),
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.10.0"),
],
targets: [
.target(
@@ -26,8 +27,10 @@ let package = Package(
.product(name: "SwiftTerm", package: "SwiftTerm"),
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
.product(name: "Sparkle", package: "Sparkle"),
],
swiftSettings: [.strictMemorySafety()]
swiftSettings: [.strictMemorySafety()],
linkerSettings: [.unsafeFlags(["-Xlinker", "-rpath", "-Xlinker", "@executable_path/../Frameworks"])]
),
]
)
+4
View File
@@ -14,6 +14,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
Notifier.shared.install()
Updater.shared.start()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
@@ -64,6 +65,9 @@ struct ByreApp: App {
.defaultSize(width: 1280, height: 820)
.commands {
CommandGroup(after: .appInfo) {
Button("Check for Updates…") {
Updater.shared.check()
}
Button("Change Server…") {
connection.changeServer()
}
+1 -1
View File
@@ -371,7 +371,7 @@ private actor Hands {
let stdin = Pipe()
let stdout = Pipe()
let p = Process()
p.executableURL = Bundle.main.executableURL!.deletingLastPathComponent().appending(path: "byre-hands")
p.executableURL = Bundle.main.bundleURL.appending(path: "Contents/Helpers/byre-hands")
p.standardInput = stdin
p.standardOutput = stdout
p.standardError = FileHandle.nullDevice
+53
View File
@@ -0,0 +1,53 @@
import Foundation
import Sparkle
private struct LaunchAgent: Decodable {
let programArguments: [String]
enum CodingKeys: String, CodingKey {
case programArguments = "ProgramArguments"
}
}
private let lastVersionKey = "lastLaunchedVersion"
@MainActor
final class Updater {
static let shared = Updater()
private lazy var controller = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
func start() {
let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
let last = UserDefaults.standard.string(forKey: lastVersionKey)
UserDefaults.standard.set(version, forKey: lastVersionKey)
if last != nil && last != version {
restartServer()
}
_ = controller
}
func check() {
controller.checkForUpdates(nil)
}
private func restartServer() {
let agent = FileManager.default.homeDirectoryForCurrentUser.appending(path: "Library/LaunchAgents/run.fc.byre.plist")
let server = Bundle.main.bundleURL.appending(path: "Contents/Helpers/byre").path
guard let data = try? Data(contentsOf: agent),
let plist = try? PropertyListDecoder().decode(LaunchAgent.self, from: data),
plist.programArguments.first == server
else {
return
}
let p = Process()
p.executableURL = URL(filePath: "/bin/launchctl")
p.arguments = ["kickstart", "-k", "gui/\(getuid())/run.fc.byre"]
do {
try p.run()
p.waitUntilExit()
log.info("restarted byre server: status \(p.terminationStatus)")
} catch {
log.error("restart byre server: \(errorText(error), privacy: .public)")
}
}
}
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.automation.apple-events</key>
<true/>
</dict>
</plist>
+27 -6
View File
@@ -7,14 +7,22 @@ build=$mac/.build
app=$build/byre.app
svg=$mac/icon.svg
icns=$build/byre.icns
identity="Developer ID Application"
mkdir -p "$shared" "$build"
if [ "$(security cms -D -i "$mac/byre.provisionprofile" | plutil -extract ProvisionsAllDevices raw -)" != true ]; then
echo "bundle: mac/byre.provisionprofile is not a Developer ID profile" >&2
exit 1
fi
lockf -k "$shared.lock" sh -ec '
rsync -rlc --delete --exclude /mac/.build --exclude /hands/.build "$1/mac" "$1/api" "$1/hands" "$2/"
swift build --package-path "$2/mac" --configuration release --no-color-diagnostics
swift build --package-path "$2/hands" --configuration release --no-color-diagnostics
cp "$2/mac/.build/release/Byre" "$3"
cp "$2/hands/.build/release/byre-hands" "$4"
' sh "$root" "$shared" "$build/Byre" "$build/byre-hands"
rm -rf "$5"
ditto "$2/mac/.build/release/Sparkle.framework" "$5"
' sh "$root" "$shared" "$build/Byre" "$build/byre-hands" "$build/Sparkle.framework"
go -C "$root" build -trimpath -o "$build/server/byre" ./cmd/byre
if [ ! "$icns" -nt "$svg" ]; then
iconset=$build/byre.iconset
rm -rf "$iconset"
@@ -25,17 +33,30 @@ if [ ! "$icns" -nt "$svg" ]; then
done
iconutil --convert icns --output "$icns" "$iconset"
fi
version=$(git -C "$root" describe --tags --always)
version=${version#v}
rm -rf "$app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources/fonts"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Helpers" "$app/Contents/Frameworks" "$app/Contents/Resources/fonts"
cp "$build/Byre" "$app/Contents/MacOS/Byre"
cp "$build/byre-hands" "$app/Contents/MacOS/byre-hands"
codesign --force --sign "Apple Development" --identifier run.fc.byre.hands "$app/Contents/MacOS/byre-hands"
cp "$build/server/byre" "$app/Contents/Helpers/byre"
cp "$build/byre-hands" "$app/Contents/Helpers/byre-hands"
ditto "$build/Sparkle.framework" "$app/Contents/Frameworks/Sparkle.framework"
cp "$mac/Info.plist" "$app/Contents/Info.plist"
plutil -insert ByreCommit -string "$(git -C "$root" rev-parse HEAD)" "$app/Contents/Info.plist"
plutil -insert CFBundleShortVersionString -string "$(git -C "$root" rev-parse --short HEAD)" "$app/Contents/Info.plist"
plutil -insert CFBundleShortVersionString -string "$version" "$app/Contents/Info.plist"
plutil -insert CFBundleVersion -string "$(git -C "$root" rev-list --count HEAD)" "$app/Contents/Info.plist"
cp "$mac"/fonts/*.woff2 "$app/Contents/Resources/fonts/"
cp "$icns" "$app/Contents/Resources/byre.icns"
cp "$mac/byre.provisionprofile" "$app/Contents/embedded.provisionprofile"
codesign --force --sign "Apple Development" --entitlements "$mac/byre.entitlements" "$app"
sparkle=$app/Contents/Frameworks/Sparkle.framework
rm -rf "$sparkle/Versions/B/XPCServices"
sign() {
codesign --force --timestamp --options runtime --sign "$identity" "$@"
}
sign --identifier run.fc.byre.hands --entitlements "$mac/automation.entitlements" "$app/Contents/Helpers/byre-hands"
sign --identifier run.fc.byre --entitlements "$mac/automation.entitlements" "$app/Contents/Helpers/byre"
sign "$sparkle/Versions/B/Autoupdate"
sign "$sparkle/Versions/B/Updater.app"
sign "$sparkle"
sign --entitlements "$mac/byre.entitlements" "$app"
echo "$app"
+5 -1
View File
@@ -14,7 +14,11 @@
<array>
<string>CloudKit</string>
</array>
<key>com.apple.developer.icloud-container-environment</key>
<string>Production</string>
<key>com.apple.developer.aps-environment</key>
<string>development</string>
<string>production</string>
<key>com.apple.security.automation.apple-events</key>
<true/>
</dict>
</plist>
Binary file not shown.
Executable
+90
View File
@@ -0,0 +1,90 @@
#!/bin/sh
set -eu
mac=$(cd "$(dirname "$0")" && pwd)
root=$(dirname "$mac")
repo=misc/byre
api=https://g.fc.run/api/v1/repos/$repo
sparkle_version=2.10.0
sparkle=$HOME/Library/Caches/byre/sparkle-$sparkle_version
identity="Developer ID Application"
if [ -n "$(git -C "$root" status --porcelain)" ]; then
echo "release: the tree has uncommitted changes" >&2
exit 1
fi
tag=$(git -C "$root" describe --tags --exact-match HEAD)
version=${tag#v}
git -C "$root" fetch origin "refs/tags/$tag"
if [ "$(git -C "$root" rev-parse "FETCH_HEAD^{commit}")" != "$(git -C "$root" rev-parse HEAD)" ]; then
echo "release: $tag on origin is not HEAD" >&2
exit 1
fi
token=$(security find-generic-password -s byre-release -w)
if [ ! -x "$sparkle/bin/sign_update" ]; then
mkdir -p "$sparkle"
curl -fsSL "https://github.com/sparkle-project/Sparkle/releases/download/$sparkle_version/Sparkle-$sparkle_version.tar.xz" | tar -xJ -C "$sparkle"
fi
"$mac/bundle.sh"
app=$mac/.build/byre.app
out=$mac/.build/release/$version
rm -rf "$out"
mkdir -p "$out"
ditto -c -k --keepParent "$app" "$out/byre.zip"
xcrun notarytool submit "$out/byre.zip" --keychain-profile byre-notary --wait
xcrun stapler staple "$app"
rm "$out/byre.zip"
dmg=byre-$version.dmg
stage=$out/stage
mkdir "$stage"
ditto "$app" "$stage/byre.app"
ln -s /Applications "$stage/Applications"
hdiutil create -volname byre -srcfolder "$stage" -format UDZO "$out/$dmg"
rm -rf "$stage"
codesign --force --timestamp --sign "$identity" "$out/$dmg"
xcrun notarytool submit "$out/$dmg" --keychain-profile byre-notary --wait
xcrun stapler staple "$out/$dmg"
(cd "$out" && shasum -a 256 "$dmg" > "$dmg.sha256")
build=$(plutil -extract CFBundleVersion raw "$app/Contents/Info.plist")
minimum=$(plutil -extract LSMinimumSystemVersion raw "$app/Contents/Info.plist")
signature=$("$sparkle/bin/sign_update" --account byre "$out/$dmg")
cat > "$out/appcast.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>byre</title>
<item>
<title>byre $version</title>
<pubDate>$(LC_ALL=C date -u "+%a, %d %b %Y %H:%M:%S +0000")</pubDate>
<sparkle:version>$build</sparkle:version>
<sparkle:shortVersionString>$version</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>$minimum</sparkle:minimumSystemVersion>
<enclosure url="https://g.fc.run/$repo/releases/download/$tag/$dmg" type="application/octet-stream" $signature/>
</item>
</channel>
</rss>
EOF
previous=$(git -C "$root" describe --tags --abbrev=0 "$tag^" || true)
if [ -n "$previous" ]; then
notes=$(git -C "$root" log --format='- %s' "$previous..$tag")
else
notes="First release."
fi
request=$out/release.json
plutil -create xml1 "$request"
plutil -insert tag_name -string "$tag" "$request"
plutil -insert name -string "byre $version" "$request"
plutil -insert body -string "$notes" "$request"
plutil -insert draft -bool true "$request"
plutil -convert json "$request"
id=$(curl -fsS -X POST -H "Authorization: token $token" -H "Content-Type: application/json" --data-binary "@$request" "$api/releases" | plutil -extract id raw -)
for file in "$dmg" "$dmg.sha256" appcast.xml; do
curl -fsS -o /dev/null -H "Authorization: token $token" -F "attachment=@$out/$file" "$api/releases/$id/assets?name=$file"
done
curl -fsS -o /dev/null -X PATCH -H "Authorization: token $token" -H "Content-Type: application/json" --data '{"draft":false}' "$api/releases/$id"
echo "https://g.fc.run/$repo/releases/tag/$tag"