Files
nucleic/scripts/package-app.sh
T
NucleicandClaude Opus 4.8 8bad615fa8 Container: auto-download the kernel (zero user setup)
The kernel was the last bundled asset (needing scripts/fetch-kernel.sh +
codesign-time bundling). Now it's acquired automatically on first use like the
vminitd initfs and the sandbox image — nothing is bundled and the user runs no
setup step.

- ContainerEngine.ensureKernel: resolve order = NUCLEIC_KERNEL_PATH → on-disk
  cache → bundled Resources (optional) → reuse Apple `container`'s installed
  kernel (no download) → download the kernel release asset. The asset is hosted
  on a GitHub release (ProjectSandbox.kernelRelease*) and fetched with the app's
  GitHub token when the repo is private (same token as the sandbox image).
  NUCLEIC_KERNEL_NO_REUSE forces the download (testing / incompatible local kernel).
- isSupported/unsupportedReason now gate only on Apple silicon; the kernel is no
  longer a precondition. Settings copy + package-app.sh + BUILD.md updated;
  bundling is an optional offline fast-path.
- registryAuth: scope credentials to the sandbox image's registry host, so a
  configured token is never attached to a different registry (e.g. docker.io
  public bases, which 401 on unrelated Basic creds). Fixes anonymous base pulls.
- Verified end-to-end on macOS 27 / Apple silicon: forced kernel download +
  anonymous docker.io base pull + boot + exec + stdout. Builds clean; 21 tests pass.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-21 02:37:14 -07:00

