The macOS app and the Windows bundle shipped every module in the repo, on
by default, and a shell update silently added whatever new module had
landed on main — Band Lineup appeared on AB that way. A module was also
only as fresh as the last shell update. Now a packaged install (no .git —
the same test the updater uses to choose the archive over git pull)
carries no modules at all: every one arrives from the admin page's catalog
into the data directory, and the shell's own update never writes modules/.
A git checkout keeps running the modules in its tree.
Existing installs move over by themselves: at the first start, the bundled
modules that are switched on are copied into the data directory as
installed copies (a compiled helper keeps its mode), an uninstalled one is
dropped, and the app folder's modules/ is removed. Settings are keyed by
module id, so nothing changes for the operator.
Around it: the dashboard's empty state says "No modules yet — open Admin"
when the registry is empty; the admin page says "Nothing installed yet",
puts a checkbox on every compatible catalog entry and an Install selected
button (POST /api/admin/modules/install {ids}) that fetches them in one go.
The Mac build stops copying modules/ and no longer compiles or signs Swift
helpers — those ship prebuilt inside a module, per the guide.
tools/unbundled.test.js drives a packaged copy of the shell through the
move, an archive update that must not bring modules back, and a multi-install.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
206 lines
8.5 KiB
Bash
Executable File
206 lines
8.5 KiB
Bash
Executable File
#!/bin/zsh
|
|
# Build ProdDash.app — the whole application: launcher, server and its own
|
|
# Node runtime, with nothing to install first. It carries no modules: those
|
|
# come from the admin page's catalog into the data directory, and a module's
|
|
# native helper ships prebuilt inside it (docs/MODULE-GUIDE.md).
|
|
#
|
|
# ./build.sh build/ProdDash.app, self-contained
|
|
# ./build.sh --dmg …and build/ProdDash-<version>-arm64.dmg
|
|
# ./build.sh --install …and copy it to /Applications
|
|
# ./build.sh --run …and launch it
|
|
# ./build.sh --notarize with --dmg: notarise and staple (needs a
|
|
# Developer ID and a notarytool keychain profile)
|
|
# ./build.sh --dev launcher only — no Node, no server payload, so
|
|
# it runs against the checkout. Seconds, not
|
|
# minutes, while working on the launcher itself.
|
|
#
|
|
# Signing: a "Developer ID Application" certificate is used when one is
|
|
# installed (override with CODESIGN_IDENTITY). Without one the app is signed
|
|
# ad-hoc, which is fine on your own Macs but makes another Mac ask before it
|
|
# will open the app.
|
|
#
|
|
# Needs the Xcode command line tools. Everything else it fetches or builds.
|
|
set -e -u
|
|
cd "$(dirname "$0")"
|
|
|
|
APP_NAME="ProdDash"
|
|
BUNDLE_ID="org.waterschurch.proddash"
|
|
LAUNCHER_VERSION="1.0.0"
|
|
# The Node the app ships. Bump it here; the download is checksum-verified
|
|
# against nodejs.org's own SHASUMS256.txt and cached in .cache/.
|
|
NODE_VERSION="v24.21.0"
|
|
ARCH="arm64"
|
|
DEPLOYMENT_TARGET="13.0"
|
|
|
|
REPO_ROOT="../.."
|
|
OUT_DIR="build"
|
|
CACHE_DIR=".cache"
|
|
APP="$OUT_DIR/$APP_NAME.app"
|
|
CONTENTS="$APP/Contents"
|
|
PAYLOAD="$CONTENTS/Resources/app"
|
|
|
|
DEV=0
|
|
INSTALL=0
|
|
RUN=0
|
|
MAKE_DMG=0
|
|
NOTARIZE=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--dev) DEV=1 ;;
|
|
--install) INSTALL=1 ;;
|
|
--run) RUN=1 ;;
|
|
--dmg) MAKE_DMG=1 ;;
|
|
--notarize) NOTARIZE=1; MAKE_DMG=1 ;;
|
|
-h|--help) sed -n '2,22p' "$0"; exit 0 ;;
|
|
*) echo "build.sh: unknown option $1" >&2; exit 2 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
command -v swiftc >/dev/null || {
|
|
echo "build.sh: swiftc not found — install the Xcode command line tools:" >&2
|
|
echo " xcode-select --install" >&2
|
|
exit 1
|
|
}
|
|
|
|
# The product's version is ProdDash's own; the launcher keeps its own number
|
|
# for when something is wrong with the launcher rather than the dashboard.
|
|
VERSION=$(sed -n 's/.*"version" *: *"\([^"]*\)".*/\1/p' "$REPO_ROOT/package.json" | head -1)
|
|
[[ -n "$VERSION" ]] || { echo "build.sh: can't read the version from package.json" >&2; exit 1; }
|
|
|
|
echo "ProdDash $VERSION (launcher $LAUNCHER_VERSION) — building for $ARCH"
|
|
|
|
rm -rf "$OUT_DIR"
|
|
mkdir -p "$CONTENTS/MacOS" "$CONTENTS/Resources"
|
|
|
|
# ── the launcher ─────────────────────────────────────────────────────────
|
|
echo " compiling the launcher"
|
|
swiftc -O -whole-module-optimization \
|
|
-target "$ARCH-apple-macos$DEPLOYMENT_TARGET" \
|
|
-module-name ProdDashLauncher \
|
|
-o "$CONTENTS/MacOS/$APP_NAME" \
|
|
Sources/*.swift
|
|
|
|
cp Resources/Info.plist "$CONTENTS/Info.plist"
|
|
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$CONTENTS/Info.plist" >/dev/null
|
|
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" "$CONTENTS/Info.plist" >/dev/null
|
|
/usr/libexec/PlistBuddy -c "Set :ProdDashLauncherVersion $LAUNCHER_VERSION" "$CONTENTS/Info.plist" >/dev/null
|
|
printf 'APPL????' > "$CONTENTS/PkgInfo"
|
|
|
|
SOURCE_ICON="$REPO_ROOT/public/icons/icon-512.png"
|
|
if [[ -f "$SOURCE_ICON" ]]; then
|
|
echo " building the icon"
|
|
ICONSET="$OUT_DIR/AppIcon.iconset"
|
|
mkdir -p "$ICONSET"
|
|
for size in 16 32 64 128 256 512 1024; do
|
|
sips -z $size $size "$SOURCE_ICON" --out "$ICONSET/icon_${size}x${size}.png" >/dev/null 2>&1
|
|
done
|
|
mv "$ICONSET/icon_32x32.png" "$ICONSET/icon_16x16@2x.png"
|
|
cp "$ICONSET/icon_64x64.png" "$ICONSET/icon_32x32@2x.png"
|
|
mv "$ICONSET/icon_64x64.png" "$ICONSET/icon_32x32.png"
|
|
cp "$ICONSET/icon_256x256.png" "$ICONSET/icon_128x128@2x.png"
|
|
cp "$ICONSET/icon_512x512.png" "$ICONSET/icon_256x256@2x.png"
|
|
mv "$ICONSET/icon_1024x1024.png" "$ICONSET/icon_512x512@2x.png"
|
|
iconutil -c icns "$ICONSET" -o "$CONTENTS/Resources/AppIcon.icns"
|
|
rm -rf "$ICONSET"
|
|
fi
|
|
|
|
# ── the server, and the Node that runs it ────────────────────────────────
|
|
if [[ $DEV -eq 0 ]]; then
|
|
TARBALL="node-$NODE_VERSION-darwin-$ARCH.tar.gz"
|
|
mkdir -p "$CACHE_DIR"
|
|
if [[ ! -f "$CACHE_DIR/$TARBALL" ]]; then
|
|
echo " fetching Node $NODE_VERSION (~50 MB, cached for next time)"
|
|
curl -fsSL --retry 3 -o "$CACHE_DIR/$TARBALL.part" "https://nodejs.org/dist/$NODE_VERSION/$TARBALL"
|
|
mv "$CACHE_DIR/$TARBALL.part" "$CACHE_DIR/$TARBALL"
|
|
fi
|
|
SUMS="$CACHE_DIR/SHASUMS256-$NODE_VERSION.txt"
|
|
[[ -f "$SUMS" ]] || curl -fsSL --retry 3 -o "$SUMS" "https://nodejs.org/dist/$NODE_VERSION/SHASUMS256.txt"
|
|
# This binary is about to be signed with your name on it — check it every build.
|
|
WANT=$(awk -v f="$TARBALL" '$2 == f {print $1}' "$SUMS")
|
|
GOT=$(shasum -a 256 "$CACHE_DIR/$TARBALL" | awk '{print $1}')
|
|
if [[ -z "$WANT" || "$WANT" != "$GOT" ]]; then
|
|
echo "build.sh: $TARBALL does not match nodejs.org's checksum — refusing to ship it" >&2
|
|
exit 1
|
|
fi
|
|
echo " embedding Node $NODE_VERSION"
|
|
mkdir -p "$CONTENTS/Resources/node/bin"
|
|
tar -xzf "$CACHE_DIR/$TARBALL" -C "$CONTENTS/Resources/node/bin" --strip-components 2 \
|
|
"node-$NODE_VERSION-darwin-$ARCH/bin/node"
|
|
|
|
echo " embedding ProdDash $VERSION (no modules — the catalog installs them)"
|
|
mkdir -p "$PAYLOAD/config"
|
|
cp "$REPO_ROOT/server.js" "$REPO_ROOT/package.json" "$PAYLOAD/"
|
|
cp -R "$REPO_ROOT/public" "$PAYLOAD/"
|
|
# Only the checked-in defaults: config/ also holds this machine's own
|
|
# modules.json and layouts when ProdDash has ever run from the checkout.
|
|
cp "$REPO_ROOT/config/proddash.json" "$PAYLOAD/config/"
|
|
find "$PAYLOAD" -name '.DS_Store' -delete
|
|
fi
|
|
|
|
# ── signing ──────────────────────────────────────────────────────────────
|
|
IDENTITY="${CODESIGN_IDENTITY:-}"
|
|
if [[ -z "$IDENTITY" ]]; then
|
|
IDENTITY=$(security find-identity -v -p codesigning 2>/dev/null \
|
|
| awk -F'"' '/Developer ID Application/ {print $2; exit}')
|
|
fi
|
|
if [[ -n "$IDENTITY" ]]; then
|
|
echo " signing as $IDENTITY"
|
|
SIGN=(--force --sign "$IDENTITY" --options runtime --timestamp)
|
|
else
|
|
echo " signing ad-hoc (no Developer ID installed — other Macs will ask before opening it)"
|
|
IDENTITY="-"
|
|
SIGN=(--force --sign -)
|
|
fi
|
|
|
|
# Nested code first, outermost last, or the outer signature seals a lie.
|
|
if [[ -f "$CONTENTS/Resources/node/bin/node" ]]; then
|
|
codesign "${SIGN[@]}" --entitlements Resources/node.entitlements "$CONTENTS/Resources/node/bin/node"
|
|
fi
|
|
codesign "${SIGN[@]}" --entitlements Resources/ProdDash.entitlements --identifier "$BUNDLE_ID" "$APP"
|
|
codesign --verify --deep --strict "$APP" && echo " signature verifies"
|
|
|
|
SIZE=$(du -sh "$APP" | awk '{print $1}')
|
|
echo "built $APP ($SIZE)"
|
|
|
|
# ── disk image ───────────────────────────────────────────────────────────
|
|
if [[ $MAKE_DMG -eq 1 ]]; then
|
|
DMG="$OUT_DIR/$APP_NAME-$VERSION-$ARCH.dmg"
|
|
STAGE="$OUT_DIR/dmg"
|
|
rm -rf "$STAGE"
|
|
mkdir -p "$STAGE"
|
|
cp -R "$APP" "$STAGE/"
|
|
ln -s /Applications "$STAGE/Applications" # drag-to-install, the usual way
|
|
hdiutil create -volname "$APP_NAME $VERSION" -srcfolder "$STAGE" \
|
|
-ov -format UDZO -quiet "$DMG"
|
|
rm -rf "$STAGE"
|
|
[[ "$IDENTITY" == "-" ]] || codesign --force --sign "$IDENTITY" --timestamp "$DMG"
|
|
echo "built $DMG ($(du -sh "$DMG" | awk '{print $1}'))"
|
|
|
|
if [[ $NOTARIZE -eq 1 ]]; then
|
|
if [[ "$IDENTITY" == "-" ]]; then
|
|
echo "build.sh: can't notarise an ad-hoc signed app — a Developer ID is required" >&2
|
|
exit 1
|
|
fi
|
|
PROFILE="${NOTARY_PROFILE:-proddash}"
|
|
echo " notarising as keychain profile \"$PROFILE\" (a few minutes)"
|
|
xcrun notarytool submit "$DMG" --keychain-profile "$PROFILE" --wait
|
|
xcrun stapler staple "$DMG"
|
|
echo " notarised and stapled — this opens anywhere with no warning"
|
|
fi
|
|
fi
|
|
|
|
if [[ $INSTALL -eq 1 ]]; then
|
|
DEST="/Applications/$APP_NAME.app"
|
|
osascript -e "tell application \"$APP_NAME\" to quit" >/dev/null 2>&1 || true
|
|
sleep 1
|
|
rm -rf "$DEST"
|
|
cp -R "$APP" "$DEST"
|
|
echo "installed $DEST"
|
|
APP="$DEST"
|
|
fi
|
|
|
|
[[ $RUN -eq 1 ]] && open "$APP"
|
|
exit 0
|