Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

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.

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

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

- All option types are immutable with static `of(...)` factories and `@Default` annotated defaults
- `Customizer` inner classes on `Application` interfaces are auto-discovered and applied at launch
- Launcher registry: `META-INF/<PlatformClassName>` properties files map `Application=Launcher`
- Launcher registry: JPMS `ServiceLoader<LauncherRegistration>` — modules declare `provides LauncherRegistration with ...` in `module-info.java`
- Checkstyle enforced: no tabs, no star imports, final locals, no asserts, braces required
45 changes: 27 additions & 18 deletions docs/CODEBASE_MAP.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
---
last_mapped: 2026-04-12T00:00:00Z
total_files: 233
total_tokens: ~180000
last_mapped: 2026-05-04T00:00:00Z
total_files: 236
total_tokens: ~193000
---

# Codebase Map

> Auto-generated by Cartographer. Last mapped: 2026-04-12
> Auto-generated by Cartographer. Last mapped: 2026-05-04

## System Overview

Expand Down Expand Up @@ -87,7 +87,7 @@ sequenceDiagram
participant Application

User->>Platform: launch(Specification)
Platform->>Platform: Discover Launcher via META-INF registry
Platform->>Platform: Discover Launcher via ServiceLoader<LauncherRegistration>
Platform->>Launcher: launch(platform, appClass, config)
Launcher->>Launcher: Customizer.onPreparing() loop
Launcher->>Launcher: Customizer.onLaunching()
Expand Down Expand Up @@ -151,9 +151,10 @@ spawn.build/
| `Customizer.java` | Lifecycle observer stored as an `Option`; `onPreparing/onLaunching/onLaunched/onStart/onTerminated` |
| `Launcher.java` | Functional interface; performs platform-specific launch work |
| `Machine.java` | `Platform` that is also `Addressable`; `workingDirectory()`, `temporaryDirectory()` |
| `AbstractTemplatedPlatform.java` | META-INF registry discovery, expression resolution, customizer auto-discovery, cascading preparation loop |
| `AbstractTemplatedLauncher.java` | Full launch orchestration: facet assembly, argument conversion, diagnostics tabulation |
| `AbstractApplication.java` | Wires process I/O to console via Pipes; registers customizer callbacks; `@Inject Iterable<Lifecycle<?>>` |
| `LauncherRegistration.java` | SPI interface: maps `(platformClass, applicationClass) → launcherClass`; discovered via `ServiceLoader` |
| `AbstractTemplatedPlatform.java` | `ServiceLoader<LauncherRegistration>` discovery at construction; expression resolution; customizer auto-discovery; `min()` launcher selection by most-specific Application subtype |
| `AbstractTemplatedLauncher.java` | Full launch orchestration: facet assembly, argument conversion, diagnostics tabulation; DI via `bind(class).to(instance)`, `bindSet`, `addResolver` |
| `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) |
| `Console.java` | stdin/stdout/stderr abstraction; `Console.Supplier` option selects implementation |
| `facet/Faceted.java` | JDK proxy implementing multiple unrelated interfaces simultaneously |
| `facet/FacetedInvocationHandler.java` | Dispatch: primary map + lazy superinterface map + `Faceted.as()` escape hatch |
Expand All @@ -162,6 +163,7 @@ spawn.build/

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

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

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

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

**Docker option types** — all implement `DockerOption.configure(ObjectNode, ObjectMapper)`:
**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:
| Option | Docker JSON field | Notes |
|--------|------------------|-------|
| `ImageName` | (image reference) | Handles tags, SHA256 refs, registry prefixing |
Expand All @@ -334,7 +336,7 @@ Server (listens on spawn:// URI)

### `spawn-docker-jdk`

**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.
**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.
**Entry point:** Four `Session.Factory` implementations discovered via `ServiceLoader`, in priority order:
1. `UnixDomainSocketBasedSession.Factory` — Unix socket (`/var/run/docker.sock` or Docker Desktop socket)
2. `LocalHostBasedSessionFactory` — TCP `localhost:2375`
Expand All @@ -359,7 +361,7 @@ Server (listens on spawn:// URI)
| `event/GetSystemEvents.java` | Streaming JSON parser; publishes `ActionEvent`; virtual thread |
| `model/DockerContainer.java` | Full `Container` impl; `@PostInject` wires event subscription for `onStart/onExit` |
| `model/DockerImage.java` | `Image` impl; `start()` creates then starts container; auto-removes on start failure |
| `model/AbstractJsonBasedResult.java` | DI-injected `Session`, `JsonNode`, `ObjectMapper` for all model classes |
| `model/AbstractJsonBasedResult.java` | DI-injected `Session` + `JsonValue` (base-json); `at(keys)` / `text(keys)` / `intAt` / `boolAt` navigation helpers |

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

### Launcher Registry Pattern
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).
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`:
```
provides build.spawn.application.LauncherRegistration with com.example.MyLauncherRegistration;
```
`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.

### Nested Class Conventions
- `Application.Implementation` (or `JDKApplication.Implementation`) — public static concrete class found by `Application.getImplementationClass()` via reflection
Expand All @@ -393,7 +399,7 @@ Tests requiring Docker are gated by `@EnabledIf("isDockerAvailable")` — they w
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)`.

### `Iterable<Lifecycle<?>> lifecycles` in `AbstractApplication`
This is `@Inject`-annotated. If an `AbstractApplication` subclass is created outside the DI framework, `lifecycles` will be `null` and `onStart()` will throw `NullPointerException`.
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.

### Default Environment Variables
`@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.
Expand Down Expand Up @@ -431,11 +437,14 @@ Child JDK processes are killed when the parent JVM exits by default. Use `Orphan
1. Create interface extending `Application` (or `JDKApplication`)
2. Add nested `public static class Implementation extends AbstractApplication`
3. Optionally add `public static class Customizer implements Customizer<MyApp>` for defaults
4. Register a `Launcher` in `META-INF/<PlatformClassName>`: `MyApp=MyLauncher`
- Files: `Application.java`, `AbstractApplication.java`, `Launcher.java`, `AbstractTemplatedLauncher.java`
4. Create a `LauncherRegistration` record linking your platform, application class, and launcher class
5. Declare `provides build.spawn.application.LauncherRegistration with MyLauncherRegistration` in your `module-info.java`
- Files: `Application.java`, `AbstractApplication.java`, `Launcher.java`, `AbstractTemplatedLauncher.java`, `LauncherRegistration.java`

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

Expand Down
Loading