Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,8 @@ jobs:
cargo test --manifest-path rust/Cargo.toml --test legacy_parity
cargo test --manifest-path rust/Cargo.toml --test security_regressions

- name: Build release binary
run: cargo build --manifest-path rust/Cargo.toml --release

- name: Build native app bundle
run: ./scripts/build-native-app.sh
- name: Build universal release binaries
run: ./scripts/build-universal-release.sh

- name: Build local source tarball for Homebrew smoke
id: source_tarball
Expand Down Expand Up @@ -99,9 +96,6 @@ jobs:
./rust/target/release/apw --version
./rust/target/release/apw status --json

- name: Build native app bundle
run: ./scripts/build-native-app.sh

- name: Homebrew smoke install from source tarball
env:
APW_SRC_TARBALL: ${{ steps.source_tarball.outputs.path }}
Expand Down Expand Up @@ -150,6 +144,9 @@ jobs:
tar -tzf "$archive" | grep -qx 'APW.app/Contents/Info.plist'
tar -tzf "$archive" | grep -qx 'APW.app/Contents/MacOS/APW'

- name: Validate universal release binaries
run: ./scripts/verify-universal-binaries.sh

- name: Publish release assets
uses: softprops/action-gh-release@v2
with:
Expand Down
22 changes: 20 additions & 2 deletions docs/INSTALLATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,14 @@ The resulting binary is:
rust/target/release/apw
```

To build release-equivalent macOS artifacts from source, including universal
`arm64` and `x86_64` slices for both the CLI and `APW.app`, run:

```bash
./scripts/build-universal-release.sh
./scripts/verify-universal-binaries.sh
```

### Install manually

```bash
Expand All @@ -54,6 +62,16 @@ apw
APW.app/
```

Release archives are built as universal macOS artifacts. Verify the architecture
slices after extracting an archive with:

```bash
lipo -archs ./apw
lipo -archs ./APW.app/Contents/MacOS/APW
```

Both commands must include `arm64` and `x86_64`.

Extract the archive and keep `apw` beside `APW.app` while installing the
per-user app bundle:

Expand Down Expand Up @@ -233,8 +251,8 @@ cargo fmt --manifest-path rust/Cargo.toml -- --check
cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings
cargo test --manifest-path rust/Cargo.toml --all-targets
cargo test --manifest-path rust/Cargo.toml --test native_app_e2e
cargo build --manifest-path rust/Cargo.toml --release
./scripts/build-native-app.sh
./scripts/build-universal-release.sh
./scripts/verify-universal-binaries.sh
```

