edmund-build-and-env
GitHubEdmund macOS应用构建与环境配置指南,涵盖SwiftPM依赖、Xcode版本要求及build-app.sh脚本详解,解决构建失败、代码陈旧及LaTeX渲染崩溃等问题。
Trigger Scenarios
Install
npx skills add I7T5/Edmund --skill edmund-build-and-env -g -y
SKILL.md
Frontmatter
{
"name": "edmund-build-and-env",
"description": "Build, environment, and toolchain runbook for the Edmund repo (native macOS Markdown editor; AppKit + TextKit 2, SwiftPM). Load this skill when: setting up the environment from scratch (fresh clone, new machine, CI mirror); a build fails or a \"successful\" build behaves stale (change \"doesn't take\", old code runs, Build complete! but nothing changed); the app crashes on launch or the instant it renders LaTeX; you need to construct the debug bundle (EdmundDbg.app) for live runs; or BEFORE trusting any binary you just built for a visual check or repro run. Covers swift build\/test, build-app.sh anatomy (codesign sealing order, SwiftMath bundle placement), the stale-build disease and its detection, safe launch\/kill hygiene around the user's live instance, and the CI environment."
}
Edmund — build & environment runbook
All paths relative to the repo root. Facts date-stamped 2026-07-05 are volatile — re-verify per the last section.
When NOT to use this skill
| You actually need | Go to |
|---|---|
| The storage==rawSource / TextKit-2-only invariants, render pipeline | edmund-architecture-contract |
| Branch/commit/PR rules, what you may touch | edmund-change-control |
| Diagnosing a bug (not the build) | edmund-debugging-playbook |
| ReproScript / CGEvent live-repro driving | edmund-live-repro-and-diagnostics |
| Launch flags, defaults keys, settings | edmund-config-and-flags |
| Cutting a release, DMG, Sparkle appcast | edmund-release-and-operate |
| Screencapture verification method, test policy | edmund-validation-and-qa |
1. Environment from scratch
Requirements (verified 2026-07-05):
| Tool | Version | Why | Check |
|---|---|---|---|
| macOS | 14+ | Package.swift platforms .macOS(.v14) |
sw_vers |
| Xcode | 16+ (full Xcode, not just CLT) | swift-tools-version: 6.0; build-app.sh needs actool |
swift --version (local: Swift 6.0.3) |
| gh CLI | any recent | releases, PR ops | gh --version |
| Node | ≥20 | releases only | node --version |
| create-dmg | npm package | releases only | npm install --global create-dmg |
create-dmg trap: install via npm, NOT Homebrew. brew install create-dmg
is a different tool with an incompatible CLI. Not needed for dev work —
only for cutting releases (see edmund-release-and-operate).
Dependencies are fetched by SPM on first build — nothing to install by hand
(verified against Package.swift / Package.resolved, 2026-07-05):
swift-markdown≥0.5.0 (CommonMark/GFM parsing; pullsswift-cmarktransitively)SwiftMath≥1.7.0 (LaTeX rendering — its resource bundle is a launch-crash landmine, §3)Sparkle≥2.6.0 (auto-update — its framework is a dyld-abort landmine, §4)
Two SPM targets: EdmundCore (library + all tests; most work happens here)
and edmd (the app shell executable). The binary is named edmd even
though the app presents as "Edmund" — deliberate, see the comment in
Package.swift.
2. Core commands
swift build # debug build of both targets
swift test # full suite: ~750+ tests, ~10s (2026-07-05)
swift test --filter Callout # one suite
./scripts/build-app.sh # release build → build/Edmund.app (SANDBOXED; launch via `open -n -a`, not by binary path)
./scripts/build-app.sh --variant adhoc # unsandboxed — required for ReproScript / CGEvent live-repro runs
./scripts/build-app.sh --variant mas # Mac App Store build: EDMUND_MAS=1 drops Sparkle from the edmd target
swift test also runs automatically as a Stop hook after code-touching turns.
3. What build-app.sh actually does (and why the order matters)
scripts/build-app.sh → build/Edmund.app. Steps, in order:
swift build -c release- Assemble
build/Edmund.app/Contents/{MacOS,Resources}; copy.build/release/edmd,Info.plist,Resources/AppIcon.icns. - Compile
Resources/Assets.xcassetswithactool(falls back to/Applications/Xcode.app/.../actoolif xcode-select points at the CLT). - Embed
Sparkle.frameworkintoContents/Frameworks/(found under.build/; SwiftPM links Sparkle but never copies the framework — without it the updater crashes on first check) andinstall_name_tool -add_rpath "@executable_path/../Frameworks"so@rpathresolves post-install. - Codesign inside-out: Sparkle.framework first (nested XPC helpers must
be signed before macOS will launch them), then the three appexes
(Quick Look preview, thumbnail, Open in Edmund folder action), each with its
own entitlements (
Resources/{QuickLook,Thumbnail,OpenFolder}.entitlements) (then copy.build/release/*.bundleinto each appex root, for the same reason as step 6), then the outer.appwithout--deep(entitlements and identifier vary by variant: sparkle, mas, adhoc;--deepwould re-sign the appexes and strip their sandbox entitlements). Sealing the bundle — not just the binary — is what Sparkle's update validator requires. - Only after sealing: copy
.build/release/*.bundle(SwiftMath's math fonts) into the.approot.
Why step 6 is last and at the root — two constraints collide:
codesignrefuses to seal a bundle with any extra item at the.approot ("unsealed contents present in the bundle root"), so the seal must happen while the root holds onlyContents/.- SwiftMath's generated
Bundle.moduleaccessor hardcodesBundle.main.bundleURL— the.approot — with only a hardcoded absolute.buildpath as fallback. So the bundle must sit at the root.
Both appexes hit the same pair: the accessor looks at each appex's root, and
the sandbox blocks the .build fallback (#388). Resolution for both: seal first,
copy after. The unsealed root items make
codesign --verify and --strict complain, but Sparkle's actual check is
non-strict and tolerates it (verified end-to-end; details in
docs/ARCHITECTURE.md §8).
Missing SwiftMath bundle = instant crash the moment the app renders any
LaTeX. App launches fine, opens documents fine, dies on the first math
block. If you see that crash, check ls build/Edmund.app/*.bundle first.
4. Debug bundle fast path (EdmundDbg.app)
For live runs of a debug build, skip build-app.sh and hand-assemble
(from docs/dev-guides/live-repro-guide.md §4):
swift build
mkdir -p build/EdmundDbg.app/Contents/MacOS
cp Info.plist build/EdmundDbg.app/Contents/
cp .build/arm64-apple-macosx/debug/edmd build/EdmundDbg.app/Contents/MacOS/
cp -R .build/arm64-apple-macosx/debug/Sparkle.framework build/EdmundDbg.app/Contents/MacOS/
- Sparkle.framework must sit next to the binary — dyld aborts without it.
- A bare
.build/debug/edmdruns but never creates a window. It needs the bundle (Info.plist) around it. - Launch by direct exec of the bundle binary, never
open -a:
build/EdmundDbg.app/Contents/MacOS/edmd /path/to/test.md \
-ApplePersistenceIgnoreState YES &
LaunchServices (open -a) can silently run a stale cached/translocated
copy — you'd be executing last hour's code. Direct exec runs exactly the
binary you just copied. -ApplePersistenceIgnoreState YES stops state
restoration from reopening previous (possibly mutated) documents.
- Recreate the test document fresh before every run — autosave mutates it.
5. THE STALE BUILD DISEASE
The single most expensive trap in this repo: it has produced entire wrong debugging conclusions ("my fix doesn't work" when the fix was never in the binary).
Symptom: swift build prints Build complete! having compiled a changed
file but not relinked edmd. The app then runs old code. Release builds
(swift build -c release / build-app.sh) reuse stale objects too.
Detection — before trusting ANY binary you just built:
# 1. Grep for a LONG string literal unique to your new code:
strings .build/arm64-apple-macosx/debug/edmd | grep 'your long unique literal'
# 2. Hash before/after the build:
shasum .build/arm64-apple-macosx/debug/edmd
Literal length matters: string literals ≤15 bytes are stored inline in
the Mach-O on arm64 and never appear in strings output. A short probe
literal gives a false "stale" verdict. Use a long one (a distinctive log
message works well).
Cure:
swift package clean # first resort
rm -rf .build # visual change "doesn't take" → nuke it all
Never hand-delete .build/…/edmd.build/ — that corrupts SwiftPM's
output-file-map and wedges the target until a full clean anyway.
6. Running for visual checks — launch/kill hygiene
The user's daily-driver app has the same binary name (edmd). A blanket
pkill -x edmd kills their live session. Always, in order:
# 1. Who is running, and since when?
pgrep -lx edmd
ps -o lstart=,command= -p <pid>
# 2. Kill ONLY your own PID — or, if you launched the debug bundle:
pkill -f EdmundDbg
Other run gotchas:
open Edmund.appforegrounds a running instance instead of relaunching — you'll be looking at the old binary. Kill your instance first or direct-exec the binary.- Always pass
-ApplePersistenceIgnoreState YES(see §4). - After many rapid launch/kill cycles the window server can glitch (tiny
windows, broken state restoration):
rm -rf ~/Library/"Saved Application State"/com.i7t5.edmund.savedStateand relaunch. - Verification method (window-id screencapture, offscreen render fallback):
edmund-validation-and-qaanddocs/ARCHITECTURE.md§8.
7. CI environment
.github/workflows/ci.yml (verified 2026-07-05): runs swift test on
macos-14 with latest-stable Xcode (Swift 6.0 needs Xcode 16+), triggered
on PRs and pushes to main.
-
Zero warnings: the
Teststep teesswift testtotest.log(shell: bash, sopipefailkeeps a failing run red), and theNo warningsstep fails the job on any(Sources|Tests)/…: warning:line. A grep, not-warnings-as-errors:-Xswiftcwould also reach the remote dependencies. -
SPM cache:
.buildis cached keyed onspm-v2-${{ runner.os }}-${{ hashFiles('Package.resolved') }}. Thev2token exists because the repo renamemd→Edmundchanged the checkout path and invalidated absolute paths baked into the cached module cache — bump the token to discard a poisoned cache. -
Concurrency:
cancel-in-progress: trueper branch/PR — private-repo macOS minutes bill at 10x, so superseded commits' runs are cancelled. -
Release pipeline (
release.yml, tag-triggered) is a separate beast:edmund-release-and-operate/docs/ARCHITECTURE.md§13.
8. Checklists
Fresh clone to green
-
sw_vers— macOS 14+;swift --version— Swift 6.x (Xcode 16+) -
git clone+cd Edmund -
swift build— SPM fetches swift-markdown, SwiftMath, Sparkle -
swift test— ~750+ tests green in ~10s - Read
docs/ARCHITECTURE.md(mandated by CLAUDE.md before non-trivial work) - Visual work planned?
./scripts/build-app.sh, confirmbuild/Edmund.appexists andls build/Edmund.app/*.bundleshows the SwiftMath bundle - Releases planned?
node --version≥20,npm install --global create-dmg
Before trusting any run
- Binary is fresh:
strings <binary> | grep '<long unique literal>'and/orshasumchanged since the edit (§5) -
pgrep -lx edmd— user's live instance identified; you will kill only your own PID (§6) - Launched by direct exec, not
open -a(§4) -
-ApplePersistenceIgnoreState YESpassed - Test document recreated fresh (autosave mutated the last one)
- Debug bundle: Sparkle.framework sits next to the binary
- Release bundle: SwiftMath
*.bundleat the.approot (or the first LaTeX render crashes)
Provenance and maintenance
Every claim above was read from the files below on 2026-07-05. Re-verify:
- Toolchain/deps:
cat Package.swift(tools-version, platforms, dep versions);grep identity Package.resolved - Build anatomy:
cat scripts/build-app.sh(step order, sealing comments) - Test count/time:
swift test 2>&1 | tail -3 - Debug bundle recipe:
docs/dev-guides/live-repro-guide.md§4 - Stale-build disease, launch gotchas:
docs/ARCHITECTURE.md§8 ("Stale release builds", "open Edmund.app", savedState) - CI facts:
cat .github/workflows/ci.yml(cache key comment, concurrency) - create-dmg quirks:
docs/ARCHITECTURE.md§8 ("create-dmg — npm only")
If a command here disagrees with those files, the files win — update this skill.
Version History
- 8c5db2e Current 2026-11-07 13:25
-
ae86e2a
2026-11-02 12:48
修正Quick Look扩展资源包路径错误,将SwiftPM资源包移至扩展根目录以修复崩溃;更新文档反映双扩展架构变更。
-
21c534c
2026-09-27 14:16
修复日志路径变更导致的grep-trace.sh失效;记录测试管道需启用pipefail以防止错误被掩盖的规则。
-
fe195bc
2026-09-22 03:21
新增Mac App Store构建变体(--variant mas)支持,优化Sparkle依赖处理与签名逻辑;完善沙盒文件夹访问权限管理机制。
- 4638fdb 2026-07-11 18:03


