Skip to content

Commit 69831ca

Browse files
authored
fix(packaging): trust SwiftPM product paths (#1478)
1 parent a2b56ea commit 69831ca

6 files changed

Lines changed: 246 additions & 80 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
- Menu bar: keep cached provider content visible while switching merged tabs so the open menu no longer flickers through an empty state.
1010
- Menu bar: handle the global open-menu shortcut synchronously so repeated presses close the tracked menu instead of queueing a delayed reopen (#1470). Thanks @Zihao-Qi!
1111
- Settings: memoize cookie cache lookups behind the "Cached: …" picker labels so opening Settings and switching panes no longer pays a synchronous Keychain read per SwiftUI body evaluation, which froze the Providers pane for seconds (#1471). Thanks @ProspectOre!
12-
- Build: resolve packaged binaries from the bin path SwiftPM reports instead of assuming the legacy `.build/<arch>-apple-macosx/<conf>/` layout, so a stale directory left behind by the older build system no longer silently shadows fresh swiftbuild products in `package_app.sh`. Thanks @ProspectOre!
12+
- Build: trust SwiftPM-reported product paths for packaged binaries, frameworks, resources, and dSYMs, failing instead of falling back to stale legacy outputs (#1473). Thanks @ProspectOre!
1313
- Menu bar: stop the provider-switcher shortcut monitor from killing the menu's event tracking session. Its event-queue peek re-entered the run loop in tracking mode, which could leave a zombie menu on screen that ignored clicks for tens of seconds (beach ball) — most often right after opening the menu or after rapid Cmd-number provider switching, with Settings… the usual victim. Peeks now run in a barren private run-loop mode, start only once the tracking session is pumping, and no longer touch mouse events. Thanks @ProspectOre!
1414

1515
## 0.34.0 — 2026-06-12

Scripts/lint.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,16 @@ check_codex_parser_hash() {
1515
"${ROOT_DIR}/Scripts/regenerate-codex-parser-hash.sh" --check
1616
}
1717

18+
check_package_product_paths() {
19+
"${ROOT_DIR}/Scripts/test_package_product_paths.sh"
20+
}
21+
1822
cmd="${1:-lint}"
1923

2024
case "$cmd" in
2125
lint)
2226
check_codex_parser_hash
27+
check_package_product_paths
2328
ensure_tools
2429
"${BIN_DIR}/swiftformat" Sources Tests --lint
2530
"${BIN_DIR}/swiftlint" --strict

Scripts/package_app.sh

Lines changed: 50 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ esac
1616

1717
# Load version info
1818
source "$ROOT/version.env"
19+
source "$ROOT/Scripts/package_product_paths.sh"
1920

2021
# Clean build only when explicitly requested (slower).
2122
if [[ "${CODEXBAR_FORCE_CLEAN:-0}" == "1" ]]; then
@@ -112,15 +113,6 @@ if [[ ! -f "$KEYBOARD_SHORTCUTS_UTIL" ]]; then
112113
fi
113114
patch_keyboard_shortcuts
114115

115-
build_product_path() {
116-
local name="$1"
117-
local arch="$2"
118-
case "$arch" in
119-
arm64|x86_64) echo ".build/${arch}-apple-macosx/$CONF/$name" ;;
120-
*) echo ".build/$CONF/$name" ;;
121-
esac
122-
}
123-
124116
# Resolve SwiftPM's current output path without relying on a fixed build-system layout.
125117
# The output variable keeps the per-arch cache in this shell instead of losing it to
126118
# command substitution.
@@ -130,7 +122,9 @@ swiftpm_bin_path() {
130122
local cache_var="SWIFTPM_BIN_PATH_${arch//[^A-Za-z0-9]/_}"
131123
if [[ -z "${!cache_var+set}" ]]; then
132124
local resolved
133-
resolved=$(swift build --show-bin-path -c "$CONF" --arch "$arch" 2>/dev/null || true)
125+
if ! resolved=$(codexbar_swiftpm_bin_path "$CONF" "$arch"); then
126+
return 1
127+
fi
134128
printf -v "$cache_var" '%s' "$resolved"
135129
fi
136130
printf -v "$output_var" '%s' "${!cache_var}"
@@ -147,24 +141,31 @@ binary_has_arch() {
147141
PRODUCT_STAGE_ROOT="$ROOT/.build/package-products/$LOWER_CONF"
148142
rm -rf "$PRODUCT_STAGE_ROOT"
149143

150-
stage_binary_products() {
144+
stage_build_products() {
151145
local arch="$1"
152-
local bin_dir stage_dir name
146+
local bin_dir stage_dir name product
153147
swiftpm_bin_path "$arch" bin_dir
154-
[[ -n "$bin_dir" ]] || return 0
155148

156149
stage_dir="$PRODUCT_STAGE_ROOT/$arch"
157150
mkdir -p "$stage_dir"
158151
for name in CodexBar CodexBarCLI CodexBarClaudeWatchdog; do
159-
if binary_has_arch "$bin_dir/$name" "$arch"; then
160-
cp "$bin_dir/$name" "$stage_dir/$name"
152+
if ! product=$(codexbar_require_product_file "$bin_dir" "$name" "$arch"); then
153+
return 1
154+
fi
155+
if ! binary_has_arch "$product" "$arch"; then
156+
echo "ERROR: ${product} does not contain required architecture: ${arch}" >&2
157+
return 1
161158
fi
159+
cp "$product" "$stage_dir/$name"
162160
done
161+
if [[ -d "$bin_dir/CodexBar.dSYM" ]]; then
162+
cp -R "$bin_dir/CodexBar.dSYM" "$stage_dir/"
163+
fi
163164
}
164165

165166
for ARCH in "${ARCH_LIST[@]}"; do
166167
swift build -c "$CONF" --arch "$ARCH"
167-
stage_binary_products "$ARCH"
168+
stage_build_products "$ARCH"
168169
done
169170

170171
APP_FINAL="$ROOT/CodexBar.app"
@@ -263,30 +264,21 @@ cat > "$APP/Contents/Info.plist" <<PLIST
263264
</plist>
264265
PLIST
265266

266-
# Resolve path to a built binary. Prefer the per-arch snapshot, then SwiftPM's
267-
# reported directory, then legacy layouts for older toolchains.
267+
# Resolve a built binary from the fresh per-arch snapshot or SwiftPM's reported directory.
268268
resolve_binary_path() {
269269
local name="$1"
270270
local arch="$2"
271271
local bin_dir candidate
272-
candidate="$PRODUCT_STAGE_ROOT/$arch/$name"
273-
if binary_has_arch "$candidate" "$arch"; then
274-
echo "$candidate"
275-
return
276-
fi
277272
swiftpm_bin_path "$arch" bin_dir
278-
if [[ -n "$bin_dir" ]] && binary_has_arch "$bin_dir/$name" "$arch"; then
279-
echo "$bin_dir/$name"
280-
return
273+
if ! candidate=$(codexbar_resolve_staged_or_reported_file \
274+
"$PRODUCT_STAGE_ROOT" "$bin_dir" "$name" "$arch"); then
275+
return 1
281276
fi
282-
candidate=$(build_product_path "$name" "$arch")
283-
if binary_has_arch "$candidate" "$arch"; then
284-
echo "$candidate"
285-
return
286-
fi
287-
if [[ "$arch" == "arm64" || "$arch" == "x86_64" ]] && binary_has_arch ".build/$CONF/$name" "$arch"; then
288-
echo ".build/$CONF/$name"
277+
if ! binary_has_arch "$candidate" "$arch"; then
278+
echo "ERROR: ${candidate} does not contain required architecture: ${arch}" >&2
279+
return 1
289280
fi
281+
echo "$candidate"
290282
}
291283

292284
verify_binary_arches() {
@@ -314,11 +306,8 @@ install_binary() {
314306
local dest="$2"
315307
local binaries=()
316308
for arch in "${ARCH_LIST[@]}"; do
317-
local src bin_dir
318-
src=$(resolve_binary_path "$name" "$arch")
319-
if [[ -z "$src" || ! -f "$src" ]]; then
320-
swiftpm_bin_path "$arch" bin_dir
321-
echo "ERROR: Missing ${name} build for ${arch} (looked in: $PRODUCT_STAGE_ROOT/$arch, $bin_dir, $(build_product_path "$name" "$arch"), .build/$CONF)" >&2
309+
local src
310+
if ! src=$(resolve_binary_path "$name" "$arch"); then
322311
exit 1
323312
fi
324313
binaries+=("$src")
@@ -420,21 +409,20 @@ install_widget_extension() {
420409

421410
install_binary "CodexBar" "$APP/Contents/MacOS/CodexBar"
422411
# Ship CodexBarCLI alongside the app for easy symlinking.
423-
if [[ -n "$(resolve_binary_path "CodexBarCLI" "${ARCH_LIST[0]}")" ]]; then
424-
install_binary "CodexBarCLI" "$APP/Contents/Helpers/CodexBarCLI"
425-
fi
412+
install_binary "CodexBarCLI" "$APP/Contents/Helpers/CodexBarCLI"
426413
# Watchdog helper: ensures `claude` probes die when CodexBar crashes/gets killed.
427-
if [[ -n "$(resolve_binary_path "CodexBarClaudeWatchdog" "${ARCH_LIST[0]}")" ]]; then
428-
install_binary "CodexBarClaudeWatchdog" "$APP/Contents/Helpers/CodexBarClaudeWatchdog"
429-
fi
414+
install_binary "CodexBarClaudeWatchdog" "$APP/Contents/Helpers/CodexBarClaudeWatchdog"
430415
install_widget_extension
416+
417+
swiftpm_bin_path "${ARCH_LIST[0]}" PREFERRED_BUILD_DIR
418+
431419
# Embed Sparkle.framework
432-
if [[ -d ".build/$CONF/Sparkle.framework" ]]; then
433-
cp -R ".build/$CONF/Sparkle.framework" "$APP/Contents/Frameworks/"
434-
chmod -R a+rX "$APP/Contents/Frameworks/Sparkle.framework"
435-
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP/Contents/MacOS/CodexBar"
436-
# Re-sign Sparkle and all nested components with Developer ID + timestamp
437-
SPARKLE="$APP/Contents/Frameworks/Sparkle.framework"
420+
SPARKLE_SOURCE=$(codexbar_require_product_directory "$PREFERRED_BUILD_DIR" Sparkle.framework packaging)
421+
cp -R "$SPARKLE_SOURCE" "$APP/Contents/Frameworks/"
422+
chmod -R a+rX "$APP/Contents/Frameworks/Sparkle.framework"
423+
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP/Contents/MacOS/CodexBar"
424+
# Re-sign Sparkle and all nested components with Developer ID + timestamp
425+
SPARKLE="$APP/Contents/Frameworks/Sparkle.framework"
438426
if [[ "$SIGNING_MODE" == "adhoc" ]]; then
439427
CODESIGN_ID="-"
440428
CODESIGN_ARGS=(--force --sign "$CODESIGN_ID")
@@ -446,19 +434,18 @@ else
446434
CODESIGN_ARGS=(--force --timestamp --options runtime --sign "$CODESIGN_ID")
447435
fi
448436
function resign() { codesign "${CODESIGN_ARGS[@]}" "$1"; }
449-
# Sign innermost binaries first, then the framework root to seal resources
450-
resign "$SPARKLE"
451-
resign "$SPARKLE/Versions/B/Sparkle"
452-
resign "$SPARKLE/Versions/B/Autoupdate"
453-
resign "$SPARKLE/Versions/B/Updater.app"
454-
resign "$SPARKLE/Versions/B/Updater.app/Contents/MacOS/Updater"
455-
resign "$SPARKLE/Versions/B/XPCServices/Downloader.xpc"
456-
resign "$SPARKLE/Versions/B/XPCServices/Downloader.xpc/Contents/MacOS/Downloader"
457-
resign "$SPARKLE/Versions/B/XPCServices/Installer.xpc"
458-
resign "$SPARKLE/Versions/B/XPCServices/Installer.xpc/Contents/MacOS/Installer"
459-
resign "$SPARKLE/Versions/B"
460-
resign "$SPARKLE"
461-
fi
437+
# Sign innermost binaries first, then the framework root to seal resources
438+
resign "$SPARKLE"
439+
resign "$SPARKLE/Versions/B/Sparkle"
440+
resign "$SPARKLE/Versions/B/Autoupdate"
441+
resign "$SPARKLE/Versions/B/Updater.app"
442+
resign "$SPARKLE/Versions/B/Updater.app/Contents/MacOS/Updater"
443+
resign "$SPARKLE/Versions/B/XPCServices/Downloader.xpc"
444+
resign "$SPARKLE/Versions/B/XPCServices/Downloader.xpc/Contents/MacOS/Downloader"
445+
resign "$SPARKLE/Versions/B/XPCServices/Installer.xpc"
446+
resign "$SPARKLE/Versions/B/XPCServices/Installer.xpc/Contents/MacOS/Installer"
447+
resign "$SPARKLE/Versions/B"
448+
resign "$SPARKLE"
462449

463450
if [[ -f "$ICON_TARGET" ]]; then
464451
cp "$ICON_TARGET" "$APP/Contents/Resources/Icon.icns"
@@ -475,11 +462,6 @@ if [[ ! -f "$APP/Contents/Resources/Icon-classic.icns" ]]; then
475462
fi
476463

477464
# SwiftPM resource bundles (e.g. KeyboardShortcuts) are emitted next to the built binary.
478-
swiftpm_bin_path "${ARCH_LIST[0]}" PREFERRED_BUILD_DIR
479-
if [[ -z "$PREFERRED_BUILD_DIR" ]]; then
480-
CODEXBAR_BINARY="$(resolve_binary_path "CodexBar" "${ARCH_LIST[0]}")"
481-
PREFERRED_BUILD_DIR="$(dirname "${CODEXBAR_BINARY:-$(build_product_path "CodexBar" "${ARCH_LIST[0]}")}")"
482-
fi
483465
shopt -s nullglob
484466
SWIFTPM_BUNDLES=("${PREFERRED_BUILD_DIR}/"*.bundle)
485467
shopt -u nullglob

Scripts/package_product_paths.sh

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env bash
2+
3+
codexbar_swiftpm_bin_path() {
4+
local conf="$1"
5+
shift
6+
local command=(swift build --show-bin-path -c "$conf")
7+
local arch
8+
for arch in "$@"; do
9+
command+=(--arch "$arch")
10+
done
11+
12+
local path
13+
if ! path=$("${command[@]}"); then
14+
echo "ERROR: SwiftPM failed to report the ${conf} product directory for: $*" >&2
15+
return 1
16+
fi
17+
if [[ -z "$path" ]]; then
18+
echo "ERROR: SwiftPM reported an empty ${conf} product directory for: $*" >&2
19+
return 1
20+
fi
21+
printf '%s\n' "$path"
22+
}
23+
24+
codexbar_require_product_file() {
25+
local bin_dir="$1"
26+
local name="$2"
27+
local arch_label="$3"
28+
local product="$bin_dir/$name"
29+
if [[ ! -f "$product" ]]; then
30+
echo "ERROR: Missing ${name} for ${arch_label} at SwiftPM-reported path: ${product}" >&2
31+
return 1
32+
fi
33+
printf '%s\n' "$product"
34+
}
35+
36+
codexbar_require_product_directory() {
37+
local bin_dir="$1"
38+
local name="$2"
39+
local context="$3"
40+
local product="$bin_dir/$name"
41+
if [[ ! -d "$product" ]]; then
42+
echo "ERROR: Missing ${name} for ${context} at SwiftPM-reported path: ${product}" >&2
43+
return 1
44+
fi
45+
printf '%s\n' "$product"
46+
}
47+
48+
codexbar_resolve_staged_or_reported_file() {
49+
local stage_root="$1"
50+
local bin_dir="$2"
51+
local name="$3"
52+
local arch="$4"
53+
local staged="$stage_root/$arch/$name"
54+
if [[ -f "$staged" ]]; then
55+
printf '%s\n' "$staged"
56+
return
57+
fi
58+
codexbar_require_product_file "$bin_dir" "$name" "$arch"
59+
}
60+
61+
codexbar_resolve_dsym_path() {
62+
local stage_root="$1"
63+
local bin_dir="$2"
64+
local app_name="$3"
65+
local arch="$4"
66+
local staged="$stage_root/$arch/${app_name}.dSYM"
67+
if [[ -d "$staged" ]]; then
68+
printf '%s\n' "$staged"
69+
return
70+
fi
71+
codexbar_require_product_directory "$bin_dir" "${app_name}.dSYM" "$arch"
72+
}

Scripts/sign-and-notarize.sh

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ APP_BUNDLE="CodexBar.app"
77
ROOT=$(cd "$(dirname "$0")/.." && pwd)
88
source "$ROOT/version.env"
99
source "$ROOT/Scripts/release_artifacts.sh"
10+
source "$ROOT/Scripts/package_product_paths.sh"
1011

1112
# Allow building a universal binary if ARCHES is provided; default to universal (arm64 + x86_64).
1213
ARCHES_VALUE=${ARCHES:-"arm64 x86_64"}
@@ -84,32 +85,47 @@ spctl -a -t exec -vv "$APP_BUNDLE"
8485
stapler validate "$APP_BUNDLE"
8586

8687
echo "Packaging dSYM"
87-
FIRST_ARCH="${ARCH_LIST[0]}"
88-
PREFERRED_ARCH_DIR=".build/${FIRST_ARCH}-apple-macosx/release"
89-
DSYM_PATH="${PREFERRED_ARCH_DIR}/${APP_NAME}.dSYM"
90-
if [[ ! -d "$DSYM_PATH" ]]; then
91-
echo "Missing dSYM at $DSYM_PATH" >&2
92-
exit 1
93-
fi
88+
DSYM_STAGE_ROOT="$ROOT/.build/package-products/release"
89+
DSYM_PATHS=()
90+
for ARCH in "${ARCH_LIST[@]}"; do
91+
STAGED_DSYM="$DSYM_STAGE_ROOT/$ARCH/${APP_NAME}.dSYM"
92+
if [[ -d "$STAGED_DSYM" ]]; then
93+
DSYM_PATHS+=("$STAGED_DSYM")
94+
continue
95+
fi
96+
BIN_DIR=$(codexbar_swiftpm_bin_path release "$ARCH")
97+
DSYM_PATHS+=("$(codexbar_resolve_dsym_path "$DSYM_STAGE_ROOT" "$BIN_DIR" "$APP_NAME" "$ARCH")")
98+
done
99+
100+
DSYM_PATH="${DSYM_PATHS[0]}"
94101
if [[ ${#ARCH_LIST[@]} -gt 1 ]]; then
95-
MERGED_DSYM_ROOT="${PREFERRED_ARCH_DIR}/${APP_NAME}.dSYM-universal"
102+
MERGED_DSYM_ROOT="${DSYM_STAGE_ROOT}/${APP_NAME}.dSYM-universal"
96103
MERGED_DSYM="${MERGED_DSYM_ROOT}/${APP_NAME}.dSYM"
97104
rm -rf "$MERGED_DSYM_ROOT"
98105
mkdir -p "$MERGED_DSYM_ROOT"
99106
cp -R "$DSYM_PATH" "$MERGED_DSYM"
100107
DWARF_PATH="${MERGED_DSYM}/Contents/Resources/DWARF/${APP_NAME}"
101108
BINARIES=()
102-
for ARCH in "${ARCH_LIST[@]}"; do
103-
ARCH_DSYM=".build/${ARCH}-apple-macosx/release/${APP_NAME}.dSYM/Contents/Resources/DWARF/${APP_NAME}"
109+
for ((index = 0; index < ${#ARCH_LIST[@]}; index++)); do
110+
ARCH="${ARCH_LIST[$index]}"
111+
ARCH_DSYM="${DSYM_PATHS[$index]}/Contents/Resources/DWARF/${APP_NAME}"
104112
if [[ ! -f "$ARCH_DSYM" ]]; then
105-
echo "Missing dSYM for ${ARCH} at $ARCH_DSYM" >&2
113+
echo "Missing fresh dSYM for ${ARCH} at: $ARCH_DSYM" >&2
114+
exit 1
115+
fi
116+
if ! lipo -archs "$ARCH_DSYM" | tr ' ' '\n' | grep -qx "$ARCH"; then
117+
echo "dSYM at ${ARCH_DSYM} does not contain required architecture: ${ARCH}" >&2
106118
exit 1
107119
fi
108120
BINARIES+=("$ARCH_DSYM")
109121
done
110122
lipo -create "${BINARIES[@]}" -output "$DWARF_PATH"
111123
DSYM_PATH="$MERGED_DSYM"
112124
fi
125+
if [[ ! -d "$DSYM_PATH" ]]; then
126+
echo "Missing dSYM at SwiftPM-reported path: $DSYM_PATH" >&2
127+
exit 1
128+
fi
113129
"$DITTO_BIN" --norsrc -c -k --keepParent "$DSYM_PATH" "$DSYM_ZIP"
114130

115131
echo "Done: $ZIP_NAME"

0 commit comments

Comments
 (0)