Off Apple the xz container was unpacked by running /usr/bin/env xz, which Windows has no answer for: Process reports that the file does not exist, and the message reaches the user as it stands. Decode in-process instead, the way Compression already does on Apple, and ask for the library the platforms package.
18 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Build Commands
make # Incremental release build via xcodebuild → build/Luma.app
make check-examples # Read the Pharo example catalogue in the image
make check-patches # Play every patch offline, and check panning and echo
make check-editor # Ask the compiler's language server what it completes
make clean # Remove build artifacts
make -C LumaGtk render-tests draws the shader effects and a lettered
scene offscreen, writes a PNG of each, and fails on a case that drew
nothing -- what has gone wrong in this drawing has been blank frames
and mirrored glyphs, which a compiler cannot see and a PNG shows at a
glance.
LUMA_SOAK=<snippet> make -C LumaGtk soak plays a snippet in a canvas
and reports what the process is holding as it goes -- the game leaks
nothing, but it took three fixes to get there, and a flat line is only
worth reading once the snippet is seen moving.
make check-editor opens a hook in a scratch project and asks the
compiler's language server what it completes, hovers and reports. The
editor draws what that server says, and the server sees the workspace
the way a build does -- so a typing that went missing shows here first.
PharoExampleCatalog is Smalltalk inside Swift string literals, which
no compiler reads. make check-examples parses each one in the image
and reports syntax errors, names nothing defines, and selectors
nothing implements -- the three ways examples have shipped broken.
glslang and SPIRV-Cross do the GLSL→Metal translation, at build time
and at run time, and build under neither SwiftPM nor Xcode. CI makes
them into ShaderToolchain.xcframework and publishes it against a
shader-toolchain-<version> tag; the manifest names that artifact, so
a build downloads it and nothing local is needed. It carries a slice
per platform the app ships to -- macOS, the device and the simulator --
since the runtime translation goes wherever the app does.
To work on the toolchain itself, make one and say so:
scripts/make-shader-toolchain-xcframework.sh
export SHADER_TOOLCHAIN_ROOT=artifacts/ShaderToolchain.xcframework
Both Makefiles set that themselves when the artifact is there. Only
Apple platforms have any of this: OpenGL takes the GLSL as it stands,
so CShaderTranslate is not a target at all elsewhere.
Or open Luma.xcodeproj in Xcode and build with Cmd+B (set
destination to My Mac, or to a simulator for the iOS frontend).
The agent bundles, the TypeScript typings and the shader catalogue are
generated Swift inside LumaCore, and the Pharo image is a resource
staged into it. Xcode enumerates a package target's sources and
resources when it loads the package graph -- before any build phase or
scheme pre-action runs. Anything produced during a build is therefore
missing from that build, so scripts/generate-sources.sh writes them
before xcodebuild is invoked: make and CI run it, and the Luma
scheme's pre-action refreshes them, saying what to run when they are
not there yet. A fresh checkout built from Xcode wants it once by
hand. SwiftPM generates the sources through LumaBundlePlugin and
LumaShaderPlugin instead, and both Makefiles stage the image through
scripts/stage-pharo-image.sh.
LumaCore (the cross-platform Swift package) can be built and
type-checked on Linux without Xcode:
swift build --target LumaCore
Only Apple platforms have a published Pharo VM to link against.
Elsewhere SwiftyPharo asks pkg-config for pharo-vm, which no
distribution packages, so make -C LumaGtk build builds one into
.build/pharo-vm on the first run and leaves an installed one alone.
It prefers a sibling ../SwiftyPharo checkout over the resolved one,
the way the image does, so work on the VM is picked up without
pushing. That first build is long; PLATFORM=linux tools/build-vm.sh
in SwiftyPharo is the same thing by hand.
There are no tests or linting commands. Code formatting follows
.swift-format (4-space indent, 140-char line length).
Avoid "headline comments" that narrate what the next block does. Prefer aptly named variables and functions to make code self-explanatory. Only add comments as a last resort, for non-obvious why (hidden constraints, workarounds).
Architecture
Luma is an interactive dynamic instrumentation app built on
Frida. All business logic lives in LumaCore,
a portable Swift package. The current shipping frontend is a SwiftUI
app for macOS and iOS; a GTK/Adwaita frontend for Linux can be added
against the same LumaCore.
The Pharo surface -- the playground, the notebook cells, the source
editor with its inline marks, the inspector columns and the canvas --
is written once for both Apple platforms. Luma/Platform*.swift holds
what the two frameworks name differently: a view, a representable, a
hosting view, a font, a colour, and what the pointer becomes where
there is one. The source editor is the one place the two really part
company: a text view is inside a scroll view on AppKit and is one on
UIKit, the find bar is the app's on one and the system's on the other,
and completions are a panel where there is a pointer and a strip above
the keyboard where there is not.
+-----------------------------+ +---------------------------+
| SwiftUI frontend (macOS/iOS)| | GTK frontend (Linux) |
| Luma/ | | (planned) |
+--------------+--------------+ +-------------+-------------+
| |
+---------------+-------------------+
|
+------------v-------------+
| LumaCore |
| (Swift package, all |
| business logic) |
+------------+-------------+
|
+--------------+--------------+
| | |
frida-swift SwiftyR2 GRDB.swift
|
+------------v-------------+
| Agent (TypeScript) |
| compiled & embedded |
+--------------------------+
Two-process model
- Host (Swift) — owns the UI, persistence, and Frida session
lifecycle (via
LumaCore). - Agent (TypeScript) — compiled JS injected into the target process via Frida. Exposes RPC methods for instrumentation, REPL evaluation, memory access, and symbolication.
The agent entry point is Agent/core/luma.ts, which re-exports all
RPC methods from sibling modules. Agent source is compiled and
embedded into Sources/LumaCore/Generated/LumaAgent.swift by the
LumaBundleCompiler build tool target. The Generated/ directory
is gitignored — it is produced at build time.
LumaCore (Sources/LumaCore/)
-
Engine— central@Observable @MainActorclass. Owns theDeviceManager, allProcessNodeinstances, the event log,CollaborationSession,GitHubAuth,HookPackLibrary, theDisassemblercache, address annotations, the descriptor registry, and the address-action provider list. Single public entry point:start()(called from the host once at launch). -
ProcessNode— represents one attached process. Holds the FridaSession+Script, loaded modules, andInstrumentRefs. ExposesAsyncStreamevent sources (events, REPL results, ITrace captures, module snapshots, detach events). -
EventLog—@Observablering buffer with batched 16ms flushing. Frontends readevents/totalReceiveddirectly via Observation; no mirror in the host. -
Disassembler/TraceDisassembler— concrete@MainActorclasses wrappingR2Corefor live and trace disassembly. Both return portableDisassemblyLine/StyledText(RGB-spans, no AppKit/SwiftUI dependency). Frontends ship a tiny extension to convertStyledTextinto their preferred styled-text type. -
HookPackLibrary— discovers hook packs from a directory and producesInstrumentDescriptors. Engine owns one rooted atdataDirectory/HookPacks. -
AddressAction/ThreadAction— pluggable per-address and per-thread action providers. The tracer registers itself at engine init; future instrument kinds can callengine.registerAddressActionProvider/registerThreadActionProvider.AddressContext(kind: code / function / data) lets providers tailor actions per call site. -
Persistence (GRDB / SQLite) —
.lumais a directory document containingdb.sqliteandtraces/<uuid>.bin.ProjectStoreowns the database with row models forProcessSession,InstrumentInstance,REPLCell,NotebookEntry,ITrace(metadata only; data lives inTraceStore),AddressInsight,RemoteDeviceConfig,ProjectPackagesState,InstalledPackage,ProjectCollaborationState,TargetPickerState, plus UI-state singletonsProjectUIStateand per-sessionSessionUIState. Schema is created withif not existson every open; pre-release, no migrations. -
TraceStore— file-backed blob store for raw ITrace data. Engine routes reads throughloadTraceData(traceID:sessionID:expectedSize:), which checks live in-memory pending state, then the local file, then falls back to a paginated server fetch viaCollaborationSession. -
CollaborationSession— portal bus, rooms, notebook sync, chat.GitHubAuthis a separate@Observableactor that owns the OAuth device flow and token storage;Engine.startCollaborationawaitsgitHubAuth.requestToken(), which suspends until the host finishes presenting the sign-in sheet. -
Canvas (
CanvasScene,CanvasDrawable,CanvasRegistry) — a scene the image holds by handle and changes; the host draws it. A drawable carries the author's own vertex and fragment stages, their own attribute layout, named uniforms, data buffers, a transform and a visibility flag.CanvasRegistryis lock-guarded rather than main-actor, because the image's thread needs a handle back at once; changes reach the frontends throughCanvasRegistry.onChangeon the main thread. -
Audio (
Synth,SynthEngine,EventChime) —SynthEngineis the mixer, running on the audio device's thread. Its storage is allocated once and reached through unsafe pointers, and the control side speaks to it only through a lock-free ring, so nothing under the callback allocates, retains or locks. Eight channels, each with its own patch and pattern on its own step clock.CLumaAudioholds only the device layer (miniaudio); the voices are Swift. -
TypeScript editing (
Sources/LumaCore/TypeScript/) — the editors askFrida.LanguageServerfor what they show, which is the compiler's own language server speaking LSP in-process.LanguageClientdoes the JSON-RPC,TypeScriptProjectowns one server per editor surface so the declarations a surface adds ambiently stay out of the others' programs, andTypeScriptDocumentis the open file the frontends talk to for completions, hover, diagnostics and symbols.TypeScriptEditorSessionopens a project for anEditorProfile. The server sees the compiler workspace'snode_modulesand the typings embedded in the compiler, so the editor and a build agree. Lexical colouring isTypeScriptLexer, andSourceSyntaxPaletteits colours, shared by both frontends. -
Pharo bridges (
Sources/LumaCore/Pharo/) —PharoHostBridgepublishes record feeds;PharoSynthBridgeandPharoCanvasBridgeare the image's entry points into the synthesiser and the canvas.PharoLumaBindingscompiles theLuma*classes into the image on the way up. See the symbol-visibility note below: nothing in Swift calls these@_cdeclexports, and that has bitten twice.
Host (Luma/)
Workspace— thin host adapter. OwnsEngine, exposes a few UI-only flags (targetPickerContext,isCollaborationPanelVisible), wires the SwiftUI instrument-UI registry, and providesprocessNode(for: event)/instrument(for: event)/sidebarItem(for: NavigationTarget)lookup helpers.MainWindowView— top-level SwiftUI view; splits into sidebar (process/instrument list) and detail area.Luma/Instruments/— per-instrument SwiftUI factories (TracerUI,HookPackUI,CodeShareUI), theInstrumentUIprotocol, theInstrumentUIRegistrydispatcher, andInstrumentEventMenuItem.Luma/Editor/— the TypeScript and JavaScript editor:CodeTextViewis the text view (AppKit and UIKit),CodeTextEditorthe representable that binds it to aTypeScriptEditorSession.StyledTextSwiftUI.swift— host-side conversion ofLumaCore.StyledTexttoAttributedString(SwiftUI) andNSAttributedString(AppKit / Metal CFG renderer).
Shaders (Shaders/)
Effects are authored once, body-only, in GLSL, against a preamble the
host supplies (v_uv, frag_color, u_resolution, u_time,
u_scheme, u_activity, u_pulse, dataAt(), u_mvp).
LumaShaderCompilertranslates eachShaders/*.frag.glslat build time: a Swift catalog of the GLSL for OpenGL hosts, and a Metal fragment function via glslang and spirv-cross. Driven byLumaShaderPluginfor SwiftPM, by an Xcode target feeding a script phase onAgentBundle, and by a Makefile rule for LumaGtk, whose plugins do not run when the package is resolved as a dependency.CShaderTranslatedoes the same translation at runtime, for shaders written in a snippet, and answers what the compiler said when they will not compile.- Two flavours, deliberately. OpenGL takes the source as it stands, at a version with no explicit locations, binding attributes by name; Metal's goes through glslang, which wants both. The generated preambles differ accordingly.
- spirv-cross numbers MSL resources in the order it meets them,
not by the binding written in the shader.
translate.cpins every binding explicitly — buffers 0 and 1 for the two uniform blocks, textures and samplers above — and vertices bind at index 30, clear of the range. Getting this wrong binds each buffer where another was expected, which no compiler will catch.
LumaText letters a scene from a glyph atlas the host rasterises
-- Core Text on macOS, Pango on GTK -- because what fonts an image can
reach is its own business, and the bundled one finds no scalable
family at all: every point size comes back as the same 14-pixel bitmap.
The frontends register a GlyphAtlasRasteriser on the way up; each
string becomes two triangles a letter. Saying something else goes
through remesh:primitive:, which sets vertices without a commit, so
text that changes every frame costs a buffer rather than a rasterise
and a shader compile. Its point size is in the units the rest of
the interface uses, and the atlas is rasterised to match: u_scale
carries how many physical pixels a logical one is worth, so the quads
land on whole pixels instead of being scaled, which is what would
make the lettering soft.
Keep gl_Position.z within 0..1. OpenGL's clip space runs -1..1 and
Metal's runs 0..1, so a negative depth draws on GTK and is clipped
away on macOS -- which no compiler will catch either.
The preambles are generated text, and generated text is worth
compiling. ShaderVocabulary, CanvasGeometry and CanvasScene
depend on nothing but Foundation, so they can be built on their own
and their output handed to glslang — which catches what a Swift
build cannot, a shader that compiles nowhere:
swiftc -o gencheck Sources/LumaCore/{ShaderVocabulary,CanvasGeometry,CanvasScene,CanvasInput}.swift main.swift
glslang -S frag generated.frag # OpenGL flavour
glslang -S frag -V generated.frag # Metal flavour, as translated
Symbol visibility (macOS)
An exported-symbols list is a whitelist: everything the image resolves
by name has to be in it, not just Luma's own entry points. SwiftyPharo's
swifty_pharo_* bridge is resolved the same way, and leaving it out
takes Pharo down entirely -- in release builds only.
The image resolves the host's luma_* entry points by name through
dlsym, and nothing in Swift calls them. Two consequences, both of
which have already caused bugs:
- Release builds with link-time optimisation, which internalises
anything unreferenced.
Luma/PharoBridgeExports.expnames them so it cannot. That list is a whitelist, so it is set on the Release configuration only — on Debug it would hide the entry point the stub executable looks for inLuma.debug.dylib, and the app aborts on launch. - LumaGtk passes
-export_dynamicfor the same reason.
A change to symbol visibility on the app target must be checked in both configurations; a passing Release build says nothing about Debug.
Dependencies (Swift Package Manager)
frida-swift— Frida bindingsSwiftyR2— Radare2 integration (used insideLumaCorefor disassembly)GRDB.swift— SQLite persistenceswift-crypto— collaboration cryptoSwiftyPharo— Pharo VM, image and the<gtView>builder shimminiaudio— vendored inSources/CLumaAudio, device layer onlyliblzma— unpacks the Barebone agent off Apple, where Compression is not there to do itglslang+spirv-cross— GLSL to Metal, at build time and at runtime. Found wherever they happen to be installed; shipping wants prebuilt libraries beside the Pharo VM's, neither project building under SwiftPM.
Agent modules (Agent/)
Agent/core/ contains the RPC modules: env, instrument,
memory, repl, resolver, symbolicate, console, pkg,
value. Built-in instruments live in Agent/instruments/
(e.g. tracer.ts, codeshare.ts).
Commit Style
- Subject line: max 50 characters
- Body lines: wrap at 72 characters (use the full width, or slightly less if it avoids making the next line awkward)
Requirements
- macOS 15.6+
- Xcode 26+