Optional parity and release helpers:
Expand Down
4 changes: 2 additions & 2 deletions docs/SECURITY_POSTURE_AND_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings
cargo test --manifest-path rust/Cargo.toml --all-targets
cargo test --manifest-path rust/Cargo.toml --test legacy_parity
cargo test --manifest-path rust/Cargo.toml --test native_app_e2e
cargo build --manifest-path rust/Cargo.toml --release
./scripts/build-native-app.sh
./scripts/build-universal-release.sh
./scripts/verify-universal-binaries.sh
```

## Security-focused regression coverage
Expand Down
38 changes: 28 additions & 10 deletions scripts/build-apw-release.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ set -euo pipefail

print_help() {
cat <<'EOF'
Usage: ./scripts/build-apw-release.sh [--install|--no-install] [--brew-smoke|--no-brew-smoke]
Usage: ./scripts/build-apw-release.sh [--universal|--no-universal] [--install|--no-install] [--brew-smoke|--no-brew-smoke]
[--install-dir /usr/local/bin] [--skip-version]
[--archive-smoke|--no-archive-smoke]

Options:
--universal build universal arm64 + x86_64 release binaries (default)
--no-universal build only the host architecture
--install install apw to --install-dir (defaults to /usr/local/bin)
--no-install skip installation (default)
--install-dir PATH destination directory for installation (default /usr/local/bin)
Expand All @@ -31,6 +33,7 @@ CARGO_MANIFEST="$ROOT_DIR/rust/Cargo.toml"
APP_BUNDLE_PATH="$ROOT_DIR/native-app/dist/APW.app"
VERSION="$(awk -F ' = ' '/^version = / {gsub(/"/, "", $2); print $2; exit}' "$CARGO_MANIFEST")"
ARCHIVE_PATH="$ROOT_DIR/dist/apw-macos-v${VERSION}.tar.gz"
UNIVERSAL_BUILD=1
INSTALL_BIN=0
INSTALL_DIR="/usr/local/bin"
BREW_SMOKE=0
Expand All @@ -44,6 +47,12 @@ fi

while [[ $# -gt 0 ]]; do
case "$1" in
--universal)
UNIVERSAL_BUILD=1
;;
--no-universal)
UNIVERSAL_BUILD=0
;;
--install)
INSTALL_BIN=1
;;
Expand Down Expand Up @@ -97,15 +106,19 @@ if [ ! -f "$CARGO_MANIFEST" ]; then
exit 1
fi

printf '\n[1/5] Building APW app bundle...\n'
cd "$ROOT_DIR"
"$ROOT_DIR/scripts/build-native-app.sh"

printf '\n[2/5] Building release binary...\n'
cd "$ROOT_DIR"
cargo build --manifest-path "$CARGO_MANIFEST" --release
if [ "$UNIVERSAL_BUILD" -eq 1 ]; then
printf '\n[release] Building universal release binaries...\n'
"$ROOT_DIR/scripts/build-universal-release.sh"
else
printf '\n[release] Building APW app bundle...\n'
"$ROOT_DIR/scripts/build-native-app.sh"

printf '\n[release] Building release binary...\n'
cargo build --manifest-path "$CARGO_MANIFEST" --release
fi

printf '\n[3/5] Packaging release archive...\n'
printf '\n[release] Packaging release archive...\n'
rm -rf "$ROOT_DIR/dist/apw" "$ROOT_DIR/dist/APW.app"
mkdir -p "$ROOT_DIR/dist"
cp "$BIN_PATH" "$ROOT_DIR/dist/apw"
Expand All @@ -115,13 +128,18 @@ rm -rf "$ROOT_DIR/dist/apw" "$ROOT_DIR/dist/APW.app"
echo "Created: $ARCHIVE_PATH"

if [ "$SKIP_VERSION_CHECK" -ne 1 ]; then
printf '\n[4/5] Validating binary health...\n'
printf '\n[release] Validating binary health...\n'
"$BIN_PATH" --version
"$BIN_PATH" status --json
fi

if [ "$UNIVERSAL_BUILD" -eq 1 ]; then
printf '\n[release] Validating universal binary slices...\n'
"$ROOT_DIR/scripts/verify-universal-binaries.sh" "$BIN_PATH" "$APP_BUNDLE_PATH/Contents/MacOS/APW"
fi

if [ "$ARCHIVE_SMOKE" -eq 1 ]; then
printf '\n[5/5] Validating release archive smoke...\n'
printf '\n[release] Validating release archive smoke...\n'
smoke_dir="$(mktemp -d)"
smoke_home="$(mktemp -d)"
cleanup_smoke() {
Expand Down
34 changes: 32 additions & 2 deletions scripts/build-native-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,51 @@ MACOS_DIR="$CONTENTS_DIR/MacOS"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
PLIST_PATH="$CONTENTS_DIR/Info.plist"
EXECUTABLE_PATH="$PACKAGE_DIR/.build/release/$EXECUTABLE_NAME"
UNIVERSAL=0
VERSION="$(awk -F ' = ' '$1 == "version" { gsub(/"/, "", $2); print $2; exit }' "$ROOT_DIR/rust/Cargo.toml")"

while [[ $# -gt 0 ]]; do
case "$1" in
--universal)
UNIVERSAL=1
;;
-h|--help)
cat <<'HELP'
Usage: ./scripts/build-native-app.sh [--universal]

Build native-app/dist/APW.app.

Options:
--universal Build APW.app as a universal arm64 + x86_64 bundle.
HELP
exit 0
;;
*)
echo "Unknown argument: $1" >&2
exit 1
;;
esac
shift
done

if [[ -z "$VERSION" ]]; then
echo "Unable to determine APW version from rust/Cargo.toml" >&2
exit 1
fi

swift build --package-path "$PACKAGE_DIR" -c release
if [[ "$UNIVERSAL" -eq 1 ]]; then
swift build --package-path "$PACKAGE_DIR" -c release --arch arm64 --arch x86_64
EXECUTABLE_PATH="$PACKAGE_DIR/.build/apple/Products/Release/$EXECUTABLE_NAME"
else
swift build --package-path "$PACKAGE_DIR" -c release
fi

rm -rf "$APP_DIR"
mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
cp "$EXECUTABLE_PATH" "$MACOS_DIR/$EXECUTABLE_NAME"
chmod 0755 "$MACOS_DIR/$EXECUTABLE_NAME"

RESOURCE_BUNDLE="$(find "$PACKAGE_DIR/.build" -path '*/release/*.bundle' -type d -name '*NativeAppLib*.bundle' | head -n 1 || true)"
RESOURCE_BUNDLE="$(find "$PACKAGE_DIR/.build" \( -path '*/release/*.bundle' -o -path '*/Release/*.bundle' \) -type d -name '*NativeAppLib*.bundle' | head -n 1 || true)"
if [[ -n "$RESOURCE_BUNDLE" ]]; then
cp -R "$RESOURCE_BUNDLE" "$RESOURCES_DIR/$(basename "$RESOURCE_BUNDLE")"
fi
Expand Down
98 changes: 98 additions & 0 deletions scripts/build-universal-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -euo pipefail

print_help() {
cat <<'HELP'
Usage: ./scripts/build-universal-release.sh

Build universal arm64 + x86_64 release binaries:
- rust/target/release/apw
- native-app/dist/APW.app/Contents/MacOS/APW

Requires macOS, rustup, cargo, SwiftPM, lipo, and vendored OpenSSL build tools.
HELP
}

if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
print_help
exit 0
fi

CHECK_BUILD_INPUTS=0
if [[ "${1:-}" == "--check-build-inputs" ]]; then
CHECK_BUILD_INPUTS=1
shift
fi

if [[ $# -ne 0 ]]; then
echo "Unexpected arguments: $*" >&2
print_help >&2
exit 1
fi

ROOT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CARGO_MANIFEST="$ROOT_DIR/rust/Cargo.toml"
CARGO_BIN="${CARGO_BIN:-cargo}"
RUSTUP_BIN="${RUSTUP_BIN:-rustup}"
LIPO_BIN="${LIPO_BIN:-lipo}"
TARGETS=(aarch64-apple-darwin x86_64-apple-darwin)

ensure_vendored_openssl_build_inputs() {
[[ "${OSTYPE:-}" == darwin* || "${APW_FORCE_OPENSSL_INPUT_CHECK:-}" == "1" ]] || return 0

local missing=()
for tool in cc make perl; do
if ! command -v "$tool" >/dev/null 2>&1; then
missing+=("$tool")
fi
done

if ((${#missing[@]} > 0)); then
echo "Missing required tool(s) for vendored OpenSSL build: ${missing[*]}" >&2
echo "Install Xcode Command Line Tools and Perl on the macOS release runner." >&2
echo "If intentionally using system OpenSSL, set OPENSSL_NO_VENDOR=1 and provide OPENSSL_DIR/PKG_CONFIG_PATH." >&2
exit 1
fi
}

if [[ "$(uname -s)" != "Darwin" ]]; then
echo "Universal macOS release builds must run on macOS." >&2
exit 1
fi

ensure_vendored_openssl_build_inputs

if [[ "$CHECK_BUILD_INPUTS" -eq 1 ]]; then
echo "Universal release build inputs are available."
exit 0
fi

if ! command -v "$RUSTUP_BIN" >/dev/null 2>&1 && [[ -x "$HOME/.cargo/bin/rustup" ]]; then
RUSTUP_BIN="$HOME/.cargo/bin/rustup"
fi

RUSTUP_PATH="$(command -v "$RUSTUP_BIN" || true)"
if [[ "$CARGO_BIN" == "cargo" && -n "$RUSTUP_PATH" && -x "$(dirname "$RUSTUP_PATH")/cargo" ]]; then
CARGO_BIN="$(dirname "$RUSTUP_PATH")/cargo"
fi

for tool in "$CARGO_BIN" "$RUSTUP_BIN" swift "$LIPO_BIN"; do
if ! command -v "$tool" >/dev/null 2>&1; then
echo "Required tool not found: $tool" >&2
exit 1
fi
done

for target in "${TARGETS[@]}"; do
"$RUSTUP_BIN" target add "$target"
"$CARGO_BIN" build --manifest-path "$CARGO_MANIFEST" --release --target "$target"
done

"$LIPO_BIN" -create \
"$ROOT_DIR/rust/target/aarch64-apple-darwin/release/apw" \
"$ROOT_DIR/rust/target/x86_64-apple-darwin/release/apw" \
-output "$ROOT_DIR/rust/target/release/apw"
chmod 0755 "$ROOT_DIR/rust/target/release/apw"

"$ROOT_DIR/scripts/build-native-app.sh" --universal
"$ROOT_DIR/scripts/verify-universal-binaries.sh"
2 changes: 2 additions & 0 deletions scripts/ci/run-fast-checks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,7 @@ done < <(find .github/scripts scripts -type f -name '*.sh' -print0)

./scripts/test-render-homebrew-formula.sh
./scripts/test-extended-validation-config.sh
./scripts/test-verify-universal-binaries.sh
./scripts/test-universal-release-config.sh

echo "APW fast checks passed."
36 changes: 36 additions & 0 deletions scripts/test-universal-release-config.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"

if grep -Eq 'command -v brew|brew install|brew --prefix|brew list|command -v pkg-config|pkg-config --|AARCH64_APPLE_DARWIN_OPENSSL_DIR|X86_64_APPLE_DARWIN_OPENSSL_DIR' scripts/build-universal-release.sh; then
echo "Universal release build must not depend on Homebrew/pkg-config OpenSSL discovery." >&2
exit 1
fi

tmp_bin="$(mktemp -d)"
trap 'rm -rf "$tmp_bin"' EXIT

for tool in cc make perl uname; do
cat >"$tmp_bin/$tool" <<'EOF'
#!/bin/sh
if [ "$(basename "$0")" = "uname" ]; then
if [ "${1:-}" = "-s" ]; then
echo Darwin
elif [ "${1:-}" = "-m" ]; then
echo arm64
else
echo Darwin
fi
exit 0
fi
exit 0
EOF
chmod +x "$tmp_bin/$tool"
done

PATH="$tmp_bin:$PATH" APW_FORCE_OPENSSL_INPUT_CHECK=1 \
bash scripts/build-universal-release.sh --check-build-inputs >/dev/null

echo "Universal release configuration is deterministic."
Loading
Loading