mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-30 15:48:34 +00:00
Two master-only scripts still pipe a per-record writer into a reader that exits at its match, under pipefail. In the medium-change node entrypoint, awk exiting at the first matching address can kill ip with SIGPIPE, and the plain assignment of the interface name then ends the entrypoint before the daemon starts. In the macOS package build, grep -q exiting at the key can kill launchctl the same way and fail the build with "no NSBluetoothAlwaysUsageDescription" when the key is present. Both now capture the writer's output first and match on the capture, as the rest of the tree does. A failing ip still returns its own status, and a missing key still fails the package build.
263 lines
9.2 KiB
Bash
Executable File
263 lines
9.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Build a macOS .pkg installer for FIPS.
|
|
#
|
|
# Usage: ./packaging/macos/build-pkg.sh [--version <version>] [--no-build]
|
|
# Output: deploy/fips-<version>-macos-<arch>.pkg
|
|
#
|
|
# Prerequisites: Xcode command-line tools (pkgbuild is included)
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
PACKAGING_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
PROJECT_ROOT="$(cd "${PACKAGING_DIR}/.." && pwd)"
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: packaging/macos/build-pkg.sh [options]
|
|
|
|
Options:
|
|
--version <version> Override package version
|
|
--target <triple> Rust target triple (e.g. x86_64-apple-darwin)
|
|
--no-build Package existing binaries without running cargo build
|
|
-h, --help Show this help
|
|
EOF
|
|
}
|
|
|
|
VERSION_OVERRIDE=""
|
|
TARGET_TRIPLE=""
|
|
NO_BUILD=0
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--version)
|
|
VERSION_OVERRIDE="${2:?missing value for --version}"
|
|
shift 2
|
|
;;
|
|
--target)
|
|
TARGET_TRIPLE="${2:?missing value for --target}"
|
|
shift 2
|
|
;;
|
|
--no-build)
|
|
NO_BUILD=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
usage >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
VERSION="${VERSION_OVERRIDE:-$(grep '^version' "${PROJECT_ROOT}/Cargo.toml" | head -1 | sed 's/.*"\(.*\)"/\1/')}"
|
|
|
|
# Derive the package architecture from the build target, not the build
|
|
# host. When cross-compiling (for example building the x86_64 package on
|
|
# an Apple-silicon machine) `uname -m` reports the host architecture and
|
|
# would mislabel the package; the Rust target triple is authoritative.
|
|
if [[ -n "${TARGET_TRIPLE}" ]]; then
|
|
case "${TARGET_TRIPLE}" in
|
|
aarch64-*) ARCH="arm64" ;;
|
|
x86_64-*) ARCH="x86_64" ;;
|
|
*)
|
|
echo "Unsupported target triple: ${TARGET_TRIPLE}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
else
|
|
ARCH="$(uname -m)"
|
|
fi
|
|
PKG_NAME="fips-${VERSION}-macos-${ARCH}"
|
|
DEPLOY_DIR="${PROJECT_ROOT}/deploy"
|
|
STAGING_DIR="$(mktemp -d)"
|
|
SCRIPTS_DIR="$(mktemp -d)"
|
|
trap 'rm -rf "${STAGING_DIR}" "${SCRIPTS_DIR}"' EXIT
|
|
|
|
if [[ -n "${TARGET_TRIPLE}" ]]; then
|
|
BINARY_DIR="${PROJECT_ROOT}/target/${TARGET_TRIPLE}/release"
|
|
else
|
|
BINARY_DIR="${PROJECT_ROOT}/target/release"
|
|
fi
|
|
|
|
echo "Building FIPS v${VERSION} for macOS ${ARCH}..."
|
|
|
|
# Build release binaries
|
|
if [[ "${NO_BUILD}" -eq 0 ]]; then
|
|
cargo_args=(build --release --manifest-path="${PROJECT_ROOT}/Cargo.toml")
|
|
[[ -n "${TARGET_TRIPLE}" ]] && cargo_args+=(--target "${TARGET_TRIPLE}")
|
|
cargo "${cargo_args[@]}"
|
|
fi
|
|
|
|
# Verify binaries exist
|
|
for bin in fips fipsctl fipstop; do
|
|
if [[ ! -f "${BINARY_DIR}/${bin}" ]]; then
|
|
echo "Missing binary: ${BINARY_DIR}/${bin}" >&2
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
# Stage the payload (mirrors installed filesystem layout)
|
|
mkdir -p "${STAGING_DIR}/usr/local/bin"
|
|
mkdir -p "${STAGING_DIR}/usr/local/etc/fips"
|
|
mkdir -p "${STAGING_DIR}/usr/local/var/log/fips"
|
|
mkdir -p "${STAGING_DIR}/Library/LaunchDaemons"
|
|
mkdir -p "${STAGING_DIR}/Library/LaunchAgents"
|
|
mkdir -p "${STAGING_DIR}/etc/resolver"
|
|
|
|
# Binaries
|
|
for bin in fips fipsctl fipstop; do
|
|
cp "${BINARY_DIR}/${bin}" "${STAGING_DIR}/usr/local/bin/"
|
|
strip "${STAGING_DIR}/usr/local/bin/${bin}"
|
|
done
|
|
# The BLE agent runs the daemon binary under its own name, so Activity
|
|
# Monitor tells the two processes apart. A hard link, not a symlink: macOS
|
|
# names a process after the file it executed, and a symlink resolves to fips.
|
|
ln "${STAGING_DIR}/usr/local/bin/fips" "${STAGING_DIR}/usr/local/bin/fips-ble-agent"
|
|
|
|
# CoreBluetooth kills a process that has no NSBluetoothAlwaysUsageDescription
|
|
# in its Info.plist. build.rs embeds one in the fips binary through a linker
|
|
# argument, which nothing else would notice going missing: check the stripped
|
|
# binary that ships still carries it.
|
|
# Captured before matching: under pipefail, grep -q exiting at its match
|
|
# can kill the writer with SIGPIPE and turn the match into a miss.
|
|
# Captured inside the condition so that set -e does not end the script.
|
|
if ! grep -q NSBluetoothAlwaysUsageDescription \
|
|
<<<"$(launchctl plist __TEXT,__info_plist "${STAGING_DIR}/usr/local/bin/fips" 2>/dev/null)"; then
|
|
echo "fips binary has no NSBluetoothAlwaysUsageDescription in __TEXT,__info_plist;" \
|
|
"BLE would crash it. Check the -sectcreate link argument in build.rs." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Config (marked as conf file via postinstall logic — won't overwrite on upgrade)
|
|
cp "${PACKAGING_DIR}/common/fips.yaml" "${STAGING_DIR}/usr/local/etc/fips/fips.yaml.default"
|
|
cp "${PACKAGING_DIR}/common/hosts" "${STAGING_DIR}/usr/local/etc/fips/hosts.default"
|
|
|
|
# LaunchDaemon plist
|
|
cp "${SCRIPT_DIR}/com.fips.daemon.plist" "${STAGING_DIR}/Library/LaunchDaemons/"
|
|
|
|
# BLE agent: the Bluetooth radio, run in the user's login session
|
|
cp "${SCRIPT_DIR}/com.fips.ble-agent.plist" "${STAGING_DIR}/Library/LaunchAgents/"
|
|
|
|
# DNS resolver. Must match the daemon's dns.bind_addr (defaults to ::1).
|
|
cat > "${STAGING_DIR}/etc/resolver/fips" <<EOF
|
|
nameserver ::1
|
|
port 5354
|
|
EOF
|
|
|
|
# Create postinstall script
|
|
cat > "${SCRIPTS_DIR}/postinstall" <<'POSTINSTALL'
|
|
#!/bin/sh
|
|
set -e
|
|
|
|
LOG="/var/log/fips-install.log"
|
|
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG"; logger -t fips-install "$*"; }
|
|
|
|
log "postinstall started"
|
|
|
|
CONFDIR="/usr/local/etc/fips"
|
|
|
|
# Install default config only if none exists (preserve on upgrade)
|
|
if [ ! -f "$CONFDIR/fips.yaml" ]; then
|
|
cp "$CONFDIR/fips.yaml.default" "$CONFDIR/fips.yaml"
|
|
chmod 600 "$CONFDIR/fips.yaml"
|
|
log "installed default config"
|
|
fi
|
|
if [ ! -f "$CONFDIR/hosts" ]; then
|
|
cp "$CONFDIR/hosts.default" "$CONFDIR/hosts"
|
|
fi
|
|
|
|
# The daemon writes its log here as root. A directory that predates the
|
|
# package keeps its owner, and one another user can write to would let them
|
|
# swap the log for a link elsewhere; the daemon refuses to follow one, but a
|
|
# root-owned directory keeps the log from being tampered with at all.
|
|
LOGDIR="/usr/local/var/log/fips"
|
|
mkdir -p "$LOGDIR"
|
|
chown root:wheel "$LOGDIR"
|
|
chmod 755 "$LOGDIR"
|
|
|
|
# Flush DNS cache so macOS picks up the new /etc/resolver/fips file
|
|
dscacheutil -flushcache
|
|
killall -HUP mDNSResponder 2>/dev/null || true
|
|
log "flushed DNS cache"
|
|
|
|
# Create fips group if it doesn't exist
|
|
if ! dscl . -read /Groups/fips > /dev/null 2>&1; then
|
|
dscl . -create /Groups/fips RecordName fips
|
|
dscl . -create /Groups/fips PrimaryGroupID 999
|
|
log "created group fips"
|
|
fi
|
|
|
|
# stat /dev/console gives the user logged into the GUI session —
|
|
# logname/SUDO_USER are not set in pkg postinstall context
|
|
REAL_USER="$(stat -f '%Su' /dev/console 2>/dev/null || true)"
|
|
log "console user: ${REAL_USER:-unknown}"
|
|
if [ -n "$REAL_USER" ] && [ "$REAL_USER" != "root" ]; then
|
|
if ! dscl . -read /Groups/fips GroupMembership 2>/dev/null | grep -qw "$REAL_USER"; then
|
|
dscl . -append /Groups/fips GroupMembership "$REAL_USER"
|
|
log "added $REAL_USER to group fips"
|
|
else
|
|
log "$REAL_USER already in group fips"
|
|
fi
|
|
fi
|
|
|
|
# Load the launchd service
|
|
launchctl bootout system /Library/LaunchDaemons/com.fips.daemon.plist 2>/dev/null || true
|
|
launchctl bootstrap system /Library/LaunchDaemons/com.fips.daemon.plist 2>/dev/null || true
|
|
log "launchd service loaded"
|
|
|
|
# (Re)start the BLE agent in every GUI session, so none keeps running the
|
|
# binary this install replaced: the console user's, and those of users
|
|
# logged in through fast user switching, who all appear as "console" in
|
|
# who(1). Users who log in later get it from launchd. A user added to the
|
|
# fips group above may only gain the group in a new login session, so the
|
|
# agent may not reach the daemon's socket until they log out and back in.
|
|
for AGENT_USER in $(who | awk '$2 == "console" { print $1 }' | sort -u); do
|
|
[ "$AGENT_USER" = "root" ] && continue
|
|
AGENT_UID="$(id -u "$AGENT_USER" 2>/dev/null)" || continue
|
|
launchctl bootout "gui/$AGENT_UID/com.fips.ble-agent" 2>/dev/null || true
|
|
launchctl bootstrap "gui/$AGENT_UID" /Library/LaunchAgents/com.fips.ble-agent.plist 2>/dev/null || true
|
|
log "BLE agent loaded for $AGENT_USER"
|
|
done
|
|
|
|
log "postinstall complete"
|
|
exit 0
|
|
POSTINSTALL
|
|
chmod +x "${SCRIPTS_DIR}/postinstall"
|
|
|
|
# Create preinstall script (stop service before upgrade)
|
|
cat > "${SCRIPTS_DIR}/preinstall" <<'PREINSTALL'
|
|
#!/bin/sh
|
|
# Stop service before upgrade
|
|
launchctl bootout system /Library/LaunchDaemons/com.fips.daemon.plist 2>/dev/null || true
|
|
# Stop the BLE agent in every GUI session; postinstall starts the new one.
|
|
for AGENT_USER in $(who | awk '$2 == "console" { print $1 }' | sort -u); do
|
|
[ "$AGENT_USER" = "root" ] && continue
|
|
AGENT_UID="$(id -u "$AGENT_USER" 2>/dev/null)" || continue
|
|
launchctl bootout "gui/$AGENT_UID/com.fips.ble-agent" 2>/dev/null || true
|
|
done
|
|
exit 0
|
|
PREINSTALL
|
|
chmod +x "${SCRIPTS_DIR}/preinstall"
|
|
|
|
# Build the .pkg
|
|
mkdir -p "${DEPLOY_DIR}"
|
|
pkgbuild \
|
|
--root "${STAGING_DIR}" \
|
|
--scripts "${SCRIPTS_DIR}" \
|
|
--identifier com.fips.pkg \
|
|
--version "${VERSION}" \
|
|
--ownership recommended \
|
|
"${DEPLOY_DIR}/${PKG_NAME}.pkg"
|
|
|
|
echo ""
|
|
echo "Package built: deploy/${PKG_NAME}.pkg"
|
|
ls -lh "${DEPLOY_DIR}/${PKG_NAME}.pkg"
|
|
echo ""
|
|
echo "Install with: sudo installer -pkg deploy/${PKG_NAME}.pkg -target /"
|
|
echo "Remove with: sudo packaging/macos/uninstall.sh"
|