Skip to content

Commit 36615df

Browse files
authored
Merge pull request #5 from durul/build/local-app-bundle
build: add build-app.sh for local .app bundle
2 parents cd7d354 + eea0b99 commit 36615df

2 files changed

Lines changed: 154 additions & 0 deletions

File tree

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.build/
2+
build/
3+
.vscode/
4+
.claude/worktrees/
5+
Package.resolved
6+
.DS_Store

scripts/build-app.sh

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
#!/bin/bash
2+
#
3+
# build-app.sh - Build SkillsManager.app bundle for local use
4+
#
5+
# Produces a properly bundled, ad-hoc signed macOS application at
6+
# build/SkillsManager.app. The bundle has a Dock icon, app menu,
7+
# and Sparkle.framework wired up — i.e. behaves like a real installed
8+
# app rather than a raw `swift run` executable.
9+
#
10+
# Usage:
11+
# ./scripts/build-app.sh # build, then `open` the app
12+
# ./scripts/build-app.sh --no-open # build only
13+
#
14+
15+
set -euo pipefail
16+
17+
# Colors
18+
GREEN='\033[0;32m'
19+
YELLOW='\033[1;33m'
20+
RED='\033[0;31m'
21+
NC='\033[0m'
22+
23+
OPEN_AFTER_BUILD=1
24+
for arg in "$@"; do
25+
case "$arg" in
26+
--no-open) OPEN_AFTER_BUILD=0 ;;
27+
--open) OPEN_AFTER_BUILD=1 ;; # kept for back-compat; now the default
28+
-h|--help)
29+
sed -n '2,15p' "$0"
30+
exit 0
31+
;;
32+
*)
33+
echo -e "${RED}Unknown argument: $arg${NC}"
34+
exit 1
35+
;;
36+
esac
37+
done
38+
39+
# Locate repo root (script lives in scripts/). Resolve symlinks so the
40+
# script works when invoked through a symlink in ~/Documents/BashScriptSource.
41+
SOURCE="${BASH_SOURCE[0]}"
42+
while [ -L "$SOURCE" ]; do
43+
DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
44+
SOURCE="$(readlink "$SOURCE")"
45+
[[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE"
46+
done
47+
SCRIPT_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
48+
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
49+
cd "$REPO_ROOT"
50+
51+
APP_NAME="SkillsManager"
52+
BUNDLE_NAME="SkillsManager.app"
53+
BUILD_DIR="$REPO_ROOT/build"
54+
APP_BUNDLE="$BUILD_DIR/$BUNDLE_NAME"
55+
RELEASE_DIR="$REPO_ROOT/.build/release"
56+
57+
echo -e "${YELLOW}==> Building release binary${NC}"
58+
swift build -c release
59+
60+
if [ ! -f "$RELEASE_DIR/$APP_NAME" ]; then
61+
echo -e "${RED}ERROR: Release binary not found at $RELEASE_DIR/$APP_NAME${NC}"
62+
exit 1
63+
fi
64+
65+
echo -e "${YELLOW}==> Creating bundle structure at $APP_BUNDLE${NC}"
66+
rm -rf "$APP_BUNDLE"
67+
mkdir -p "$APP_BUNDLE/Contents/MacOS"
68+
mkdir -p "$APP_BUNDLE/Contents/Resources"
69+
mkdir -p "$APP_BUNDLE/Contents/Frameworks"
70+
71+
echo -e "${YELLOW}==> Copying executable${NC}"
72+
cp "$RELEASE_DIR/$APP_NAME" "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
73+
74+
echo -e "${YELLOW}==> Copying Info.plist${NC}"
75+
cp Sources/App/Info.plist "$APP_BUNDLE/Contents/Info.plist"
76+
77+
echo -e "${YELLOW}==> Writing PkgInfo${NC}"
78+
printf 'APPL????' > "$APP_BUNDLE/Contents/PkgInfo"
79+
80+
if [ -d "$RELEASE_DIR/Sparkle.framework" ]; then
81+
echo -e "${YELLOW}==> Copying Sparkle.framework${NC}"
82+
cp -R "$RELEASE_DIR/Sparkle.framework" "$APP_BUNDLE/Contents/Frameworks/"
83+
84+
# SwiftPM-built binaries have rpath @loader_path (= Contents/MacOS) but
85+
# frameworks live in Contents/Frameworks. Add the standard app-bundle
86+
# rpath so dyld can resolve Sparkle.
87+
install_name_tool -add_rpath "@executable_path/../Frameworks" \
88+
"$APP_BUNDLE/Contents/MacOS/$APP_NAME" 2>/dev/null || true
89+
else
90+
echo -e "${YELLOW}==> Sparkle.framework not in release dir, skipping${NC}"
91+
fi
92+
93+
echo -e "${YELLOW}==> Compiling asset catalog (icon)${NC}"
94+
# actool compiles Assets.xcassets into Assets.car and emits AppIcon.icns
95+
# Partial Info.plist would contain CFBundleIcons keys, but our Info.plist
96+
# already uses the legacy CFBundleIconFile=AppIcon, so we let actool drop
97+
# AppIcon.icns into Resources/ and rely on that key.
98+
ACTOOL_PARTIAL_PLIST="$(mktemp -t actool-partial.XXXXXX.plist)"
99+
xcrun actool \
100+
Sources/App/Resources/Assets.xcassets \
101+
--compile "$APP_BUNDLE/Contents/Resources" \
102+
--platform macosx \
103+
--minimum-deployment-target 15.0 \
104+
--app-icon AppIcon \
105+
--output-partial-info-plist "$ACTOOL_PARTIAL_PLIST" \
106+
--output-format human-readable-text >/dev/null
107+
rm -f "$ACTOOL_PARTIAL_PLIST"
108+
109+
# actool may emit the icon as either AppIcon.icns or embedded in Assets.car.
110+
# If only Assets.car was produced, build AppIcon.icns from the iconset so
111+
# the legacy CFBundleIconFile=AppIcon entry resolves.
112+
if [ ! -f "$APP_BUNDLE/Contents/Resources/AppIcon.icns" ]; then
113+
echo -e "${YELLOW}==> Generating AppIcon.icns from iconset${NC}"
114+
ICONSET_SRC="Sources/App/Resources/Assets.xcassets/AppIcon.appiconset"
115+
TMP_ICONSET="$(mktemp -d)/AppIcon.iconset"
116+
mkdir -p "$TMP_ICONSET"
117+
# iconutil expects icon_<size>x<size>.png and icon_<size>x<size>@2x.png — same naming we already use
118+
cp "$ICONSET_SRC"/icon_*.png "$TMP_ICONSET/" 2>/dev/null || true
119+
if ls "$TMP_ICONSET"/icon_*.png >/dev/null 2>&1; then
120+
iconutil -c icns "$TMP_ICONSET" -o "$APP_BUNDLE/Contents/Resources/AppIcon.icns"
121+
else
122+
echo -e "${YELLOW} (no PNGs found in iconset, app will use default icon)${NC}"
123+
fi
124+
rm -rf "$(dirname "$TMP_ICONSET")"
125+
fi
126+
127+
echo -e "${YELLOW}==> Ad-hoc code signing${NC}"
128+
# Ad-hoc signature (-) lets Gatekeeper run the app locally without a
129+
# Developer ID. For distribution use scripts/sign-app.sh instead.
130+
if [ -d "$APP_BUNDLE/Contents/Frameworks/Sparkle.framework" ]; then
131+
codesign --force --deep --sign - "$APP_BUNDLE/Contents/Frameworks/Sparkle.framework" >/dev/null
132+
fi
133+
codesign --force --sign - \
134+
--entitlements Sources/App/entitlements.plist \
135+
"$APP_BUNDLE/Contents/MacOS/$APP_NAME" >/dev/null
136+
codesign --force --sign - "$APP_BUNDLE" >/dev/null
137+
138+
echo ""
139+
echo -e "${GREEN}SUCCESS${NC}: $APP_BUNDLE"
140+
echo ""
141+
echo "Run it with:"
142+
echo " open $APP_BUNDLE"
143+
echo ""
144+
145+
if [ "$OPEN_AFTER_BUILD" -eq 1 ]; then
146+
echo -e "${YELLOW}==> Opening app${NC}"
147+
open "$APP_BUNDLE"
148+
fi

0 commit comments

Comments
 (0)