Maven sync
curie maven sync generates a pom.xml from Curie.toml so that mvn verify produces the same compiled classes, tests, and packaged artifacts as curie build. Use it to keep IDEs and CI tooling that only understand Maven working alongside Curie, or to migrate a project to Curie one step at a time without losing the existing Maven build.
Generating a pom.xml
Run curie maven sync in any project or workspace root. In a workspace it writes one pom.xml per member plus an aggregator pom.xml at the root, listing members in <modules>:
Maven pom.xml written
Running it again with nothing changed does no work — generation is skipped as soon as the fingerprint in the existing file matches:
Maven pom.xml up to date
Add --check for a CI guard: nothing is written, and the command exits non-zero if any pom.xml is missing or stale — the same shape as curie fmt --check:
Maven pom.xml up to date
Pre-existing pom.xml
If a pom.xml already exists and was not generated by Curie, curie maven sync refuses to touch it and leaves the file untouched:
error: pom.xml was not generated by curie maven sync
pom.xml does not carry the curie-maven-fingerprint marker.
To proceed, either:
- delete pom.xml and re-run curie maven sync, or
- run curie maven sync --force to overwrite it, or
- remove [maven] sync from Curie.toml if this file is hand-written
curie maven sync --force overwrites the file once. The same check runs at the start of curie build when [maven] sync = true — a hand-written pom.xml fails the build with this error rather than being silently kept stale or silently destroyed.
Keeping it in sync automatically
Set sync = true under [maven] to regenerate the POM(s) at the start of every curie build, before compilation:
[maven] sync = true
[maven] is workspace-inheritable exactly like [test] and [kotlin] — set it once in the workspace root's Curie.toml and every member picks it up, unless a member overrides it.
What gets generated
Coordinates and properties
The project's name becomes <artifactId>, version becomes <version>, and an optional groupId becomes <groupId> — when omitted (as is common in Curie projects), the generator falls back to the constant curie.generated. Every generated POM also gets:
<properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <!-- matches jar.rs's reproducible-build epoch --> <project.build.outputTimestamp>2024-01-01T00:00:00Z</project.build.outputTimestamp> </properties>
maven.compiler.release comes from [java].releaseVersion (or the effective default). If [java].enablePreview = true, the compiler plugin gets --enable-preview and Surefire's <argLine> gets it too.
Source layout
Source roots are discovered with the same logic curie build uses: Maven-style src/main/{java,kotlin,groovy}, flat-package directories under src/, or a bare src/. The first production root becomes <sourceDirectory>; any additional roots are added with build-helper-maven-plugin's add-source. Test roots work the same way via <testSourceDirectory> and add-test-source.
Classes co-located with production sources but named *Test, *Tests, or *Spec are excluded from the main compile and added as test sources too — mirroring how curie build compiles them into target/test-classes only. src/main/resources / resources/ and src/test/resources / test-resources/ become <resources> and <testResources>.
Dependencies, scopes and BOM imports
| Curie.toml | pom.xml |
|---|---|
[dependencies] | <dependency>, scope compile |
[test-dependencies] | <dependency>, scope test |
exclusions = [...] | <exclusions> |
classifier | <classifier> |
{ javaAgent = true } (test dep) | maven-dependency-plugin:properties + Surefire <argLine>-javaagent:${...}</argLine> |
version "" (BOM-supplied) | <dependency> without <version> — resolved via <dependencyManagement> |
[workspace-dependencies] | <dependency> on the member's own GAV (resolved by the reactor or local repo) |
[bom-imports] and [test-bom-imports] become <dependencyManagement> imports with <type>pom</type><scope>import</scope>. Curie resolves "later wins" for inherited BOM imports, but Maven resolves "first wins" — so the generator emits a member's own [bom-imports] first, then workspace-inherited ones, preserving the same effective precedence.
Kotlin, Groovy & Spock
If Kotlin sources are present, the generator adds kotlin-maven-plugin at [kotlin].version(), wired so its compile executions run before maven-compiler-plugin's default Java compile, plus kotlin-stdlib as a compile dependency. If Groovy sources are present (production or test), it adds gmavenplus-plugin for joint compilation and org.apache.groovy:groovy at [groovy].version() — as a test-scoped dependency when Groovy is only used in tests. Enabling [spock] additionally broadens the Surefire include patterns to match *Spec classes.
Tests
curie build runs every test — co-located and tests/ — in one pass before packaging, so the generated POM configures Surefire only, bound to the normal test phase. The include patterns mirror the runner exactly:
| Configuration | Surefire <includes> |
|---|---|
| default | %regex[.*Tests?$|^Test.*|.*TestCase$] |
[spock] enabled | above, plus %regex[.*Spec$] |
Curie's bundled console runner includes the JUnit Jupiter engine for free; the generator only adds org.junit.platform:junit-platform-console-standalone (at [test].junitPlatformVersion(), scope test) when no test engine is otherwise on the classpath, so the Surefire classpath matches Curie's. [test].coverage = true adds jacoco-maven-plugin's prepare-agent and report goals.
Packaging
maven-jar-plugin is configured with <addMavenDescriptor>false</addMavenDescriptor> (no META-INF/maven/**, matching Curie's JARs), <mainClass> set to the declared or auto-detected main class, and <addClasspath>true</addClasspath> with <classpathPrefix>libs/</classpathPrefix> — the same Class-Path: libs/<dep>.jar manifest entries jar.rs writes. maven-dependency-plugin:copy-dependencies copies runtime-scope JARs into target/libs/ during the package phase.
If [fat-jar] is configured, copy-dependencies is skipped (Curie skips libs/ too) and maven-shade-plugin produces the fat JAR with classifier fat (name-version-fat.jar), a ServicesResourceTransformer for merged META-INF/services, and a ManifestResourceTransformer for Main-Class. shadeAll and per-dependency shade become shade's <includes>/<excludes>, and from/to relocations become <relocations> entries.
Protobuf and OpenAPI code generation
Projects that declare a [plugin.protobuf] or [plugin.openapi] section get a matching Maven code-generation plugin that produces the same source files as curie build:
| Curie section | Maven plugin | Version |
|---|---|---|
[plugin.protobuf] | io.github.ascopes:protobuf-maven-plugin | 5.1.2 (pinned) |
[plugin.openapi] | org.openapitools:openapi-generator-maven-plugin | = version field |
Both plugins auto-register their output directory as a compile source root, so no build-helper-maven-plugin glue is needed. Key field mappings:
- Protobuf —
version→<protoc>(theprotocbinary version to download);sourceDir→<sourceDirectories><sourceDirectory>(defaultproto);grpc = true→ adds a<binaryMavenPlugins>entry forio.grpc:protoc-gen-grpc-javaatgrpcVersion. Output directory:target/generated-sources/protobuf. - OpenAPI —
version→ plugin version;specFile→<inputSpec>;generatorName→<generatorName>;apiPackage/modelPackage/invokerPackagemap directly. CLIadditionalProperties(e.g.library = "native") become<configOptions>child elements — not<additionalProperties>.globalPropertiesbecome<globalProperties>child elements. Output directory:outputDir(defaulttarget/generated-sources/openapi).
Any other [plugin.*] section still emits a warning and is skipped; the generated POM cannot represent arbitrary external code-gen tools.
Workspaces
The workspace root gets an aggregator pom.xml: <packaging>pom</packaging> with <modules> listing every member in declaration order. Nested workspaces get their own aggregator the same way.
Member POMs do not use <parent>. Everything a member inherits from the workspace — [java], [test], [kotlin], BOM imports, annotation processors — is materialized directly into that member's pom.xml, the same fully-resolved view Curie itself builds with. Each member's POM is therefore self-contained: mvn verify works the same whether run from the workspace root (reactor build) or from inside the member directory.
Determinism and incrementality
Identical inputs produce a byte-identical pom.xml: dependency tables are emitted in sorted order, source roots use the same ordering as the compiler, and every Maven plugin version comes from one pinned table:
| Plugin | Version |
|---|---|
| maven-compiler-plugin | 3.15.0 |
| maven-surefire-plugin | 3.5.6 |
| maven-jar-plugin | 3.5.0 |
| maven-shade-plugin | 3.6.2 |
| maven-dependency-plugin | 3.11.0 |
| build-helper-maven-plugin | 3.6.0 |
| gmavenplus-plugin | 4.2.0 |
| jacoco-maven-plugin | 0.8.13 |
| protobuf-maven-plugin (io.github.ascopes) | 5.1.2 |
kotlin-maven-plugin's version always matches [kotlin].version(). openapi-generator-maven-plugin's version matches [plugin.openapi].version — the same generator version Curie invokes. No wall-clock time, hostname, username, or absolute paths appear in the output.
Every generated file starts with a header comment recording a fingerprint:
<!-- Generated by curie maven sync from Curie.toml — DO NOT EDIT.
curie-maven-fingerprint: 7a1c4e... -->
The fingerprint covers the generator's schema version, the project's (and workspace's) Curie.toml bytes, and the sorted list of source/test/resource roots — so adding a new package directory or editing Curie.toml triggers regeneration, while an unrelated rebuild does not. If the fingerprint still matches, curie maven sync skips the file entirely and reports up to date.
Pinning the transitive closure
Curie's resolver implements Maven's own mediation rules (nearest-wins, BOM precedence, exclusions) and is intended to resolve every dependency to the same version Maven would. If you hit a case where it doesn't, set pinTransitive = true as a temporary unblocking measure — it writes Curie's fully-resolved group:artifact → version closure into <dependencyManagement>, forcing Maven to match Curie's versions exactly:
[maven] sync = true pinTransitive = true
This defaults to false. A resolution mismatch is treated as a Curie resolver bug worth fixing, not something to paper over permanently.
Parity contract
mvn verify and curie build are considered equivalent for a module when:
| Aspect | Definition of equal |
|---|---|
| Compiled classes | Same .class entries in the JAR, byte-identical per entry |
| Resources | Same resource entries, byte-identical |
| Tests | Same test classes discovered and run; same pass/fail outcome and counts |
| JAR manifest | Same Main-Class and Class-Path (libs/<dep>.jar); other attributes (e.g. Created-By) may differ |
| JAR reproducibility | Both timestamped to 2024-01-01T00:00:00Z, entries sorted |
| Fat JAR | Same merged entry set, same relocations applied, META-INF/services merged the same way |
target/libs/ | Same set of dependency JARs as dependency:copy-dependencies |
| BOM modules | Same <dependencyManagement> content |
Docker images, GraalVM native binaries, coverage report layout, META-INF/build-info.properties, and sources generated by unknown [plugin.*] sections are outside this contract. Protobuf and OpenAPI sources are in contract: the Maven plugins emit the same files Curie does.
Known divergences
- Resolver mediation differences are treated as Curie bugs, not accepted divergences —
pinTransitiveis only a temporary workaround while one is being fixed. - Surefire vs. the console-standalone runner are different launchers around the same JUnit Platform; discovery matches but per-test execution order can differ (both are unordered by spec).
- Manifest extras — Maven Archiver adds attributes like
Created-Bythat Curie doesn't; onlyMain-ClassandClass-Pathare compared. - Per-dependency
repository = "id"has no per-dependency equivalent in Maven — all[[repositories]]become global<repositories>in the POM. The same artifacts are resolved either way. - Docker and GraalVM native-image are not represented in the generated POM yet (phase 2: opt-in Maven profiles).
- Unknown
[plugin.*]source generators — protobuf and OpenAPI are fully represented (see above); any other[plugin.*]section emits a warning and is skipped. - Fat JAR class bytes — both tools apply the same relocations to the same classes, but Curie rewrites constant-pool entries in place while
maven-shade-pluginre-serializes the whole class via ASM. The entry names match; the relocated.classbytes don't need to.