packaging: macOS app and dmg, Linux tar.gz and AppImage, Windows zip

packaging/macos/package.sh builds FieldCraft.app (io.github.jyviko.fieldcraft, with icon.icns
and the layered Assets.car), signs it with a Developer ID and notarizes it when the
credentials are there and ad-hoc otherwise, and puts it on a drag-to-Applications dmg that
is mounted and checked; fieldcraft-cli goes in its own zip. packaging/linux/package.sh makes
an FHS tar.gz (binaries, .desktop entry, hicolor icons, licences) and an AppImage made with
appimagetool and the type 2 runtime at pinned, hash-checked releases.
packaging/windows/package.ps1 zips fieldcraft.exe and fieldcraft-cli.exe (static CRT,
checked PE headers and icon). Every package carries README.md, LICENSE-MIT, LICENSE-APACHE,
NOTICE and THIRD-PARTY-LICENSES.txt (#72).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 07:56:06 -04:00
1 parent 858b6e9dee
commit 0d7c881eea
13 files changed
+695

No files matched your search

+50
View File
@@ -0,0 +1,50 @@
<?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">
<!-- Template: packaging/macos/package.sh substitutes @APP_ID@, @VERSION@ (full, e.g. 0.2.0-rc.1),
@SHORT_VERSION@ (numeric X.Y.Z), @BUILD_SHA@ and @MIN_MACOS@
(MACOSX_DEPLOYMENT_TARGET). Adapted from SolidCraft's and VectorCraft's
(MIT OR Apache-2.0; see ATTRIBUTION.md). -->
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>FieldCraft</string>
<key>CFBundleName</key>
<string>FieldCraft</string>
<key>CFBundleExecutable</key>
<string>FieldCraft</string>
<key>CFBundleIdentifier</key>
<string>@APP_ID@</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleShortVersionString</key>
<string>@SHORT_VERSION@</string>
<key>CFBundleVersion</key>
<string>@SHORT_VERSION@</string>
<key>FieldCraftVersion</key>
<string>@VERSION@</string>
<key>FieldCraftBuildCommit</key>
<string>@BUILD_SHA@</string>
<!-- Contents/Resources/FieldCraft.icns (assets/app-icon/icon.icns) for macOS before 26, and the
FieldCraft icon in Contents/Resources/Assets.car (assets/app-icon/Assets.car) from 26 on. -->
<key>CFBundleIconFile</key>
<string>FieldCraft</string>
<key>CFBundleIconName</key>
<string>FieldCraft</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.graphics-design</string>
<key>LSMinimumSystemVersion</key>
<string>@MIN_MACOS@</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSSupportsAutomaticGraphicsSwitching</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © FieldCraft contributors. MIT OR Apache-2.0.</string>
</dict>
</plist>
+13
View File
@@ -0,0 +1,13 @@
Install FieldCraft
Drag FieldCraft.app onto Applications.
If this build is not notarized (the release notes say so), macOS blocks the first launch:
right-click FieldCraft.app in Applications and choose Open, then Open again. Or run
xattr -dr com.apple.quarantine /Applications/FieldCraft.app
Updating: quit FieldCraft, drag FieldCraft.app from this disk image onto Applications and
choose Replace.
The command-line tool, fieldcraft-cli, is a separate download (fieldcraft-cli-*-macos-*.zip).
+9
View File
@@ -0,0 +1,9 @@
<?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">
<!-- Hardened-runtime entitlements for FieldCraft. Deliberately empty: the app is pure Rust, loads
no unsigned code, needs no JIT and is not sandboxed (it opens files by path, from the command
line and drag-and-drop). Add an exception here only with a reason in a comment. -->
<plist version="1.0">
<dict>
</dict>
</plist>
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# CI only: import the Developer ID certificate into a temporary keychain so codesign can use it.
#
# Adapted from VectorCraft's packaging/macos/import-cert.sh (MIT OR Apache-2.0; see ATTRIBUTION.md).
#
# Inputs (env): APPLE_CERTIFICATE (base64 .p12), APPLE_CERTIFICATE_PASSWORD, KEYCHAIN_PASSWORD
# (optional; random if unset), RUNNER_TEMP (or TMPDIR).
# Outputs: MACOS_SIGN_IDENTITY (SHA-1 of the identity) and MACOS_KEYCHAIN, appended to
# $GITHUB_ENV when running in Actions, otherwise printed.
#
# Missing certificate: prints a warning and exits 0, so package.sh falls back to ad-hoc signing.
set -euo pipefail
umask 077
# shellcheck source=../env.sh
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
if [ -z "${APPLE_CERTIFICATE:-}" ]; then
warn "APPLE_CERTIFICATE is not set: macOS builds will be ad-hoc signed and not notarized"
exit 0
fi
TMP="${RUNNER_TEMP:-${TMPDIR:-/tmp}}"
KEYCHAIN="$TMP/fieldcraft-signing.keychain-db"
CERT="$TMP/fieldcraft-signing.p12"
KC_PASS="${KEYCHAIN_PASSWORD:-$(openssl rand -hex 24)}"
trap 'rm -f "$CERT"' EXIT
printf '%s' "$APPLE_CERTIFICATE" | base64 --decode >"$CERT"
security create-keychain -p "$KC_PASS" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KC_PASS" "$KEYCHAIN"
security import "$CERT" -P "${APPLE_CERTIFICATE_PASSWORD:-}" -T /usr/bin/codesign -T /usr/bin/security -t cert -f pkcs12 -k "$KEYCHAIN"
rm -f "$CERT"
# Let codesign use the key without a UI prompt.
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KC_PASS" "$KEYCHAIN" >/dev/null
# Put it on the search list (keeping the login keychain) so codesign and notarytool find it.
# shellcheck disable=SC2046
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"')
IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" | awk '/Developer ID Application/ { print $2; exit }')"
if [ -z "$IDENTITY" ]; then
security find-identity -v -p codesigning "$KEYCHAIN" >&2
echo "error: no 'Developer ID Application' identity in APPLE_CERTIFICATE" >&2
exit 1
fi
echo "Imported signing identity $IDENTITY"
if [ -n "${GITHUB_ENV:-}" ]; then
{ echo "MACOS_SIGN_IDENTITY=$IDENTITY"; echo "MACOS_KEYCHAIN=$KEYCHAIN"; } >>"$GITHUB_ENV"
else
echo "MACOS_SIGN_IDENTITY=$IDENTITY"
echo "MACOS_KEYCHAIN=$KEYCHAIN"
fi
+216
View File
@@ -0,0 +1,216 @@
#!/usr/bin/env bash
# Build, sign and (with credentials) notarize FieldCraft for macOS:
#
# $DIST/fieldcraft-<version>-macos-<arch>.dmg FieldCraft.app on a drag-to-Applications DMG
# $DIST/fieldcraft-cli-<version>-macos-<arch>.zip fieldcraft-cli with the licence files
#
# Usage: packaging/macos/package.sh [--arch universal|aarch64|x86_64] [--skip-build]
#
# Signing (env):
# MACOS_SIGN_IDENTITY codesign identity (name or SHA-1). Default "-": ad-hoc, which Gatekeeper
# blocks on other Macs until the user allows it. In CI, import-cert.sh sets
# it from the APPLE_CERTIFICATE secret.
# MACOS_KEYCHAIN keychain holding the identity (optional)
# Notarization (env; all three, and a real identity): APPLE_ID, APPLE_PASSWORD (an app-specific
# password), APPLE_TEAM_ID.
#
# Writes `signing=notarized|signed|adhoc` to $GITHUB_OUTPUT when run in GitHub Actions.
# Adapted from SolidCraft's and VectorCraft's packaging/macos/package.sh (MIT OR Apache-2.0; see
# ATTRIBUTION.md).
set -euo pipefail
# shellcheck source=../env.sh
. "$(dirname "${BASH_SOURCE[0]}")/../env.sh"
HERE="$ROOT/packaging/macos"
ICONS="$ROOT/assets/app-icon"
ARCH=universal
SKIP_BUILD=0
while [ $# -gt 0 ]; do
case "$1" in
--arch) ARCH="$2"; shift 2 ;;
--skip-build) SKIP_BUILD=1; shift ;;
-h | --help) sed -n '2,17p' "$0"; exit 0 ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
case "$ARCH" in
universal) TARGETS=(aarch64-apple-darwin x86_64-apple-darwin); WANT=(arm64 x86_64) ;;
aarch64) TARGETS=(aarch64-apple-darwin); WANT=(arm64) ;;
x86_64) TARGETS=(x86_64-apple-darwin); WANT=(x86_64) ;;
*) echo "unknown --arch $ARCH" >&2; exit 2 ;;
esac
# The oldest macOS the binaries run on; also the Info.plist's LSMinimumSystemVersion.
export MACOSX_DEPLOYMENT_TARGET=11.0
IDENTITY="${MACOS_SIGN_IDENTITY:--}"
SHORT_VERSION="${VERSION%%-*}"
WORK="$CARGO_TARGET_DIR/macos-package"
APP="$WORK/FieldCraft.app"
DMG="$DIST/fieldcraft-$VERSION-macos-$ARCH.dmg"
CLI_ZIP="$DIST/fieldcraft-cli-$VERSION-macos-$ARCH.zip"
NOTARIZE=0
SIGNING=adhoc
if [ "$IDENTITY" = "-" ]; then
warn "macOS: ad-hoc signing (no MACOS_SIGN_IDENTITY); the app is not notarized"
elif [ -n "${APPLE_ID:-}" ] && [ -n "${APPLE_PASSWORD:-}" ] && [ -n "${APPLE_TEAM_ID:-}" ]; then
NOTARIZE=1
SIGNING=notarized
else
SIGNING=signed
warn "macOS: APPLE_ID / APPLE_PASSWORD / APPLE_TEAM_ID incomplete; signed but not notarized"
fi
echo "==> FieldCraft $VERSION for macOS ($ARCH), identity: $IDENTITY, notarize: $NOTARIZE"
# ---- build -------------------------------------------------------------------------------------
if [ "$SKIP_BUILD" = 0 ]; then
args=()
for t in "${TARGETS[@]}"; do args+=(--target "$t"); done
(cd "$ROOT" && cargo build --release --locked -p fieldcraft -p fieldcraft-cli "${args[@]}")
fi
rm -rf "$WORK"
mkdir -p "$WORK/bin"
for bin in fieldcraft fieldcraft-cli; do
inputs=()
for t in "${TARGETS[@]}"; do inputs+=("$CARGO_TARGET_DIR/$t/release/$bin"); done
lipo -create -output "$WORK/bin/$bin" "${inputs[@]}"
lipo -info "$WORK/bin/$bin"
done
# ---- signing helpers ---------------------------------------------------------------------------
sign() {
# Hardened runtime and a secure timestamp for a real identity; ad-hoc can't be timestamped.
local ts=(--timestamp)
[ "$IDENTITY" = "-" ] && ts=(--timestamp=none)
local kc=()
[ -n "${MACOS_KEYCHAIN:-}" ] && kc=(--keychain "$MACOS_KEYCHAIN")
codesign --force --sign "$IDENTITY" ${kc[@]+"${kc[@]}"} "${ts[@]}" "$@"
}
notarize() {
local file="$1" out id status
echo "==> notarizing $(basename "$file") (this can take a few minutes)"
out="$(xcrun notarytool submit "$file" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" \
--team-id "$APPLE_TEAM_ID" --wait --timeout 1h --output-format json)" || true
echo "$out"
id="$(printf '%s' "$out" | plutil -extract id raw -o - - 2>/dev/null || true)"
status="$(printf '%s' "$out" | plutil -extract status raw -o - - 2>/dev/null || true)"
if [ "$status" != "Accepted" ]; then
if [ -n "$id" ]; then
xcrun notarytool log "$id" --apple-id "$APPLE_ID" --password "$APPLE_PASSWORD" --team-id "$APPLE_TEAM_ID" || true
fi
echo "error: notarization of $(basename "$file") failed (status: ${status:-unknown})" >&2
exit 1
fi
}
# hdiutil attaches a device while it works, which fails now and then on CI runners ("Resource
# busy"). Usage: hdiutil_retry <hdiutil args…>; five tries with growing pauses.
hdiutil_retry() {
local what="$1" attempt
for attempt in 1 2 3 4 5; do
if hdiutil "$@"; then return 0; fi
[ "$attempt" = 5 ] && { echo "hdiutil $what failed 5 times" >&2; return 1; }
echo "hdiutil $what failed (attempt $attempt of 5); retrying in $((attempt * 10))s" >&2
sleep $((attempt * 10))
done
}
# ---- FieldCraft.app ----------------------------------------------------------------------------
echo "==> assembling $APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources/Licenses"
# The executable and the icons carry the display name (CFBundleExecutable, CFBundleIconFile,
# CFBundleIconName). Assets.car holds the layered icon for macOS 26 and later, the .icns the
# icon for earlier versions (assets/app-icon/README.md).
cp "$WORK/bin/fieldcraft" "$APP/Contents/MacOS/FieldCraft"
cp "$ICONS/icon.icns" "$APP/Contents/Resources/FieldCraft.icns"
cp "$ICONS/Assets.car" "$APP/Contents/Resources/Assets.car"
copy_docs "$APP/Contents/Resources/Licenses"
sed -e "s/@APP_ID@/$APP_ID/g" -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \
-e "s/@BUILD_SHA@/$FIELDCRAFT_BUILD_SHA/g" -e "s/@MIN_MACOS@/$MACOSX_DEPLOYMENT_TARGET/g" "$HERE/Info.plist.in" >"$APP/Contents/Info.plist"
plutil -lint "$APP/Contents/Info.plist"
printf 'APPL????' >"$APP/Contents/PkgInfo"
# Sign inside-out: the executable, then the bundle (no --deep on the bundle's own signature).
sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/MacOS/FieldCraft"
sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP"
codesign --verify --strict --deep --verbose=2 "$APP"
codesign -dv "$APP"
if [ "$NOTARIZE" = 1 ]; then
ditto -c -k --keepParent "$APP" "$WORK/FieldCraft-notarize.zip"
notarize "$WORK/FieldCraft-notarize.zip"
xcrun stapler staple "$APP"
xcrun stapler validate "$APP"
spctl --assess --type execute -vvv "$APP"
fi
# ---- DMG ---------------------------------------------------------------------------------------
echo "==> building $DMG"
STAGE="$WORK/dmg"
mkdir -p "$STAGE"
ditto "$APP" "$STAGE/FieldCraft.app"
ln -s /Applications "$STAGE/Applications"
cp "$HERE/README.txt" "$STAGE/README.txt"
rm -f "$DMG"
# `create -srcfolder` writes the files as they are; `makehybrid -hfs` gives every file a
# com.apple.FinderInfo, which `codesign --verify --strict` rejects (VectorCraft found this).
hdiutil_retry create -srcfolder "$STAGE" -volname "FieldCraft" -fs HFS+ -format UDZO \
-imagekey zlib-level=9 -ov "$DMG"
sign "$DMG"
codesign --verify --strict --verbose=2 "$DMG"
# Check the image as a user gets it: mount it and look inside.
MNT="$WORK/dmg-check"
rm -rf "$MNT"
mkdir -p "$MNT"
hdiutil_retry attach -nobrowse -readonly -mountpoint "$MNT" "$DMG" >/dev/null
# Detach on the way out, also when a check below fails and ends the script.
trap 'hdiutil detach "$MNT" >/dev/null 2>&1 || true' EXIT
M_APP="$MNT/FieldCraft.app"
M_PLIST="$M_APP/Contents/Info.plist"
test -L "$MNT/Applications"
test "$(readlink "$MNT/Applications")" = /Applications
cmp "$HERE/README.txt" "$MNT/README.txt"
plutil -lint "$M_PLIST"
for key in CFBundleName CFBundleDisplayName CFBundleExecutable CFBundleIconFile CFBundleIconName; do
test "$(/usr/libexec/PlistBuddy -c "Print :$key" "$M_PLIST")" = FieldCraft
done
test "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$M_PLIST")" = "$APP_ID"
test "$(/usr/libexec/PlistBuddy -c 'Print :FieldCraftVersion' "$M_PLIST")" = "$VERSION"
cmp "$ICONS/icon.icns" "$M_APP/Contents/Resources/FieldCraft.icns"
cmp "$ICONS/Assets.car" "$M_APP/Contents/Resources/Assets.car"
for f in "${DOC_FILES[@]}"; do
test -s "$M_APP/Contents/Resources/Licenses/$f"
done
for arch in "${WANT[@]}"; do lipo "$M_APP/Contents/MacOS/FieldCraft" -verify_arch "$arch"; done
codesign --verify --strict --deep --verbose=2 "$M_APP"
"$M_APP/Contents/MacOS/FieldCraft" --version
hdiutil_retry detach "$MNT" >/dev/null
trap - EXIT
if [ "$NOTARIZE" = 1 ]; then
notarize "$DMG"
xcrun stapler staple "$DMG"
xcrun stapler validate "$DMG"
spctl --assess --type open --context context:primary-signature -vvv "$DMG"
fi
# ---- CLI ---------------------------------------------------------------------------------------
echo "==> building $CLI_ZIP"
CLI_DIR="$WORK/fieldcraft-cli-$VERSION-macos-$ARCH"
mkdir -p "$CLI_DIR"
cp "$WORK/bin/fieldcraft-cli" "$CLI_DIR/"
copy_docs "$CLI_DIR"
sign --options runtime --identifier "$APP_ID.cli" "$CLI_DIR/fieldcraft-cli"
codesign --verify --strict --verbose=2 "$CLI_DIR/fieldcraft-cli"
rm -f "$CLI_ZIP"
ditto -c -k --keepParent "$CLI_DIR" "$CLI_ZIP"
# A bare Mach-O can't carry a stapled ticket; Gatekeeper looks the notarization up online.
if [ "$NOTARIZE" = 1 ]; then notarize "$CLI_ZIP"; fi
"$CLI_DIR/fieldcraft-cli" --version
if [ -n "${GITHUB_OUTPUT:-}" ]; then echo "signing=$SIGNING" >>"$GITHUB_OUTPUT"; fi
echo "==> done (signing: $SIGNING)"
ls -lh "$DMG" "$CLI_ZIP"