Make mac release rebuilds incremental and drop debug builds

This commit is contained in:
Ian Gulliver
2026-10-17 09:33:27 -07:00
parent 7b172b9e3a
commit ff0595ec04
24 changed files with 53 additions and 19 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ These are requirements, not preferences. Changing one needs the owner's agreemen
- Never deploy unless the owner asks for it in the current turn. A finished change is reported undeployed; the owner deploys it, or asks you to. Never run `go install ./cmd/byre` or restart the service on your own initiative, not even to check that a change works.
- `docs/operations.md` describes the layout, data, service and APIs; keep it current with every change.
- Never add tests. byre has no test suite; check a change with `go vet ./...`, `go generate ./web` (which type-checks the UI) and, when `mac/` changed, `swift build --package-path mac`.
- Never add tests. byre has no test suite; check a change with `go vet ./...`, `go generate ./web` (which type-checks the UI) and, when `mac/` changed, `mac/bundle.sh` (release only; debug builds are refused).
- The web UI (`web/src/`) and the macOS app (`mac/Sources/Byre/`) are two clients of the same views. A change to what a client shows or does, such as Markdown rendering, goes into both in the same task; never leave one client behind.
## Deploying
+5 -3
View File
@@ -18,7 +18,9 @@ The first version copies the web client: the same two equal panes, tabs, labels,
- `UserNotifications` for notifications.
- Third-party code: Apple's `swift-openapi-generator` (with `swift-openapi-runtime` and `swift-openapi-urlsession`) for the API types and client, and SwiftTerm for the terminal.
The host has the Command Line Tools and no Xcode (`xcode-select --print-path` prints which), so the project is a Swift package under `mac/`, built with `swift build`, not an Xcode project. `mac/bundle.sh` builds the release configuration and assembles `mac/.build/byre.app` (the executable, `mac/Info.plist` with the fixed bundle identifier `run.fc.byre.mac`, the `web/fonts/` files and an `.icns` rendered from `web/favicon-tile.svg`) and signs it ad hoc; notification permission needs the bundle identifier. The app keeps its server origin in the `byre.server` default (`defaults read run.fc.byre.mac byre.server` prints it).
The project is a Swift package under `mac/`, built with `swift build`, not an Xcode project. It builds with Xcode's toolchain: `xcode-select --print-path` must print `/Applications/Xcode.app/Contents/Developer`. With the Command Line Tools selected while Xcode is installed, Swift Build finds two toolchains for the Command Line Tools path and picks one at random on each run; the pick is part of the build request, so every change of pick rebuilds every target. `mac/bundle.sh` builds the release configuration and assembles `mac/.build/byre.app` (the executable, `mac/Info.plist` with the fixed bundle identifier `run.fc.byre.mac`, the `web/fonts/` files and an `.icns` rendered from `web/favicon-tile.svg`, cached as `mac/.build/byre.icns` until the SVG changes) and signs it ad hoc; notification permission needs the bundle identifier.
Only the release configuration is built. A debug build stops at an `#error` in `ByreApp.swift`, and even a failed one leaves the next release build recompiling every target, since the two configurations share `mac/.build`. The generated API client is its own target, `ByreAPI`, so an edit to the app recompiles only the `Byre` target. SwiftPM builds release targets whole-module, so that is one compile of the whole `Byre` target, and a one-file change still costs that. The app keeps its server origin in the `byre.server` default (`defaults read run.fc.byre.mac byre.server` prints it).
SwiftUI's own macros don't build here: the SDK declares `@State`, `@Entry`, `@Animatable` and the rest as macros implemented in the `SwiftUIMacros` plugin, which ships with Xcode and not with the Command Line Tools (`ls /Library/Developer/CommandLineTools/usr/lib/swift/host/plugins` lists the plugins there). `@State` is reached through its property-wrapper type instead, as `typealias ViewState = State` and `@ViewState var …`; other SwiftUI macros aren't used. `@Observable` and `@Bindable` (Observation's plugin, which the Command Line Tools have), `@Environment` and `@FocusState` work as usual.
@@ -28,7 +30,7 @@ Values the web client defines are generated into Swift, never copied by hand, so
| Value | Source | Swift |
| --- | --- | --- |
| API types and client | `api/openapi.yaml` | Generated at build time by the `swift-openapi-generator` plugin (`mac/Sources/Byre/openapi-generator-config.yaml`, idiomatic naming: `display_name` is `displayName`) from the symlink `mac/Sources/Byre/openapi.yaml` to the spec. The output lands in `mac/.build/plugins/outputs/`, not in git |
| API types and client | `api/openapi.yaml` | Generated at build time into the `ByreAPI` target by the `swift-openapi-generator` plugin (`mac/Sources/ByreAPI/openapi-generator-config.yaml`, public access, idiomatic naming: `display_name` is `displayName`) from the symlink `mac/Sources/ByreAPI/openapi.yaml` to the spec. App files that use it `import ByreAPI`. The output lands in `mac/.build/plugins/outputs/`, not in git |
| Palette, light and dark | Custom properties at the top of `web/style.css` (`:root` and its `prefers-color-scheme: dark` block) | `go generate ./mac` (`mac/palette`) writes `Palette.swift`: one dynamic `Color` per hex property, `--accent-tint` as `Palette.accentTint`, resolving light or dark with the window's appearance. Properties that aren't a hex colour (`--task`, `--shadow`, `--font`) are left out. The file is committed, so `swift build` needs no Go; a property missing from either block fails the generator |
| Font | `web/fonts/` | The build script copies the woff2 files into the bundle; the app loads each with `CTFontManagerCreateFontDescriptorsFromURL` (see "Gaps in SwiftUI") |
| App icon | `web/favicon-tile.svg` | The build script renders it to an `.icns` with `rsvg-convert` and `iconutil` |
@@ -188,7 +190,7 @@ Platform differences the copy can't avoid, each with what the web client does an
## Building and checking
`swift build --package-path mac` is the compile check for changes under `mac/`, alongside the checks `AGENTS.md` already names. No tests are added. Installing the app is a separate action from deploying the byre service.
`mac/bundle.sh` is the compile check for changes under `mac/`, alongside the checks `AGENTS.md` already names. No tests are added. Installing the app is a separate action from deploying the byre service.
## Not in version one
+11 -4
View File
@@ -11,14 +11,21 @@ let package = Package(
.package(url: "https://github.com/apple/swift-openapi-urlsession", from: "1.1.0"),
],
targets: [
.executableTarget(
name: "Byre",
.target(
name: "ByreAPI",
dependencies: [
.product(name: "SwiftTerm", package: "SwiftTerm"),
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
],
plugins: [.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator")]
),
.executableTarget(
name: "Byre",
dependencies: [
"ByreAPI",
.product(name: "SwiftTerm", package: "SwiftTerm"),
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
]
),
]
)
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import Foundation
import HTTPTypes
import OpenAPIRuntime
+4
View File
@@ -1,6 +1,10 @@
import AppKit
import SwiftUI
#if DEBUG
#error("debug builds are unsupported: build with mac/bundle.sh")
#endif
typealias ViewState = State
@MainActor
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import SwiftUI
typealias TaskChanges = Components.Schemas.TaskChanges
+1
View File
@@ -1,4 +1,5 @@
import AppKit
import ByreAPI
import SwiftUI
enum ChatKind: Equatable {
+1
View File
@@ -1,4 +1,5 @@
import AppKit
import ByreAPI
import OpenAPIRuntime
import SwiftUI
import UniformTypeIdentifiers
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import SwiftUI
typealias DirListing = Components.Schemas.DirListing
+1
View File
@@ -1,4 +1,5 @@
import AppKit
import ByreAPI
import OpenAPIRuntime
import SwiftUI
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import OpenAPIRuntime
import SwiftUI
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import SwiftUI
typealias Participant = Components.Schemas.Participant
+1
View File
@@ -1,4 +1,5 @@
import AppKit
import ByreAPI
import SwiftUI
@MainActor
+1
View File
@@ -1,4 +1,5 @@
import AppKit
import ByreAPI
import SwiftUI
enum TabBadge {
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import SwiftUI
typealias Todo = Components.Schemas.Todo
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import SwiftUI
typealias FindingState = Components.Schemas.FindingState
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import SwiftUI
import UserNotifications
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import SwiftUI
typealias AgentProcess = Components.Schemas.AgentProcess
+1
View File
@@ -1,4 +1,5 @@
import AppKit
import ByreAPI
import SwiftUI
typealias WorkflowSummary = Components.Schemas.WorkflowSummary
+1
View File
@@ -1,3 +1,4 @@
import ByreAPI
import SwiftUI
typealias WorkflowRun = Components.Schemas.WorkflowRun
+1
View File
@@ -0,0 +1 @@
import OpenAPIRuntime
@@ -1,5 +1,5 @@
generate:
- types
- client
accessModifier: internal
accessModifier: public
namingStrategy: idiomatic
+14 -10
View File
@@ -4,20 +4,24 @@ mac=$(cd "$(dirname "$0")" && pwd)
root=$(dirname "$mac")
build=$mac/.build
app=$build/byre.app
svg=$root/web/favicon-tile.svg
icns=$build/byre.icns
swift build --package-path "$mac" --configuration release
bin=$(swift build --package-path "$mac" --configuration release --show-bin-path)
if [ ! "$icns" -nt "$svg" ]; then
iconset=$build/byre.iconset
rm -rf "$iconset"
mkdir -p "$iconset"
for size in 16 32 128 256 512; do
rsvg-convert --width $size --height $size --output "$iconset/icon_${size}x${size}.png" "$svg"
rsvg-convert --width $((size * 2)) --height $((size * 2)) --output "$iconset/icon_${size}x${size}@2x.png" "$svg"
done
iconutil --convert icns --output "$icns" "$iconset"
fi
rm -rf "$app"
mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources/fonts"
cp "$bin/Byre" "$app/Contents/MacOS/Byre"
cp "$build/release/Byre" "$app/Contents/MacOS/Byre"
cp "$mac/Info.plist" "$app/Contents/Info.plist"
cp "$root"/web/fonts/*.woff2 "$app/Contents/Resources/fonts/"
iconset=$build/byre.iconset
rm -rf "$iconset"
mkdir -p "$iconset"
for size in 16 32 128 256 512; do
rsvg-convert --width $size --height $size --output "$iconset/icon_${size}x${size}.png" "$root/web/favicon-tile.svg"
rsvg-convert --width $((size * 2)) --height $((size * 2)) --output "$iconset/icon_${size}x${size}@2x.png" "$root/web/favicon-tile.svg"
done
iconutil --convert icns --output "$app/Contents/Resources/byre.icns" "$iconset"
cp "$icns" "$app/Contents/Resources/byre.icns"
codesign --force --sign - "$app"
echo "$app"