Repository navigation
Expand file tree
/
Copy pathMakefile
More file actions
318 lines (292 loc) · 16.6 KB
/
Copy pathMakefile
File metadata and controls
318 lines (292 loc) · 16.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
EXEC := NextNotes
CONFIG := debug
## Build products live OUTSIDE this directory, for the same reason the .app does.
##
## ~/Desktop is iCloud/file-provider synced, and the provider mutates files inside
## .build while the compiler is using them — producing "input file was modified during
## the build" on random object files, and occasionally a wedged swift-frontend stuck at
## 0% CPU. Moving the scratch path to ~/Library/Caches (never synced) removes the race.
SCRATCH := $(HOME)/Library/Caches/NextNotesBuild/scratch
TEST_SCRATCH := $(HOME)/Library/Caches/NextNotesBuild/test-scratch
## Recursive, not `:=`, so a target-specific `CONFIG := release` actually changes
## which binary and framework get copied. Immediate expansion would bake `debug`
## in at parse time and `make release` would ship yesterday's debug build.
BUILD = $(SCRATCH)/$(CONFIG)/$(EXEC)
LLAMA_FRAMEWORK = $(SCRATCH)/$(CONFIG)/llama.framework
## The bundle is assembled and signed OUTSIDE this directory on purpose.
##
## This tree lives under ~/Desktop, which is iCloud/file-provider synced. The provider
## stamps com.apple.FinderInfo onto files inside an .app faster than we can strip them,
## and codesign hard-refuses anything carrying them ("resource fork, Finder information,
## or similar detritus not allowed"). `xattr -cr` immediately before signing is not enough
## — the provider re-stamps in between. Staging in ~/Library/Caches sidesteps it entirely.
STAGE := $(HOME)/Library/Caches/NextNotesBuild
APPNAME := Next Notes.app
## The staging bundle is **per make invocation**, not a fixed path.
##
## `app:` assembles it with `rm -rf` + a dozen `cp`s, and `install` (under
## `$(STAGE)/install.lock`) swaps it into /Applications and then deletes it. When both used
## one shared path, two concurrent `make install` runs interleaved their writes into the
## same directory *and* the winner's `rm -rf` deleted the loser's half-assembled bundle
## mid-`cp` — measured during the 2026-09-25 roadmap as three install failures in an hour,
## each of which can also install a plausible-but-wrong bundle (one run's binary, another's
## frameworks). A directory nobody else can name cannot be written by two processes.
##
## The suffix is **make's own pid**, read with `$(shell …)`. `$$` does not work here: in a
## variable assignment it expands to a literal `$`, so every invocation would share the
## directory named `install-$` — measured, the first version of this fix printed
## `built …/install-$/Next Notes.app` and staged into one shared path again. Every
## `$(shell)` child is parented to the same make, so `$$PPID` is that pid, and it is stable
## across expansions within one run and different between concurrent runs.
STAGE_RUN = $(STAGE)/install-$(shell printf %s $$PPID)
BUNDLE := $(STAGE_RUN)/$(APPNAME)
CONTENTS := $(BUNDLE)/Contents
WEBRTC_LIB_DIR := $(STAGE)/webrtc/current
## TCC keys the Accessibility grant to the code signature, so an ad-hoc signature — which
## changes on every build — makes the user re-grant after every `make`. Signing with a
## stable Developer ID keeps the identity constant and the grant sticky. Falls back to
## ad-hoc ("-") on a machine without the cert.
LOCAL_SIGN_CN := Next Notes Local Signing
SIGN_ID := $(shell security find-identity -v -p codesigning 2>/dev/null \
| grep "Developer ID Application" | head -1 | sed -E 's/.*"(.*)".*/\1/')
## Second choice: a stable self-signed certificate, created once by `make signing-cert`.
##
## This exists because ad-hoc ("-") is not merely unsigned, it is a *different identity on
## every build*: the signature is a hash of the binary, TCC stores a code-signing
## requirement next to each grant, and so Accessibility, Audio Recording and Notifications
## are all silently invalidated by every `make install`. The symptom lies — the toggle in
## System Settings still reads as on while the app is untrusted — and toggling it does not
## repair it, because the stored requirement is what is stale. A self-signed certificate is
## no more trusted than ad-hoc by Gatekeeper, but it is *constant*, so the grants stick.
ifeq ($(strip $(SIGN_ID)),)
SIGN_ID := $(shell security find-identity -v -p codesigning 2>/dev/null \
| grep "$(LOCAL_SIGN_CN)" | head -1 | sed -E 's/.*"(.*)".*/\1/')
endif
ifeq ($(strip $(SIGN_ID)),)
SIGN_ID := -
endif
## What kind of identity we landed on. The release path uses this to decide whether a
## secure timestamp and the stricter entitlements file are legal: both assume a real
## Developer ID. A local or ad-hoc signature must keep `disable-library-validation`
## and `--timestamp=none`, or the bundled llama.framework will not load.
SIGN_KIND := adhoc
ifneq ($(findstring Developer ID Application,$(SIGN_ID)),)
SIGN_KIND := developer-id
else ifneq ($(SIGN_ID),-)
SIGN_KIND := local
endif
TIMESTAMP := --timestamp=none
ENTITLEMENTS := Resources/$(EXEC).entitlements
STAMP_VERSION :=
## Disk image for GitHub Releases. The versioned name is what a human reads on the
## release page; `NextNotes.dmg` is the stable URL the website downloads from
## (`…/releases/latest/download/NextNotes.dmg`). Both are the same bytes.
## Do not use `$(...)` or `#` inside `$(shell ...)`. Make treats the first `)` as the
## end of the function and `#` as the start of a comment, which is how the first
## version of this line failed to parse.
VERSION ?= $(shell git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//')
ifeq ($(strip $(VERSION)),)
VERSION := 0.1.0
endif
BUILD_NUMBER ?= $(shell git rev-list --count HEAD 2>/dev/null || echo 1)
DMG_STAGE := $(STAGE)/dmg-root
DMG := $(STAGE)/NextNotes-$(VERSION).dmg
DMG_STABLE := $(STAGE)/NextNotes.dmg
.PHONY: all build test app run install selftest acceptance clean icon signing-cert release dmg webrtc-audio
all: app
webrtc-audio:
@mkdir -p "$(STAGE)"
@NEXTNOTES_WEBRTC_CACHE="$(STAGE)/webrtc" bash Tools/build-webrtc-audio.sh > "$(STAGE)/webrtc-build.log" 2>&1 || { cat "$(STAGE)/webrtc-build.log"; exit 1; }
build: webrtc-audio
swift build -c $(CONFIG) --scratch-path "$(SCRATCH)"
## Two suites, named explicitly rather than matched by one regex.
##
## --filter takes a regex, and a single pattern loose enough to catch both would also catch
## whatever gets added next — or miss it. Worse, a suite accidentally named
## SpokenFormsVectorTests would be swept up by "VectorTests" alone and appear to run until
## the day somebody renamed it. Each suite gets its own flag; SwiftPM ORs them.
test:
swift test --scratch-path "$(TEST_SCRATCH)" --filter VectorTests --filter SpokenFormsTests
## Regenerates AppIcon.icns from Tools/makeicon.swift. Not a dependency of `app` — the
## icon rarely changes and rendering 10 PNGs on every build is wasted time.
## Compiled with `OrbGeometry.swift` rather than run as a script, so the mark *is* the orb
## the app animates — same generator, same tuned constants — instead of a drawing of it that
## silently goes stale. `OrbGeometry` imports only CoreGraphics and Foundation and touches no
## `DS` token, which is what makes this possible.
icon:
@swiftc -O -o "$(SCRATCH)/makeicon" \
Tools/makeicon.swift \
Sources/NextNotes/UI/Components/ThinkingOrbs/OrbGeometry.swift
@"$(SCRATCH)/makeicon"
@iconutil -c icns Resources/AppIcon.iconset -o Resources/AppIcon.icns
@echo "wrote Resources/AppIcon.icns"
## Assemble a real .app bundle. TCC (microphone + Accessibility) keys on bundle identity
## and code signature, so the raw SwiftPM binary can't be used directly.
app: build
@mkdir -p "$(STAGE_RUN)"
@rm -rf "$(BUNDLE)"
@mkdir -p "$(CONTENTS)/MacOS" "$(CONTENTS)/Resources" "$(CONTENTS)/Frameworks"
@cp $(BUILD) "$(CONTENTS)/MacOS/$(EXEC)"
@cp -R "$(LLAMA_FRAMEWORK)" "$(CONTENTS)/Frameworks/"
@cp "$(WEBRTC_LIB_DIR)/libNextNotesAEC.dylib" "$(WEBRTC_LIB_DIR)/libwebrtc-audio-processing-2.1.dylib" "$(CONTENTS)/Frameworks/"
@mkdir -p "$(CONTENTS)/Resources/WebRTCAudio"
@cp "$(WEBRTC_LIB_DIR)"/*LICENSE "$(WEBRTC_LIB_DIR)"/*PATENTS "$(CONTENTS)/Resources/WebRTCAudio/"
@cp Vendor/WebRTCAudio/THIRD-PARTY-LICENSE "$(CONTENTS)/Resources/WebRTCAudio/"
@cp Resources/Info.plist "$(CONTENTS)/Info.plist"
@if [ -f Resources/AppIcon.icns ]; then cp Resources/AppIcon.icns "$(CONTENTS)/Resources/"; fi
@# The Workspace CLI installer the Settings tab opens in Terminal. A resource rather
@# than a string in Swift so the commands the user is asked to run are reviewable as a
@# script; the app falls back to a one-line equivalent when running outside a bundle.
@cp Resources/install-gws.sh "$(CONTENTS)/Resources/"
@# The persona preset `persona.md` is seeded from. PersonaStore carries a compiled-in copy
@# for bare binaries, and --selftest-persona fails if the two drift.
@cp Resources/agent-persona-base.md "$(CONTENTS)/Resources/"
@# Notion-style avatar parts (Felix Wong / Noto, CC0). See Resources/NotionAvatar/ATTRIBUTION.md.
@cp -R Resources/NotionAvatar "$(CONTENTS)/Resources/"
@cp -R Resources/MeetingEditor "$(CONTENTS)/Resources/"
@# The knowledge graph's ontology. Ontology.swift carries a compiled-in copy for bare
@# binaries, and --selftest-extract fails if the two drift.
@cp Resources/knowledge-ontology.yaml "$(CONTENTS)/Resources/"
@cp Sources/SpeexEcho/LICENSE "$(CONTENTS)/Resources/SpeexDSP-LICENSE.txt"
@# The app's own licence travels inside the bundle: an AGPL binary has to be able to
@# point whoever received it at the terms it came under.
@cp LICENSE "$(CONTENTS)/Resources/LICENSE.txt"
@cp THIRD-PARTY-NOTICES.md "$(CONTENTS)/Resources/THIRD-PARTY-NOTICES.md"
@chmod +x "$(CONTENTS)/Resources/install-gws.sh"
@printf 'APPL????' > "$(CONTENTS)/PkgInfo"
@# Belt and braces: the staging dir isn't synced, but the copied binary can still carry
@# xattrs inherited from the synced .build directory.
@xattr -cr "$(BUNDLE)"
@install_name_tool -add_rpath "@executable_path/../Frameworks" "$(CONTENTS)/MacOS/$(EXEC)"
@if [ "$(STAMP_VERSION)" = "1" ]; then \
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $(VERSION)" "$(CONTENTS)/Info.plist"; \
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $(BUILD_NUMBER)" "$(CONTENTS)/Info.plist"; \
echo "stamped $(VERSION) ($(BUILD_NUMBER))"; \
fi
@codesign --force --sign "$(SIGN_ID)" --options runtime $(TIMESTAMP) \
"$(CONTENTS)/Frameworks/llama.framework"
@codesign --force --sign "$(SIGN_ID)" --options runtime $(TIMESTAMP) \
"$(CONTENTS)/Frameworks/libwebrtc-audio-processing-2.1.dylib" "$(CONTENTS)/Frameworks/libNextNotesAEC.dylib"
@codesign --force --sign "$(SIGN_ID)" \
--entitlements "$(ENTITLEMENTS)" \
--options runtime \
$(TIMESTAMP) \
"$(BUNDLE)"
@echo "built $(BUNDLE) [signed: $(SIGN_ID)]"
@# Refresh the D-13 / D-14 evidence-gate report in the background. Detached, read-only,
@# and unable to fail this build: it reads usage.jsonl and writes three files into
@# ~/Library/Caches/NextNotesBuild/dictation-meetings/. The next agent reads
@# gates-latest.txt instead of re-deriving the gate by hand.
@if [ -x Scripts/dictation-gates.sh ]; then \
nohup Scripts/dictation-gates.sh >/dev/null 2>&1 & \
fi
## The D-13 / D-14 evidence-gate report, in the foreground, for when you want to read it now.
## `make app` already refreshes it in the background; this is the same script, attached.
gates:
@NEXTNOTES_GATES_VERBOSE=1 Scripts/dictation-gates.sh
@cat "$${NEXTNOTES_GATES_DIR:-$$HOME/Library/Caches/NextNotesBuild/dictation-meetings}/gates-latest.txt"
## Run the canonical installed copy, so LaunchServices never selects a stale
## cache bundle with the same identifier.
run: install
## Set OPEN=0 to install without launching (agents running self-tests must).
## A mid-`rm`/`cp` window used to leave `/Applications/Next Notes.app` without its
## executable: `open` then fails with kLSNoExecutableErr, and a concurrent direct
## binary launch under Cursor has aborted inside HIServices `_RegisterApplication`
## before any app code runs. The swap goes via a sibling `.new` bundle so
## LaunchServices never sees a half-deleted app.
##
## `install` publishes the staged bundle into /Applications under the same
## `$(STAGE)/install.lock` that `Scripts/run-selftest.sh` waits on, so a self-test never
## runs against a bundle that is mid-swap. The *staging* half needs no lock since
## `$(BUNDLE)` is per make invocation (see `STAGE_RUN`); the swap half does, because it
## touches the one path every self-test and every launch resolves.
OPEN ?= 1
INSTALL_LOCK := $(STAGE)/install.lock
## Ad-hoc signatures change on every rebuild, which resets the Accessibility grant.
## Installing to /Applications keeps the path stable and makes re-granting a one-click fix.
install: app
@mkdir -p "$(STAGE)"
@APPNAME="$(APPNAME)" EXEC="$(EXEC)" BUNDLE="$(BUNDLE)" STAGE="$(STAGE)" OPEN="$(OPEN)" \
Scripts/with-install-lock.sh "$(INSTALL_LOCK)" \
bash Scripts/install-bundle.sh
## Agent entry point for self-tests. Never opens the GUI from make.
## make selftest SELFTEST_ARGS='--selftest-graph-layout'
## make selftest SELFTEST_ARGS='--via-open --selftest-systemaudio'
## Prefer `Scripts/run-selftest.sh …` directly when embedding in agent shells.
SELFTEST_ARGS ?=
selftest:
@Scripts/run-selftest.sh $(SELFTEST_ARGS)
## Tiered acceptance over the whole self-test catalogue. Never builds and never installs:
## it drives the installed bundle through `Scripts/run-selftest.sh`, which waits on the
## install lock, and prints one PASS/SKIP/FAIL line per tier plus a reason for every
## failure or skip. CORE failures make it exit non-zero; a CORE skip is reported but
## never counts as a pass. See Scripts/acceptance.sh for the membership and the
## accounting rules.
## make acceptance # all three tiers (the EXPERIMENTAL sweep is long)
## make acceptance TIER=core # release blockers only
## make acceptance --dry-run # print the manifest; run nothing
## make acceptance ARGS='--only ui-strings'
## The `+` keeps `--dry-run` working: make's own -n prints the recipe but does not run
## it, and the script only receives --dry-run when make was actually asked to dry-run.
acceptance:
+@Scripts/acceptance.sh $(if $(TIER),--tier $(TIER)) $(if $(filter n,$(MAKEFLAGS)),--dry-run) $(ARGS)
## Release configuration of the same bundle `make app` builds. Stamps the version from
## the current git tag (or 0.1.0 if there isn't one). Does not pass `-DPAID_BUILD` —
## that flag is the paid-product boundary in the local roadmap/PAID-RELEASE.md plan, and a source-built
## or GitHub-hosted DMG is still the free app.
##
## A Developer ID switches on a secure timestamp and drops `disable-library-validation`,
## because both halves are then signed with the same Team ID. Without one, this is a
## release-optimized binary that Gatekeeper will still refuse to open from a download.
release: CONFIG := release
release: STAMP_VERSION := 1
ifeq ($(SIGN_KIND),developer-id)
release: TIMESTAMP := --timestamp
release: ENTITLEMENTS := Resources/$(EXEC).release.entitlements
endif
release: app
## Wrap the release bundle in a drag-to-Applications disk image. The image lives under
## ~/Library/Caches, not in the repo — committing a 50 MB binary per tag would bloat
## git permanently; GitHub Releases is where it is published.
dmg: release
@rm -rf "$(DMG_STAGE)"
@mkdir -p "$(DMG_STAGE)"
@cp -R "$(BUNDLE)" "$(DMG_STAGE)/"
@ln -s /Applications "$(DMG_STAGE)/Applications"
@rm -f "$(DMG)" "$(DMG_STABLE)"
@hdiutil create \
-srcfolder "$(DMG_STAGE)" \
-volname "Next Notes" \
-fs HFS+ \
-format UDZO \
-ov \
"$(DMG)"
@cp "$(DMG)" "$(DMG_STABLE)"
@shasum -a 256 "$(DMG)" | awk '{print $$1 " NextNotes-$(VERSION).dmg"}' > "$(DMG).sha256"
@if [ "$(SIGN_KIND)" = "developer-id" ]; then \
codesign --force --sign "$(SIGN_ID)" --timestamp "$(DMG)"; \
codesign --force --sign "$(SIGN_ID)" --timestamp "$(DMG_STABLE)"; \
fi
@echo "built $(DMG)"
@ls -lh "$(DMG)"
@cat "$(DMG).sha256"
## Creates the stable self-signed certificate the signing block above looks for.
##
## Run once per machine. It is additive and reversible: delete "Next Notes Local Signing"
## from Keychain Access to undo it. Gatekeeper trusts it no more than ad-hoc — the point is
## only that it does not change between builds, so a TCC grant given once keeps applying.
signing-cert:
@if security find-identity -v -p codesigning | grep -q "$(LOCAL_SIGN_CN)"; then \
echo "already present: $(LOCAL_SIGN_CN)"; \
else \
echo "Creating a self-signed code-signing certificate."; \
echo "Keychain Access will open. Choose:"; \
echo " Certificate Type: Code Signing"; \
echo " Name: $(LOCAL_SIGN_CN)"; \
echo " Let me override defaults: not needed"; \
open -a "Certificate Assistant" 2>/dev/null || \
open "/System/Library/CoreServices/Applications/Certificate Assistant.app" 2>/dev/null || \
echo "Open Keychain Access > Certificate Assistant > Create a Certificate."; \
fi
clean:
@rm -rf .build "$(STAGE)" "$(SCRATCH)"