Skip to content

Commit 2b686f7

Browse files
committed
fix(build): Use unsigned AXe homebrew archive for Homebrew-compatible bundling
Homebrew applies ad-hoc signatures during installation, which failed with "bundle format is ambiguous (could be app or framework)" on the bundled frameworks. Two root causes: 1. cp -r (lowercase) dereferences symlinks on macOS, turning the canonical versioned-framework layout into flat copies. Fixed by using cp -R. 2. The universal AXe archive carries Developer ID signatures that conflict with Homebrew's ad-hoc signing. Now prefers the AXe homebrew archive (unsigned), falling back to universal then legacy. Adds validate_framework_bundles() to catch both regressions: verifies framework symlink structure and ensures homebrew archives are unsigned.
1 parent d7bdbee commit 2b686f7

1 file changed

Lines changed: 72 additions & 10 deletions

File tree

scripts/bundle-axe.sh

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,12 @@ if [ "$USE_LOCAL_AXE" = true ] && [ -d "$AXE_LOCAL_DIR" ] && [ -f "$AXE_LOCAL_DI
7676
for framework in .build/release/*.framework; do
7777
if [ -d "$framework" ]; then
7878
echo "📦 Copying framework: $(basename "$framework")"
79-
cp -r "$framework" "$BUNDLED_DIR/Frameworks/"
79+
cp -R "$framework" "$BUNDLED_DIR/Frameworks/"
8080

8181
# Only copy nested frameworks if they exist
8282
if [ -d "$framework/Frameworks" ]; then
8383
echo "📦 Found nested frameworks in $(basename "$framework")"
84-
cp -r "$framework/Frameworks"/* "$BUNDLED_DIR/Frameworks/" 2>/dev/null || true
84+
cp -R "$framework/Frameworks"/* "$BUNDLED_DIR/Frameworks/" 2>/dev/null || true
8585
fi
8686
fi
8787
done
@@ -95,15 +95,22 @@ else
9595
echo "📥 Downloading latest AXe release from GitHub..."
9696

9797
AXE_RELEASE_BASE_URL="https://github.com/cameroncooke/AXe/releases/download/v${PINNED_AXE_VERSION}"
98+
AXE_HOMEBREW_URL="${AXE_RELEASE_BASE_URL}/AXe-macOS-homebrew-v${PINNED_AXE_VERSION}.tar.gz"
9899
AXE_UNIVERSAL_URL="${AXE_RELEASE_BASE_URL}/AXe-macOS-v${PINNED_AXE_VERSION}-universal.tar.gz"
99100
AXE_LEGACY_URL="${AXE_RELEASE_BASE_URL}/AXe-macOS-v${PINNED_AXE_VERSION}.tar.gz"
100101

101102
# Create temp directory
102103
mkdir -p "$AXE_TEMP_DIR"
103104
cd "$AXE_TEMP_DIR"
104105

105-
# Download and extract the release
106-
if curl -fL -o "axe-release.tar.gz" "$AXE_UNIVERSAL_URL"; then
106+
# Download the release archive.
107+
# Prefer the Homebrew archive (unsigned, proper framework symlinks) so that
108+
# Homebrew can cleanly apply its own ad-hoc signatures during installation.
109+
# Fall back to universal, then legacy.
110+
if curl -fL -o "axe-release.tar.gz" "$AXE_HOMEBREW_URL"; then
111+
AXE_ARCHIVE_FLAVOR="homebrew"
112+
echo "✅ Downloaded AXe homebrew archive"
113+
elif curl -fL -o "axe-release.tar.gz" "$AXE_UNIVERSAL_URL"; then
107114
AXE_ARCHIVE_FLAVOR="universal"
108115
if [ "$(uname -s)" != "Darwin" ]; then
109116
echo "✅ Downloaded AXe universal archive for non-macOS bundling"
@@ -151,10 +158,10 @@ else
151158
mkdir -p "$BUNDLED_DIR/Frameworks"
152159

153160
if [ -d "Frameworks" ]; then
154-
cp -r Frameworks/* "$BUNDLED_DIR/Frameworks/"
161+
cp -R Frameworks/* "$BUNDLED_DIR/Frameworks/"
155162
elif [ -d "lib" ]; then
156163
# Look for frameworks in lib directory
157-
find lib -name "*.framework" -exec cp -r {} "$BUNDLED_DIR/Frameworks/" \;
164+
find lib -name "*.framework" -exec cp -R {} "$BUNDLED_DIR/Frameworks/" \;
158165
else
159166
echo "⚠️ No frameworks directory found in release archive"
160167
echo "📂 Contents of release archive:"
@@ -170,6 +177,61 @@ echo "📦 Copied $FRAMEWORK_COUNT frameworks"
170177
echo "🔍 Bundled frameworks:"
171178
ls -la "$BUNDLED_DIR/Frameworks/"
172179

180+
# Validate bundled framework structure.
181+
# Catches two regressions that break Homebrew installations:
182+
# 1. Dereferenced symlinks (e.g. cp -r instead of cp -R) cause codesign to
183+
# report "bundle format is ambiguous (could be app or framework)".
184+
# 2. Pre-signed binaries in the homebrew archive would conflict with
185+
# Homebrew's own ad-hoc signing step.
186+
validate_framework_bundles() {
187+
local has_errors=false
188+
189+
while IFS= read -r framework_path; do
190+
local fw_name="$(basename "$framework_path" .framework)"
191+
192+
# Versioned framework must have Versions/A
193+
if [ ! -d "$framework_path/Versions/A" ]; then
194+
continue
195+
fi
196+
197+
# Versions/Current must be a symlink to A
198+
if [ -d "$framework_path/Versions/Current" ] && [ ! -L "$framework_path/Versions/Current" ]; then
199+
echo "$fw_name: Versions/Current is a real directory, expected symlink to A"
200+
has_errors=true
201+
fi
202+
203+
# Top-level binary must be a symlink into Versions/Current/
204+
if [ -e "$framework_path/$fw_name" ] && [ ! -L "$framework_path/$fw_name" ]; then
205+
echo "$fw_name: top-level binary is a real file, expected symlink"
206+
has_errors=true
207+
fi
208+
209+
# For the homebrew archive, binaries must be unsigned
210+
if [ "$AXE_ARCHIVE_FLAVOR" = "homebrew" ]; then
211+
local fw_binary="$framework_path/Versions/A/$fw_name"
212+
if [ -f "$fw_binary" ] && codesign -dv "$fw_binary" >/dev/null 2>&1; then
213+
echo "$fw_name: binary has a code signature but homebrew archive should be unsigned"
214+
has_errors=true
215+
fi
216+
fi
217+
done < <(find "$BUNDLED_DIR/Frameworks" -name "*.framework" -type d -maxdepth 1)
218+
219+
if [ "$AXE_ARCHIVE_FLAVOR" = "homebrew" ] && [ -f "$BUNDLED_DIR/axe" ]; then
220+
if codesign -dv "$BUNDLED_DIR/axe" >/dev/null 2>&1; then
221+
echo "❌ axe binary has a code signature but homebrew archive should be unsigned"
222+
has_errors=true
223+
fi
224+
fi
225+
226+
if [ "$has_errors" = true ]; then
227+
echo "❌ Framework bundle validation failed"
228+
exit 1
229+
fi
230+
echo "✅ Framework bundle structure validated"
231+
}
232+
233+
validate_framework_bundles
234+
173235
ad_hoc_sign_bundled_axe_assets() {
174236
echo "🔏 Applying ad-hoc signatures to bundled AXe assets..."
175237

@@ -196,8 +258,8 @@ if [ "$OS_NAME" = "Darwin" ]; then
196258
ad_hoc_sign_bundled_axe_assets
197259
fi
198260

199-
if [ "$AXE_ARCHIVE_FLAVOR" = "universal" ]; then
200-
echo "ℹ️ Universal AXe archive detected; using ad-hoc signatures for local runtime compatibility"
261+
if [ "$AXE_ARCHIVE_FLAVOR" = "homebrew" ] || [ "$AXE_ARCHIVE_FLAVOR" = "universal" ]; then
262+
echo "ℹ️ ${AXE_ARCHIVE_FLAVOR} AXe archive detected; using ad-hoc signatures for local runtime compatibility"
201263
else
202264
echo "🔏 Verifying AXe signatures..."
203265
if ! codesign --verify --deep --strict "$BUNDLED_DIR/axe"; then
@@ -222,8 +284,8 @@ if [ "$OS_NAME" = "Darwin" ]; then
222284
done < <(find "$BUNDLED_DIR/Frameworks" -name "*.framework" -type d)
223285
fi
224286

225-
if [ "$AXE_ARCHIVE_FLAVOR" = "universal" ]; then
226-
echo "ℹ️ Skipping Gatekeeper assessment for universal AXe archive"
287+
if [ "$AXE_ARCHIVE_FLAVOR" = "homebrew" ] || [ "$AXE_ARCHIVE_FLAVOR" = "universal" ]; then
288+
echo "ℹ️ Skipping Gatekeeper assessment for ${AXE_ARCHIVE_FLAVOR} AXe archive"
227289
else
228290
echo "🛡️ Assessing AXe with Gatekeeper..."
229291
SPCTL_LOG="$(mktemp)"

0 commit comments

Comments
 (0)