171 lines
6.8 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# Assemble a macOS .app bundle for a Nucleic build channel.
#
# scripts/package-app.sh {dev|beta|rc|stable}
#
# Wraps the channel's bare SwiftPM executable in a proper .app: writes Info.plist,
# ships the SwiftPM resource bundles (GRDB / SwiftTerm) inside Contents/Resources,
# and ad-hoc code-signs so it launches locally. The channels build to distinct
# app names + bundle IDs, so they coexist:
#
# channel product (process) app bundle bundle id
# dev nucleic-local Nucleic Dev.app com.abkslm.nucleic.dev
# beta nucleic-beta Nucleic Beta.app com.abkslm.nucleic.beta
# rc nucleic-rc Nucleic RC.app com.abkslm.nucleic.rc
# stable nucleic Nucleic.app com.abkslm.nucleic
#
# Env overrides:
# NUCLEIC_CONFIG build configuration (default: release)
# NUCLEIC_VERSION marketing version / CFBundleShortVersionString (default: 0.1.0)
# NUCLEIC_SIGN_ID codesign identity (default: "-", ad-hoc). Set to a Developer ID
# to sign for distribution (notarization/TestFlight is separate).
#
# An app icon is picked up automatically if present at Resources/AppIcon.icns
# (or per-channel Resources/AppIcon-<channel>.icns).
set -euo pipefail
CHANNEL="${1:-dev}"
CONFIG="${NUCLEIC_CONFIG:-release}"
MARKETING_VERSION="${NUCLEIC_VERSION:-0.1.0}"
SIGN_ID="${NUCLEIC_SIGN_ID:--}"
case "$CHANNEL" in
dev) PRODUCT=nucleic-local; APP_NAME="Nucleic Dev"; BUNDLE_ID="com.abkslm.nucleic.dev" ;;
beta) PRODUCT=nucleic-beta; APP_NAME="Nucleic Beta"; BUNDLE_ID="com.abkslm.nucleic.beta" ;;
rc) PRODUCT=nucleic-rc; APP_NAME="Nucleic RC"; BUNDLE_ID="com.abkslm.nucleic.rc" ;;
stable) PRODUCT=nucleic; APP_NAME="Nucleic"; BUNDLE_ID="com.abkslm.nucleic" ;;
*) echo "usage: $0 {dev|beta|rc|stable}" >&2; exit 2 ;;
esac
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# iCloud sync drops "conflict copies" beside source files — "Foo 2.swift", "Foo 3.swift"
# — that are byte-identical to the original. SwiftPM globs the whole Sources/Tests tree,
# so each copy compiles as a second definition and the build dies with "invalid
# redeclaration". Sweep them before building. We only touch our own source trees, and
# only files whose name is "<base> <n>.swift" with an existing "<base>.swift" alongside,
# so a deliberately-named file is never removed.
clean_icloud_dupes() {
local dupe base removed=0
while IFS= read -r -d '' dupe; do
base="${dupe% [0-9].swift}.swift"
[ -f "$base" ] || continue
rm -f "$dupe"
echo " removed iCloud conflict copy: ${dupe#"$ROOT/"}"
removed=$((removed + 1))
done < <(find "$ROOT/Sources" "$ROOT/Tests" -type f -name '* [0-9].swift' -print0 2>/dev/null)
[ "$removed" -gt 0 ] && echo "▸ Cleaned $removed iCloud conflict cop$([ "$removed" -eq 1 ] && echo y || echo ies) before build"
return 0
}
clean_icloud_dupes
echo "▸ Building $PRODUCT (channel=$CHANNEL, config=$CONFIG)"
NUCLEIC_CHANNEL="$CHANNEL" swift build -c "$CONFIG" --build-system native --product "$PRODUCT"
BIN_DIR="$(NUCLEIC_CHANNEL="$CHANNEL" swift build -c "$CONFIG" --build-system native --product "$PRODUCT" --show-bin-path)"
BUILD_NUMBER="$(git rev-list --count HEAD 2>/dev/null || echo 1)"
COMMIT="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
APP="$ROOT/dist/$APP_NAME.app"
CONTENTS="$APP/Contents"
echo "▸ Assembling $APP"
rm -rf "$APP"
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources"
# Executable — keep the channel's product name so the OS process name is preserved
# even when launched from the bundle.
cp "$BIN_DIR/$PRODUCT" "$CONTENTS/MacOS/$PRODUCT"
# SwiftPM resource bundles must ship inside the app — `Bundle.module` resolves them
# via Bundle.main.resourceURL (= Contents/Resources).
shopt -s nullglob
for b in "$BIN_DIR"/*.bundle; do
cp -R "$b" "$CONTENTS/Resources/"
done
shopt -u nullglob
# Container runtime: NOTHING is required to be bundled. The kernel, the vminitd initfs, and the
# sandbox image all download + cache automatically on first use. Optionally, a kernel staged at
# Resources/vmlinux-arm64 (scripts/fetch-kernel.sh) is bundled here as an offline/dev fast-path so
# the app skips that one download.
if [ -f "$ROOT/Resources/vmlinux-arm64" ]; then
cp "$ROOT/Resources/vmlinux-arm64" "$CONTENTS/Resources/vmlinux-arm64"
echo " • bundled Resources/vmlinux-arm64 (kernel fast-path)"
else
echo " • kernel not bundled — it downloads automatically at runtime (optional: scripts/fetch-kernel.sh)"
fi
# Optional icon.
ICON_PLIST=""
ICON_SRC=""
[ -f "$ROOT/Resources/AppIcon-$CHANNEL.icns" ] && ICON_SRC="$ROOT/Resources/AppIcon-$CHANNEL.icns"
[ -z "$ICON_SRC" ] && [ -f "$ROOT/Resources/AppIcon.icns" ] && ICON_SRC="$ROOT/Resources/AppIcon.icns"
if [ -n "$ICON_SRC" ]; then
cp "$ICON_SRC" "$CONTENTS/Resources/AppIcon.icns"
ICON_PLIST="
<key>CFBundleIconFile</key>
<string>AppIcon</string>"
fi
cat > "$CONTENTS/Info.plist" <<PLIST
<?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>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$PRODUCT</string>
<key>CFBundleIdentifier</key>
<string>$BUNDLE_ID</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$APP_NAME</string>
<key>CFBundleDisplayName</key>
<string>$APP_NAME</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$MARKETING_VERSION</string>
<key>CFBundleVersion</key>
<string>$BUILD_NUMBER</string>$ICON_PLIST
<key>LSMinimumSystemVersion</key>
<string>26.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>NSSupportsAutomaticTermination</key>
<true/>
<key>NSSupportsSuddenTermination</key>
<true/>
</dict>
</plist>
PLIST
printf 'APPL????' > "$CONTENTS/PkgInfo"
# Strip extended attributes (resource forks / Finder info) that iCloud and Finder
# attach to copied files — codesign rejects them with "resource fork, Finder
# information, or similar detritus not allowed". SwiftPM ships some resource files
# read-only, so make the tree writable first or xattr -c gets EACCES.
chmod -R u+w "$APP"
xattr -cr "$APP"
echo "▸ Code-signing (identity: $SIGN_ID)"
# The virtualization entitlement is required to boot Linux VMs in-process (ContainerEngine).
# Honored with ad-hoc signing for local dev on Apple silicon; distribution needs it provisioned.
codesign --force --options runtime --timestamp=none \
--entitlements "$ROOT/signing/nucleic.entitlements" \
--sign "$SIGN_ID" "$APP"
codesign --verify --verbose=1 "$APP"
echo "✓ $APP"
echo " bundle id $BUNDLE_ID"
echo " version $MARKETING_VERSION (build $BUILD_NUMBER, commit $COMMIT)"
echo " open with: open \"$APP\""