Incremental builds
Curie's rebuild gate is precise — false negatives (a missed edit) would be worse than a stray rebuild, and the design is biased accordingly. Three signals decide whether to invoke the compiler at all.
The mtime check
The compiler runs when any source file's mtime is greater than or equal to the oldest .class file's mtime in target/classes. The comparison is intentionally ≥, not > — on second-resolution filesystems (FAT, some NFS mounts, CI cache restores) a fast edit/test/edit loop can put an edit and a stamp in the same filesystem second, and silently masking the second edit would be a correctness bug.
Non-class files in target/classes (annotation-processor resources like BenchmarkList or CompilerHints) are deliberately ignored — they can have older mtimes than your sources and would otherwise force a recompile on every build.
The JDK fingerprint
Curie writes the output of javac -version to target/.javac-version after every successful build. On the next build that string is compared to the current javac -version; any difference forces a full recompile regardless of source mtimes. Upgrading the JDK between builds is an unambiguous "everything is stale" signal.
Stale-class detection
The mtime check decides whether to recompile. Stale-class detection decides what stays on disk. The story differs by language because Java and Kotlin emit metadata differently.
Java: source→class manifest
Curie ships a javac wrapper that registers a TaskListener on JavacTask. For every TaskEvent.Kind.GENERATE event the listener records the originating source path and the binary name of the emitted class. After a successful build the mapping is written atomically to target/.classes.toml:
# Authoritative source → class-file mapping [sources] "/abs/path/src/com/foo/Bar.java" = ["com/foo/Bar.class", "com/foo/Bar$Inner.class"]
On the next build:
- Pre-compile prune — any source in the previous manifest that no longer exists in the current source set has all of its old classes deleted before javac runs. Stops a deleted-but-still-on-disk class from being picked up via javac's classes-dir-on-classpath fall-through.
- Post-compile prune — for every source present in both old and new manifests, the set difference (classes in old, not in new) is pruned. Catches the case where a still-present source produces fewer classes than before (e.g. an inner / companion type was removed).
Annotation-processor outputs (under target/generated-sources) are exempted from pre-pruning so a no-change rebuild doesn't churn through every AP-generated class — the post-prune handles the "AP stopped producing this" case correctly.
Kotlin: SourceFile attribute + source-set stamp
kotlinc offers no equivalent TaskListener hook, so Curie exploits a different invariant: every Kotlin source is passed to kotlinc on every recompile, so kotlinc re-emits every class the current source set still produces.
Before kotlinc runs, Curie parses each .class file's JVM SourceFile attribute and deletes any whose value ends with .kt. kotlinc then puts back exactly what's still live. Anything not re-emitted (a deleted source, a removed top-level declaration) is gone.
Pure deletions wouldn't bump any surviving mtime, so the source list is also stamped to target/.kt-sources. Any change between builds — additions, deletions, or renames — forces a recompile so the wipe gets a chance to run.
What you'll see
Compile up to date Tests up to date Package up to date Done target/hello-0.1.0.jar
Compile 2 source file(s) [source changed] Stale (Kotlin) removed 1 orphan class file Tests ✔ 2 tests successful Done target/hello-0.1.0.jar
Forcing a clean rebuild
curie clean wipes the target/ directory entirely. The next build starts from a cold cache — same as a fresh clone.