Skip to content

Commit 0e04a91

Browse files
authored
chore: update codebase map (#29)
1 parent 9d74972 commit 0e04a91

2 files changed

Lines changed: 29 additions & 20 deletions

File tree

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
Spawn is a Java 25 framework for programmatically launching and controlling processes, JVMs, and Docker containers. It provides a unified abstraction (`Platform` / `Application` / `Process`) over different execution environments. The core pattern: define a `Specification`, call `platform.launch(spec)`, get back an `Application` with `CompletableFuture`-based lifecycle hooks.
66

7-
**Stack**: Java 25, Maven, Jackson, `build.base.*` and `build.codemodel.injection`
7+
**Stack**: Java 25, Maven, `build.base.*` (incl. `build.base.json`) and `build.codemodel.injection`
88

99
**Structure**: 8 Maven modules in a monorepo, each mapping to a JPMS module:
1010
- `spawn-option` → shared option types
@@ -31,5 +31,5 @@ Tests requiring Docker are gated by `@EnabledIf("isDockerAvailable")`. The `spaw
3131

3232
- All option types are immutable with static `of(...)` factories and `@Default` annotated defaults
3333
- `Customizer` inner classes on `Application` interfaces are auto-discovered and applied at launch
34-
- Launcher registry: `META-INF/<PlatformClassName>` properties files map `Application=Launcher`
34+
- Launcher registry: JPMS `ServiceLoader<LauncherRegistration>` — modules declare `provides LauncherRegistration with ...` in `module-info.java`
3535
- Checkstyle enforced: no tabs, no star imports, final locals, no asserts, braces required

docs/CODEBASE_MAP.md

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
---
2-
last_mapped: 2026-04-12T00:00:00Z
3-
total_files: 233
4-
total_tokens: ~180000
2+
last_mapped: 2026-05-04T00:00:00Z
3+
total_files: 236
4+
total_tokens: ~193000
55
---
66

77
# Codebase Map
88

9-
> Auto-generated by Cartographer. Last mapped: 2026-04-12
9+
> Auto-generated by Cartographer. Last mapped: 2026-05-04
1010
1111
## System Overview
1212

@@ -87,7 +87,7 @@ sequenceDiagram
8787
participant Application
8888
8989
User->>Platform: launch(Specification)
90-
Platform->>Platform: Discover Launcher via META-INF registry
90+
Platform->>Platform: Discover Launcher via ServiceLoader<LauncherRegistration>
9191
Platform->>Launcher: launch(platform, appClass, config)
9292
Launcher->>Launcher: Customizer.onPreparing() loop
9393
Launcher->>Launcher: Customizer.onLaunching()
@@ -151,9 +151,10 @@ spawn.build/
151151
| `Customizer.java` | Lifecycle observer stored as an `Option`; `onPreparing/onLaunching/onLaunched/onStart/onTerminated` |
152152
| `Launcher.java` | Functional interface; performs platform-specific launch work |
153153
| `Machine.java` | `Platform` that is also `Addressable`; `workingDirectory()`, `temporaryDirectory()` |
154-
| `AbstractTemplatedPlatform.java` | META-INF registry discovery, expression resolution, customizer auto-discovery, cascading preparation loop |
155-
| `AbstractTemplatedLauncher.java` | Full launch orchestration: facet assembly, argument conversion, diagnostics tabulation |
156-
| `AbstractApplication.java` | Wires process I/O to console via Pipes; registers customizer callbacks; `@Inject Iterable<Lifecycle<?>>` |
154+
| `LauncherRegistration.java` | SPI interface: maps `(platformClass, applicationClass) → launcherClass`; discovered via `ServiceLoader` |
155+
| `AbstractTemplatedPlatform.java` | `ServiceLoader<LauncherRegistration>` discovery at construction; expression resolution; customizer auto-discovery; `min()` launcher selection by most-specific Application subtype |
156+
| `AbstractTemplatedLauncher.java` | Full launch orchestration: facet assembly, argument conversion, diagnostics tabulation; DI via `bind(class).to(instance)`, `bindSet`, `addResolver` |
157+
| `AbstractApplication.java` | Wires process I/O to console via Pipes; registers customizer callbacks; `@Inject Iterable<Lifecycle<?>> lifecycles = List.of()` (defaults to empty — safe without DI) |
157158
| `Console.java` | stdin/stdout/stderr abstraction; `Console.Supplier` option selects implementation |
158159
| `facet/Faceted.java` | JDK proxy implementing multiple unrelated interfaces simultaneously |
159160
| `facet/FacetedInvocationHandler.java` | Dispatch: primary map + lazy superinterface map + `Faceted.as()` escape hatch |
@@ -162,6 +163,7 @@ spawn.build/
162163

163164
**Exports:** `build.spawn.application`, `.console`, `.facet`, `.option`
164165
**Dependencies:** `spawn-option`, `build.base.*`, `build.codemodel.injection`, `jakarta.inject`
166+
**ServiceLoader SPI:** `uses LauncherRegistration` — modules contribute launcher registrations with `provides LauncherRegistration with ...` in their `module-info.java`
165167

166168
**Option type pattern** (applies everywhere):
167169
- Private constructor + `static of(...)` factory
@@ -251,7 +253,7 @@ Server (listens on spawn:// URI)
251253
| `LocalMachine.java` | Singleton `Machine`; extends `AbstractTemplatedPlatform`; PID from `RuntimeMXBean.getName()` |
252254
| `LocalProcess.java` | Wraps `java.lang.Process`; virtual thread watcher; `suspend/resume` via `kill -STOP/-CONT` |
253255
| `LocalLauncher.java` | `ProcessBuilder` invocation; sets env vars, working dir, arguments; double-quotes paths containing spaces |
254-
| `META-INF/build.spawn.platform.local.LocalMachine` | Registry: `Application=LocalLauncher` |
256+
| `LocalLauncherRegistration.java` | `LauncherRegistration` record: maps `LocalMachine + ApplicationLocalLauncher`; declared in `module-info.java` with `provides` |
255257

256258
**`suspend()`/`resume()`:** Use POSIX signals — only work on Unix/Linux/macOS.
257259
**`shutdown()`:** SIGTERM (`process.destroy()`). **`destroy()`:** SIGKILL (`process.destroyForcibly()`).
@@ -273,7 +275,7 @@ Server (listens on spawn:// URI)
273275
| `JDKHomeBasedPatternDetector.java` | Native JPMS service impl (registered via `module-info.java` `provides…with`); reads `java.home.properties` OS-specific globs; caches result in `AtomicReference` |
274276
| `LocalJDKLauncher.java` | Builds `java` command line including SpawnAgent injection; discovers or creates `spawn-agent.jar` |
275277
| `java.home.properties` | OS-keyed glob patterns: `mac@*`, `unix@*` entries for JDK installation directories |
276-
| `META-INF/build.spawn.platform.local.LocalMachine` | Registry: `JDKApplication=LocalJDKLauncher` |
278+
| `LocalJDKLauncherRegistration.java` | `LauncherRegistration` record: maps `LocalMachine + JDKApplicationLocalJDKLauncher`; declared in `module-info.java` with `provides` |
277279

278280
**JDK detection two-phase approach (post commit `b7f1f96`):**
279281
1. `paths()` — cheap: expand OS-specific globs, walk filesystem, return matching directory paths (no subprocess)
@@ -314,7 +316,7 @@ Server (listens on spawn:// URI)
314316
| `DockerFileBuilder.java` | Fluent Dockerfile content builder |
315317
| `DockerContextBuilder.java` | Builds Docker build context tarball (extends `AbstractTarBuilder`) |
316318

317-
**Docker option types**all implement `DockerOption.configure(ObjectNode, ObjectMapper)`:
319+
**Docker option types**`DockerOption` is a **sealed interface** (`permits Bind, Command, ExposedPort, ExtraHost, Link, PublishAllPorts, PublishPort`). Options carry data only; serialization into JSON is handled entirely by the command classes in `spawn-docker-jdk` via pattern-matching switch, keeping JSON libraries out of the public API:
318320
| Option | Docker JSON field | Notes |
319321
|--------|------------------|-------|
320322
| `ImageName` | (image reference) | Handles tags, SHA256 refs, registry prefixing |
@@ -334,7 +336,7 @@ Server (listens on spawn:// URI)
334336

335337
### `spawn-docker-jdk`
336338

337-
**Purpose:** JDK-native concrete implementation of `spawn-docker` interfaces. Uses `java.net.http.HttpClient` for TCP and `java.nio.channels.SocketChannel` with `UnixDomainSocketAddress` for Unix domain sockets. No third-party HTTP dependencies.
339+
**Purpose:** JDK-native concrete implementation of `spawn-docker` interfaces. Uses `java.net.http.HttpClient` for TCP and `java.nio.channels.SocketChannel` with `UnixDomainSocketAddress` for Unix domain sockets. JSON handled by `build.base.json` (base-json). No third-party dependencies.
338340
**Entry point:** Four `Session.Factory` implementations discovered via `ServiceLoader`, in priority order:
339341
1. `UnixDomainSocketBasedSession.Factory` — Unix socket (`/var/run/docker.sock` or Docker Desktop socket)
340342
2. `LocalHostBasedSessionFactory` — TCP `localhost:2375`
@@ -359,7 +361,7 @@ Server (listens on spawn:// URI)
359361
| `event/GetSystemEvents.java` | Streaming JSON parser; publishes `ActionEvent`; virtual thread |
360362
| `model/DockerContainer.java` | Full `Container` impl; `@PostInject` wires event subscription for `onStart/onExit` |
361363
| `model/DockerImage.java` | `Image` impl; `start()` creates then starts container; auto-removes on start failure |
362-
| `model/AbstractJsonBasedResult.java` | DI-injected `Session`, `JsonNode`, `ObjectMapper` for all model classes |
364+
| `model/AbstractJsonBasedResult.java` | DI-injected `Session` + `JsonValue` (base-json); `at(keys)` / `text(keys)` / `intAt` / `boolAt` navigation helpers |
363365

364366
**Known bugs:**
365367
- `GetSystemEvents` and `DockerContainer` have debug `System.out.println` calls in production code
@@ -377,7 +379,11 @@ Server (listens on spawn:// URI)
377379
- Braces required on all blocks
378380

379381
### Launcher Registry Pattern
380-
Platform classes have a `META-INF/<ConcreteClassName>` properties file on the classpath. Each line: `ApplicationClass=LauncherClass`. Multiple modules can contribute to the same registry file. `AbstractTemplatedPlatform` discovers launchers by reading all matching resources and selecting the most-specific `Application` supertype match (NOT `findFirst()` — avoids classpath-order bugs).
382+
Launcher registration uses JPMS `ServiceLoader`. Each module that provides a launcher declares a `LauncherRegistration` implementation (typically a `record`) and registers it in `module-info.java`:
383+
```
384+
provides build.spawn.application.LauncherRegistration with com.example.MyLauncherRegistration;
385+
```
386+
`AbstractTemplatedPlatform` loads all `LauncherRegistration` providers at construction time and selects the most-specific `Application` subtype match using `min()` with class-hierarchy ordering — NOT `findFirst()`, which would produce silent order-dependent failures if multiple modules are on the classpath.
381387

382388
### Nested Class Conventions
383389
- `Application.Implementation` (or `JDKApplication.Implementation`) — public static concrete class found by `Application.getImplementationClass()` via reflection
@@ -393,7 +399,7 @@ Tests requiring Docker are gated by `@EnabledIf("isDockerAvailable")` — they w
393399
The returned `Application` from `launch()` is a JDK `Proxy` (`Faceted` implementation), NOT a direct instance of `Application.Implementation`. `instanceof Application.Implementation` will always return `false`. Use interface types or `Faceted.as(Class)`.
394400

395401
### `Iterable<Lifecycle<?>> lifecycles` in `AbstractApplication`
396-
This is `@Inject`-annotated. If an `AbstractApplication` subclass is created outside the DI framework, `lifecycles` will be `null` and `onStart()` will throw `NullPointerException`.
402+
This field is `@Inject`-annotated but also initialized to `List.of()` as a default. Subclasses created outside the DI framework will have an empty lifecycle list rather than null `onStart()` is safe.
397403

398404
### Default Environment Variables
399405
`@Default` on `EnvironmentVariables.none()` means child processes inherit NO environment variables by default. Add `EnvironmentVariables.inherited()` explicitly to pass through the parent JVM's environment.
@@ -431,11 +437,14 @@ Child JDK processes are killed when the parent JVM exits by default. Use `Orphan
431437
1. Create interface extending `Application` (or `JDKApplication`)
432438
2. Add nested `public static class Implementation extends AbstractApplication`
433439
3. Optionally add `public static class Customizer implements Customizer<MyApp>` for defaults
434-
4. Register a `Launcher` in `META-INF/<PlatformClassName>`: `MyApp=MyLauncher`
435-
- Files: `Application.java`, `AbstractApplication.java`, `Launcher.java`, `AbstractTemplatedLauncher.java`
440+
4. Create a `LauncherRegistration` record linking your platform, application class, and launcher class
441+
5. Declare `provides build.spawn.application.LauncherRegistration with MyLauncherRegistration` in your `module-info.java`
442+
- Files: `Application.java`, `AbstractApplication.java`, `Launcher.java`, `AbstractTemplatedLauncher.java`, `LauncherRegistration.java`
436443

437444
**To add a Docker option:**
438-
- Implement `DockerOption.configure(ObjectNode, ObjectMapper)` in a new class
445+
- Add a new class to `spawn-docker/src/main/java/build/spawn/docker/option/` implementing `DockerOption` (data only — no JSON methods)
446+
- Add the new type to `DockerOption`'s `permits` clause (sealed interface)
447+
- Handle the new type in the relevant command class in `spawn-docker-jdk` (pattern-matching switch over `DockerOption`)
439448
- Implement `CollectedOption<LinkedHashSet>` if multiple instances should accumulate
440449
- Files: `DockerOption.java`, existing option classes in `spawn-docker/src/main/java/build/spawn/docker/option/`
441450

0 commit comments

Comments
 (0)