Repository navigation
Expand file tree
/
Copy pathREADME
More file actions
162 lines (137 loc) · 10.4 KB
/
Copy pathREADME
File metadata and controls
162 lines (137 loc) · 10.4 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
SpacialShell dev scripts
========================
dev.sh swift build (debug) + run .build/debug/SpacialShell in the foreground.
Ctrl-C is a clean quit: SIGINT restores every managed window to the centre
of its screen before exiting (spec §7.4). One shell at a time (#131): with the
installed app running, it exits 1 at once; `spacialctl quit` that one first.
bundle.sh swift build -c release + assemble build/SpacialShell.app (Sparkle.framework embedded),
signed with the best identity available (below); hardened runtime unless ad-hoc.
package-dmg.sh bundle.sh + UDZO DMG (Applications symlink). VERSION= overrides the name and
the bundle version.
notarize.sh package-dmg.sh + sign the DMG + notarytool submit + staple + spctl check (#16).
Credentials: NOTARY_PROFILE, or ASC_KEY_PATH/ASC_KEY_ID/ASC_ISSUER_ID. Without
them it stops before submitting and says so; --require-notarization makes that
an error. One-time credential setup: docs/release.md.
sparkle-keys.sh one-time: Sparkle EdDSA keypair into the login keychain, public key into
Info.plist, --upload-secret for the private key (#58, docs/release.md).
install.sh build/SpacialShell.app into /Applications (the pre-commit hook). --relaunch also
restarts the shell on it (make install); --relaunch-only just restarts it (make
relaunch). The restart copies first, then quits, polls for the exit every 50 ms and
opens the new app at once: while no shell runs, Globe+S reaches Siri (#184).
check-target-deps.py fails if a target imports a package module it doesn't list as a
dependency, which makes incremental `swift test` segfault after a Kit struct
change (#156, docs/testing.md). Run by the pre-commit hook and CI.
e2e/ end-to-end scenarios against the real shell, on this Mac or in a Tart VM;
see e2e/README.md (#38).
TCC identity — read this before wondering why the Accessibility checkbox "doesn't work"
--------------------------------------------------------------------------------------
macOS identifies an ad-hoc-signed program to TCC (Accessibility, Input Monitoring) by
its **path** when it is a loose binary, and by its **bundle id + code signature** when it
is an .app.
* `Scripts/dev.sh` runs `.build/debug/SpacialShell`. The grant is attached to that exact
path. Move the checkout, rename the directory, or switch to a different build
configuration and the grant no longer applies — you have to remove the stale entry in
System Settings → Privacy & Security → Accessibility (select it, press "−") and grant
the new path. Nothing in the app does this for you: `Permissions.waitForAccessibility`
used to shell out to `tccutil reset Accessibility sh.emu.SpacialShell` on its first
retry, and no longer does (#19 — a sandboxed process cannot launch executables, the
stable Developer ID below removed the reason, and the reset deleted the row outright,
which dropped the app off the Accessibility list with no route back but "+"). It never
helped here anyway: a loose binary at a *new* path is a new subject, not a stale row.
What the app does now is say so — after 20 s with no grant it logs the remedy at
`.error` under category `permissions`, which is the same two clicks described below.
* `Scripts/bundle.sh` produces `build/SpacialShell.app` with a stable bundle id, so its
grant survives rebuilds in place. Re-granting is still needed if you move the .app.
* Rebuilding changes the ad-hoc signature. macOS keeps the old TCC row and silently
treats the new binary as untrusted; the symptom is a checkbox that is ticked while
`AXIsProcessTrusted()` returns false. Untick and re-tick it, or remove the row.
**Fix this once and it stops happening.** TCC keys the grant to the *designated
requirement*, so a stable one survives rebuilds where an ad-hoc one cannot. `bundle.sh`
takes the best identity available, in order: `SPACIAL_SIGN_IDENTITY`, then the hash
pinned in the gitignored `Scripts/sign-identity` (when that certificate is present on this Mac), then
**Developer ID Application**, then **Apple Development**, then a self-signed
`SpacialShell Dev`, then ad-hoc with a warning.
The pin exists because a Mac can hold two certificates with the same common name — a
renewed Developer ID gives you exactly that — and the search would otherwise take
whichever came first. Building with the other one produces an app TCC does not
recognise: the Accessibility checkbox stays ticked and `AXIsProcessTrusted()` returns
false, with nothing to explain why. A fresh clone on another Mac does not have the
pinned certificate, so it falls through to the search as before.
A Developer ID is the best of these and not only because it is Apple-issued. Its
designated requirement keys on the *team* — `certificate leaf[subject.OU] = <TEAMID>`
— so the grant survives even a certificate renewal, where a self-signed one keys on
that certificate's own hash and would not.
Timestamps: an Apple identity asks Apple's timestamp server on every sign, which needs
the network and fails the build when it is unreachable ("A timestamp was expected but
was not found"). A timestamp only matters for notarised distribution, so `bundle.sh`
passes `--timestamp=none` unless `SPACIAL_TIMESTAMP` is set; `package-dmg.sh` sets it.
Apple issues intermediates in generations and does not backfill older ones, so a certificate
can be perfectly good and still refuse to sign with `errSecInternalComponent` and "unable to
build chain to self-signed root" — it means the issuing intermediate is missing, not that the
certificate is bad. `security find-identity -v` hides such an identity while plain
`find-identity` still lists it, which is the tell. Fetch the matching intermediate from
https://www.apple.com/certificateauthority/ and `security import` it. Seen here with WWDR G3
(Mac App Store certs) and Developer ID CA G2, on a machine that only had WWDR G5.
Renewing a certificate leaves two with the same common name, and `codesign --sign` on an
ambiguous name fails outright. `bundle.sh` therefore resolves the name to a SHA-1 hash and signs
by hash, warning when several match. The Accessibility grant does not care which you use: a
Developer ID's designated requirement keys on `certificate leaf[subject.OU] = <TEAMID>`, so it
survives a renewal.
Signing is not notarising. `spctl -a` still reports `rejected / Unnotarized Developer
ID` — fine for a locally-built app, but a DMG handed to anyone else needs
`xcrun notarytool submit` and `xcrun stapler staple` first.
To create a self-signed
code-signing certificate — Keychain Access > Certificate Assistant > Create a
Certificate, name `SpacialShell Dev`, Identity Type "Self Signed Root", Certificate
Type **Code Signing**. You have to grant Accessibility one more time after switching
identity — the signature changes once — and it sticks from then on.
Or without the GUI, in one go:
openssl req -x509 -newkey rsa:2048 -keyout k.pem -out c.pem -days 3650 -nodes \
-subj "/CN=SpacialShell Dev" \
-addext "basicConstraints=critical,CA:false" \
-addext "keyUsage=critical,digitalSignature" \
-addext "extendedKeyUsage=critical,codeSigning"
openssl pkcs12 -export -legacy -out i.p12 -inkey k.pem -in c.pem \
-name "SpacialShell Dev" -passout pass:tmp
security import i.p12 -k ~/Library/Keychains/login.keychain-db -P tmp -T /usr/bin/codesign
`-legacy` is required: macOS's Security framework cannot parse OpenSSL 3's default
AES-256/SHA-256 PKCS#12 MAC, and the import fails with "MAC verification failed".
The identity reports `CSSMERR_TP_NOT_TRUSTED` and `security find-identity -v` will not
list it, because a self-signed root is not trusted. That does not matter: trust governs
Gatekeeper *verification*, not signing. `bundle.sh` deliberately queries without `-v`.
What actually fixes the problem is the designated requirement. Ad-hoc gives
`cdhash H"..."`, which is the code hash and changes every build. A certificate gives
`certificate leaf = H"..."`, which is byte-identical across rebuilds — verified by
bundling twice and diffing `codesign -d -r-`.
Where the config lives, sandboxed and not
-----------------------------------------
`Paths.swift` derives every path from `FileManager.default.homeDirectoryForCurrentUser`.
Under the App Sandbox that accessor returns the app's **container**, not the real home —
probed on macOS 26.5 with an ad-hoc-signed bundle carrying only
`com.apple.security.app-sandbox`:
NSHomeDirectory() /Users/<you>/Library/Containers/<bundleid>/Data
FileManager.homeDirectoryForCurrentUser /Users/<you>/Library/Containers/<bundleid>/Data
getpwuid(getuid()).pw_dir /Users/<you>
Only `getpwuid` reports the real home; both Foundation accessors are redirected, and so
is `("~" as NSString).expandingTildeInPath`. So the config path follows the container with
no code change and no `isSandboxed` check, and the unsandboxed build is untouched:
unsandboxed ~/.config/spacial-shell/config.toml
sandboxed ~/Library/Containers/sh.emu.SpacialShell/Data/.config/spacial-shell/config.toml
Also probed inside the container: writes to `.config/spacial-shell` and to
`Library/Application Support`, the `O_EVTONLY` directory watch the live config reload
depends on, and `NSWorkspace.open` on the app's own config file — all succeed. Reading the
real `~/.config/spacial-shell` from inside the sandbox fails, which is the accepted cost:
see docs/config.md.
Debugging keys
--------------
`SPACIAL_LOG_KEYS=1 Scripts/dev.sh` logs every keyDown's keycode and flags at .info under
subsystem `sh.emu.SpacialShell`, category `hotkeys`. Read it live with:
/usr/bin/log stream --predicate 'subsystem == "sh.emu.SpacialShell"' --info
That is the instrument for the checklist in docs/platform-notes.md.
Measuring re-tile motion
------------------------
`SPACIAL_LOG_RETILE=1 Scripts/dev.sh` (with `animate-retile = true` in the config) logs one
`retile N=…` line per animated re-tile under category `motion` (capture per window, first frame,
dropped frames, main-thread CPU), and emits `retile*` signposts on Points of Interest for
Instruments. It measures only; it changes nothing about how a re-tile runs. The full benchmark is
`Scripts/e2e/scenarios/perf/retile.scn` (#140, M4 spec §7).