From 85836a9927633efc28150e18c222fc9e740a4a98 Mon Sep 17 00:00:00 2001 From: Son Date: Sat, 27 Jun 2026 09:26:00 +0900 Subject: [PATCH] fix: resolve Highlightr Bundle.module in packaged .app (editor crash) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1 shipped the SPM resource bundle in Contents/Resources, but the swift-build Bundle.module accessor looks at the .app root (unsignable) and a baked .build path (absent on user machines), never Contents/Resources — so source/split editor render still traps with SIGTRAP on 1.4.7. - package_app.sh: repoint the baked fallback path to the shipped Contents/Resources bundle before codesign (scripts/fix-highlightr-bundle.py). - FIX_FOR_CLAUDE_CODE.md: root cause, reproduce, fix options, regression test. Co-Authored-By: Claude Opus 4.8 (1M context) --- FIX_FOR_CLAUDE_CODE.md | 124 +++++++++++++++++++++++++++++++ scripts/fix-highlightr-bundle.py | 61 +++++++++++++++ scripts/package_app.sh | 14 ++++ 3 files changed, 199 insertions(+) create mode 100644 FIX_FOR_CLAUDE_CODE.md create mode 100644 scripts/fix-highlightr-bundle.py diff --git a/FIX_FOR_CLAUDE_CODE.md b/FIX_FOR_CLAUDE_CODE.md new file mode 100644 index 0000000..1ab01cf --- /dev/null +++ b/FIX_FOR_CLAUDE_CODE.md @@ -0,0 +1,124 @@ +# Fix: source/split editor still crashes after #1 — Highlightr `Bundle.module` lookup + +> **Claude Code–ready.** Open this repo in Claude Code and say: +> *"Implement the fix in FIX_FOR_CLAUDE_CODE.md and prove it with the regression test."* +> Everything needed to reproduce, fix, and verify is below. + +## Status + +PR #1 (*"bundle Highlightr SPM resources"*, merged) copies `Highlightr_Highlightr.bundle` +into `Contents/Resources/`. That is **necessary but not sufficient** — the app still hard-crashes +(`SIGTRAP`) the instant the **source/split editor** renders, because the SwiftPM-generated +`Bundle.module` accessor never looks in `Contents/Resources`. + +## Symptom + +- Preview-only use looks fine (the editor `NSView` is never instantiated). +- Switching to **Source (⌘1) / Split (⌘2)**, or restoring a window that had the editor open, crashes immediately. +- `EXC_BREAKPOINT (SIGTRAP)` — a Swift `fatalError`. + +Top of the crash stack: + +``` +_assertionFailure +closure #1 … static Bundle.module +Highlightr.init(highlightPath:) // src/classes/Highlightr.swift:56 → let bundle = Bundle.module +SyntaxHighlighter.applyHighlights(…) +MarkdownTextEditor.makeNSView(context:) +``` + +`Highlightr.init` calls `Bundle.module` **unconditionally** (line 56) before it looks at +`highlightPath`, so passing a path cannot avoid it. + +## Reproduce + +```bash +printf '# t\n```swift\nlet x = 1\n```\n' > /tmp/t.md +open -a /Applications/CmdMD.app /tmp/t.md +osascript -e 'tell application "CmdMD" to activate' \ + -e 'tell application "System Events" to keystroke "1" using command down' # Source view +# → process dies; a new ~/Library/Logs/DiagnosticReports/CmdMD-*.ips appears +``` + +## Root cause + +The toolchain emits the two-path accessor in +`.build/…/Highlightr.build/DerivedSources/resource_bundle_accessor.swift`: + +```swift +let mainPath = Bundle.main.bundleURL.appendingPathComponent("Highlightr_Highlightr.bundle").path +let buildPath = "/Users/runner/work/CmdMD/CmdMD/.build/arm64-apple-macosx/release/Highlightr_Highlightr.bundle" +guard let bundle = Bundle(path: mainPath) ?? Bundle(path: buildPath) else { Swift.fatalError(…) } +``` + +For a packaged `.app`, `Bundle.main.bundleURL` is the **`.app` root**, therefore: + +- `mainPath` = `…/CmdMD.app/Highlightr_Highlightr.bundle` — the app root, **outside `Contents/`**. + macOS code signing forbids resources there (`codesign: unsealed contents present in the bundle root`), + so the bundle can never legally live at this path in a signed app. +- `buildPath` = the CI's `.build` directory, which does not exist on a user's machine. + +Both candidates fail → `fatalError`. The accessor **never checks `Bundle.main.resourceURL`** +(= `Contents/Resources`), which is exactly where #1 put the bundle. That is why the crash persists. + +Evidence — a minimal `.app` with the same layout prints: + +``` +Bundle.main.bundleURL = …/X.app +accessor mainPath = …/X.app/Highlightr_Highlightr.bundle ← app root (unsignable) +``` + +## Fix + +Two options. **(A)** is verified and minimal; **(B)** is the clean, location-independent long-term fix. + +### (A) Verified, minimal — repoint the dead `buildPath` fallback (this PR) + +The accessor falls back to `Bundle(path: buildPath)`. Rewrite `buildPath` *in the built binary* +to the bundle #1 already ships in `Contents/Resources`, **before** codesign re-seals it. + +`scripts/fix-highlightr-bundle.py` (included) does an in-place, equal-length, null-padded +string replacement (Mach-O offsets preserved): + +``` +old (any builder): …/.build//release/Highlightr_Highlightr.bundle +new: /Applications/CmdMD.app/Contents/Resources/Highlightr_Highlightr.bundle +``` + +`package_app.sh` runs it right after the executable + bundles are staged and just before +`codesign --force --deep --sign -`. Verified on an installed `1.4.7`: Source / Split / Preview +all render, **zero** new crash reports. + +**Caveat:** this assumes the documented `/Applications` install location (the README already +instructs dragging to `/Applications`). An `.app` run from elsewhere still won't resolve — use (B) +for full independence. + +### (B) Robust, recommended — build the `.app` with Xcode / `xcodebuild` + +An Xcode app target emits the multi-candidate resource accessor (it checks +`Bundle.main.resourceURL`), so the `Contents/Resources` bundle from #1 resolves with no patching +and no path assumptions. Replace `swift build` + manual `package_app.sh` staging with an +`xcodebuild` app target. + +## Regression test (acceptance criteria) + +A fix is complete only when this prints `PASS` and produces **no** new crash report, **and** a code +block visibly renders with syntax colors in the editor (proves the bundle actually loaded — not just +"did not crash"): + +```bash +printf '# t\n```swift\nlet x = 1\n```\n' > /tmp/t.md +open -a /Applications/CmdMD.app /tmp/t.md +osascript -e 'tell application "CmdMD" to activate' \ + -e 'tell application "System Events" to keystroke "1" using command down' \ + -e 'tell application "System Events" to keystroke "2" using command down' +sleep 3 +pgrep -x CmdMD >/dev/null && echo PASS || echo FAIL +``` + +## Verified here + +- Reproduced on `1.4.6 (12)` and `1.4.7 (13)`, macOS 26.5.1, Apple Silicon. +- Confirmed the bundle is present in `Contents/Resources` yet `Bundle.module` still fails — so #1 alone is insufficient. +- Applied (A) to an installed `1.4.7`: Source / Split / Preview render, **0** new crash reports. +- (B) is recommended but not built/verified here. diff --git a/scripts/fix-highlightr-bundle.py b/scripts/fix-highlightr-bundle.py new file mode 100644 index 0000000..c6c97e0 --- /dev/null +++ b/scripts/fix-highlightr-bundle.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Repoint Highlightr's Bundle.module fallback path inside a built CmdMD binary. + +The SwiftPM-generated accessor resolves the resource bundle as: + + Bundle(path: Bundle.main.bundleURL + "Highlightr_Highlightr.bundle") # = .app ROOT (unsignable) + ?? Bundle(path: "<...>/.build//release/Highlightr_Highlightr.bundle") # = CI path (absent for users) + +In a packaged .app both candidates fail, so Highlightr.init() hits fatalError the moment the +editor renders. This patches the second (baked) string to the bundle that package_app.sh ships in +Contents/Resources, so the fallback resolves at runtime. + +It is an in-place, equal-length, NUL-padded replacement, so all Mach-O offsets are preserved. +Run it BEFORE codesign; the subsequent `codesign --force --deep --sign -` re-seals the binary. + +usage: fix-highlightr-bundle.py + +NOTE: the replacement path assumes the documented /Applications install location. For full +install-location independence, build the .app with an Xcode/xcodebuild app target instead (its +resource accessor checks Bundle.main.resourceURL, i.e. Contents/Resources). +""" +import sys + +NEW = b"/Applications/CmdMD.app/Contents/Resources/Highlightr_Highlightr.bundle" +NEEDLE = b"Highlightr_Highlightr.bundle" + + +def find_buildpath(data: bytearray): + """Return (start, end) of the baked '<...>/.build/.../Highlightr_Highlightr.bundle' C string.""" + pos = 0 + while True: + i = data.find(NEEDLE, pos) + if i == -1: + return None + start = data.rfind(b"\x00", 0, i) + 1 # C string begins after the previous NUL + end = data.find(b"\x00", i) + cstr = bytes(data[start:end]) + # the build-path literal contains ".build/"; the bare-name literal used by mainPath does not + if b"/.build/" in cstr and cstr.endswith(NEEDLE): + return start, end + pos = i + len(NEEDLE) + + +def main(bin_path: str) -> None: + data = bytearray(open(bin_path, "rb").read()) + hit = find_buildpath(data) + if hit is None: + sys.exit("buildPath string not found (already patched or unexpected build layout)") + start, end = hit + old_len = end - start + if len(NEW) > old_len: + sys.exit(f"replacement path ({len(NEW)}B) longer than original ({old_len}B); cannot patch in place") + data[start:end] = NEW + b"\x00" * (old_len - len(NEW)) + open(bin_path, "wb").write(data) + print(f"patched buildPath @ offset {start} -> {NEW.decode()}") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit(__doc__) + main(sys.argv[1]) diff --git a/scripts/package_app.sh b/scripts/package_app.sh index 1dcfecb..a06105d 100755 --- a/scripts/package_app.sh +++ b/scripts/package_app.sh @@ -45,6 +45,20 @@ for bundle in "${resource_bundles[@]}"; do cp -R "$bundle" "$RESOURCES_DIR/" done +# The copy above is necessary but NOT sufficient. With `swift build`, Highlightr's +# generated `Bundle.module` accessor resolves the bundle from `Bundle.main.bundleURL` +# (the .app ROOT, where code signing forbids resources) and from a baked `.build` path +# (absent on user machines) — it never checks Contents/Resources. So the app still traps +# on the first code-block highlight (editor render). Repoint the baked fallback path to +# the shipped Contents/Resources bundle, before codesign re-seals the binary. +# See FIX_FOR_CLAUDE_CODE.md. Long-term fix: build via an Xcode/xcodebuild app target. +if command -v python3 >/dev/null 2>&1; then + python3 "$(dirname "$0")/fix-highlightr-bundle.py" "$EXECUTABLE" \ + || echo "Warning: Highlightr bundle-path patch failed; editor view may still crash." >&2 +else + echo "Warning: python3 not found; skipping Highlightr bundle-path patch." >&2 +fi + if [[ -f "$APP_ICON" ]]; then cp "$APP_ICON" "$RESOURCES_DIR/AppIcon.icns" else