Subsystem · workspaces

Workspaces

Curie inherits Cargo's workspace model. One root Curie.toml lists members; each member is a self-contained project; the root inherits shared config down to every member.

Layout

tree
monorepo/
├── Curie.toml          # workspace root — no [application] / [library]
├── app/
│   ├── Curie.toml      # declares [application]
│   └── src/...
├── core/
│   ├── Curie.toml      # declares [library]
│   └── src/...
└── utils/
    ├── Curie.toml      # declares [library]
    └── src/...

Root configuration

The root Curie.toml declares members and any config that should be inherited:

Curie.toml — workspace root
[workspace]
members = ["app", "core", "utils"]

[java]
releaseVersion = "21"

[bom-imports]
"com.fasterxml.jackson:jackson-bom" = "2.17.2"

[test-bom-imports]
"org.junit:junit-bom" = "5.11.0"

Inheritance rules

Members declare dependencies with empty version strings ("") to defer to the inherited BOMs:

app/Curie.toml
[application]
name      = "app"
version   = "0.1.0"
mainClass = "com.example.App"

[dependencies]
# resolved from the workspace's jackson-bom
"com.fasterxml.jackson.core:jackson-databind" = ""

[workspace-dependencies]
core  = { path = "../core" }
utils = { path = "../utils" }

Topo-sorted builds

Run curie build from the workspace root and Curie computes a topological order over [workspace-dependencies], then builds each member in dependency order. Upstream target/classes directories — not just their JARs — are placed on downstream compile classpaths, so downstream tests don't have to wait for upstream JAR packaging to complete.

$ curie build # from workspace root
Workspace . build (3 members)

[1/3] utils
  Compile         3 source file(s)
  Tests           ✔ 8 tests successful
[2/3] core
  Compile         5 source file(s)
  Tests           ✔ 12 tests successful
[3/3] app
  Resolve deps    7 JAR(s)
  Compile         4 source file(s)
  Tests           ✔ 7 tests successful
  Done            target/app-0.1.0.jar

Targeted builds

Run curie build from a member directory and Curie still respects topology — it builds the member plus its transitive workspace dependencies, but skips siblings the target doesn't depend on. This is the everyday loop in a large monorepo: edit one package, build only what's affected.

List members

curie list from the root prints the member roster and their declared roles (application vs library):

$ curie list
Workspace . (3 members)
  app    application  v0.1.0
  core   library      v0.1.0
  utils  library      v0.1.0

Nested workspaces

A workspace member can itself be a workspace. Curie recursively flattens nested workspaces into a single member list, and configuration inheritance cascades through all levels — from the outermost root, through each intermediate workspace, down to every leaf member.

3-level nested workspace
# root/ — level 0
[workspace]
members = ["core-lib", "services"]

# root/services/ — level 1
[workspace]
members = ["greeter-lib", "apps"]

# root/services/apps/ — level 2
[workspace]
members = ["hello-app"]

Workspace dependencies can cross nesting levels. When curie build runs from the root, all leaf members are visible and built in topo order.

Running from any directory: a command invoked from anywhere inside the tree resolves to the outermost enclosing workspace — so the full inheritance chain applies and cross-level workspace-dependencies resolve. From a leaf member, only that member and its transitive deps build. From an intermediate sub-workspace directory (e.g. services/), Curie builds only that subtree's members plus their transitive deps — for example greeter-lib + hello-app, pulling in core-lib as a dependency, while leaving unrelated root-level siblings untouched.

Duplicate detection: if the same project directory appears more than once (directly or through nesting), Curie errors immediately. This also catches cycles — a nested workspace that references its parent triggers the duplicate check.

curie list shows a focused tree rooted at the outermost workspace, highlighting the current project/workspace with ← current. Nodes outside the current subtree are pruned unless they are workspace dependencies (or their ancestors). Each dependency target shows a reverse required by annotation listing its requirers, with paths relative to the target's parent workspace. curie list --all shows the complete tree including unrelated siblings.

$ curie --project services list # focused on the services sub-workspace
examples  (workspace)
└─ nested-workspace-demo  (workspace)
   ├─ core-lib  library v0.1.0
   │        (required by: services/greeter-lib, services/apps/hello-app)
   └─ services  (workspace)  ← current
      ├─ greeter-lib  library v0.1.0
      │        (required by: apps/hello-app)
      └─ apps  (workspace)
         └─ hello-app  application v0.1.0

Parallel build and test

When a workspace has more than one member to build (or test/clean), Curie runs them in parallel — bounded by --jobs (default: CPU count) and respecting the dependency DAG so no member starts before its upstream dependencies are ready.

Parallel build
# Build all members across 8 cores.
curie build -j8 --no-docker --no-native

# Run tests in parallel (default: all available cores).
curie test

# Clean everything in parallel.
curie clean -j4

Each member's external-command output (javac, kotlinc, junit) is captured on a PTY, buffered per-member, and written to stdout in contiguous blocks prefixed with the member name — for example string-utils | ✔ isBlank_empty_returns_true(). Raw output (including ANSI color codes) is also saved to target/build.log (or test.log, clean.log) inside each member's directory.

If any member fails, Curie stops dispatching new jobs (fail-early) but lets already-running jobs complete. The final error lists every failed member.

Single-member invocations (curie --project services/greeter-lib build where greeter-lib has no dependencies) keep today's direct output with no prefix overhead.

Parallel formatting

curie fmt at the workspace root fans out across every member in parallel. palantir-java-format is resolved once, then the JAR list is shared across the worker threads so there's no contention on the local cache.