Maven and Gradle builds, minus the wait.
One small static binary that resolves, compiles, tests and packages your project —
and hands anything it can't reproduce exactly back to the real tool.
$ cd my-project # any Maven or Gradle project, untouched
$ jv test # resolve, compile, run your JUnit tests
$ jv package # target/my-project-1.0.jarStatus: pre-1.0 (release candidates). The core build paths are covered by tests, but try jv on a branch before relying on it in CI. See Known limitations and Roadmap.
- Drop-in. It reads the
pom.xmlor Gradle build you already have. No config, no migration, no lock-in: deletejvand nothing about your project has changed. It shares~/.m2with Maven. - Never silently different. When a build uses something jv can't reproduce exactly (plugin executions,
filtered resources, custom source directories, ...), jv tells you why and runs the real
mvnorgradleinstead. You get the same result, just not the speed-up. - Checked against Maven. The test suite diffs jv's compile, runtime and test classpaths with
mvn dependency:build-classpathon real dependency graphs (Spring Boot, Hadoop, Hibernate, Netty, AWS SDK, Maven core, Jackson, HttpClient, Log4j, and more). - Fast where it counts.
- Parallel, checksum-verified downloads.
- Independent modules compile in parallel, in dependency order (
JV_JOBSsets the worker count). - A cached dependency resolution (
.jv/lock.json). javacis skipped when nothing changed, and a module that depends on another is only rebuilt when that module's public interface changed, not when just a method body did.- An optional shared build cache for downloaded artifacts (
JV_CACHE_URL, see Shared cache). - For Gradle, a single model query that is cached until your build scripts change.
- An optional persistent
javacworker (experimental, off by default; see Environment).
- Dependency tooling built in.
jv explain(why is this on my classpath?),jv audit(known vulnerabilities, via OSV) andjv upgrade(newer releases), plusjv initto start a project (see Dependency tools). - Editor-friendly.
jv lspis a small JSON-lines server that reports a project's classpath, source roots and coordinates (see Editor integration). - One static binary. Linux, macOS and Windows on amd64 and arm64. The only thing it needs is a JDK.
- Free software. GPL-3.0 (Copyright mxreal64, 2026).
Download a release. Grab the archive for your platform from the
Releases page, extract it, and put jv on your PATH
(Windows: unzip and add the folder to PATH). Every release includes checksums.txt:
sha256sum -c checksums.txt --ignore-missing # Linux
shasum -a 256 jv_*_darwin_*.tar.gz # macOS: compare the hash with checksums.txtOr build from source (Go 1.22+):
go install github.com/mxreal64/jv@latestYou need a JDK (JAVA_HOME or PATH). Maven or Gradle only has to be installed for the builds jv
delegates to them. Check it worked with jv --version.
jv test # resolve, compile and run JUnit tests
jv package # test, then build target/<artifactId>-<version>.jar
jv install # ...and install it into ~/.m2
jv classpath test # print the test classpath (IDEs, scripts, debugging)
jv tree # print the resolved dependency tree
jv explain guava # why is guava on the classpath, and who asked for which version?
jv audit # check the dependencies against the OSV vulnerability database
jv upgrade # which dependencies have a newer release?
jv init # scaffold a new Maven project in the current (empty) directory
jv -pl app test # one module of a multi-module build, plus the siblings it needs
jv clean install # goals run in the order you type them
jv -X test # verbose: also say whether the build ran natively or was delegatedWant it for every build? alias mvn=jv in your shell: anything jv doesn't handle goes to the real
mvn. (Use an alias, not a symlink.)
jv <goal>
|
|-- pom.xml ----------> can jv reproduce this build exactly?
| |-- yes --> native: resolve -> javac -> JUnit -> jar
| '-- no --> "delegating to mvn because ..." (your build, unchanged)
|
'-- build.gradle[.kts] -> ask Gradle once for a model (cached in .jv/)
|-- reproducible --> the same native steps
'-- otherwise --> Gradle itself, arguments translated
jv prefers a loud hand-off to a quiet difference. See Maven projects and Gradle projects below for exactly what runs natively and what is delegated.
jv compile | test | package | install | clean
jv classpath [compile|runtime|test] jv tree jv explain <dependency>
jv audit [-Daudit.failOn=none|low|medium|high|critical] [-Daudit.json=true]
jv upgrade [-Dupgrade.all=true] [-Dupgrade.json=true]
jv init [-DgroupId=g] [-DartifactId=a] [-Dversion=v] [-Dtype=spring-boot]
jv lsp
jv -o -q -X -U -P profile -Dkey=value -pl module1,module2 -s settings.xml <goal>
jv --version | -h
jv picks the build system for you: a pom.xml means Maven; otherwise a Gradle build
(build.gradle[.kts], settings.gradle[.kts] or gradlew, in the directory or an ancestor) means Gradle.
Any other goal, plugin or flag is passed to the real tool untouched.
- Dependency resolution with Maven semantics: parent POMs,
${}interpolation (including inside property values),dependencyManagement, BOM imports, exclusions, optional dependencies, scope mediation (a widened scope also widens the transitive children), nearest-wins conflicts, version ranges, and an exact port of Maven'sComparableVersionordering. - Profiles:
-P/-P!id,activeByDefault, and activation by property,<jdk>,<os>and<file>; modules declared inside profiles are part of the reactor. - Remote
-SNAPSHOTdependencies (unique and non-unique layouts);-Ure-checks them. - Multi-module reactors: independent modules compile in parallel (one worker per CPU, or
JV_JOBS), in dependency order. Tests, packaging andinstallthen run one module at a time, like Maven, so tests of different modules never overlap. Siblings resolve to their build output.-pl a,bbuilds the named modules plus the siblings they need (so-amis accepted and ignored) andcleanwith-plcleans only those modules. cleanin any position: a leadingcleantakes a fast path that needs no network or POM resolution; acleananywhere else (jv install clean) runs at its position in the goal list.~/.m2/settings.xml: mirrors, credentials,localRepository,<profiles>(repositories and properties),<proxies>; POM-declared repositories; SHA-1 verification. Encrypted server passwords ({...}) are decrypted with the master password in~/.m2/settings-security.xml.- Build steps: resource copying that also removes resources you deleted from
src/*/resources, skip-if- unchangedjavac, JUnit Platform tests (the launcher is fetched automatically; XML reports go totarget/surefire-reports), jars with directory entries, andinstallinto the local repository. - Per-project overrides in a
jv.toml(see below). - Compiler settings:
maven.compiler.*properties andmaven-compiler-plugin<release>,<source>,<target>,<encoding>. Annotation processors on the classpath keep working on javac 23+ (jv passes-proc:fullon javac 21+). - Plugin management:
<pluginManagement>entries formaven-compiler-plugin,maven-surefire-pluginandmaven-jar-pluginare read, because Maven applies them to the default lifecycle without the plugin being declared. - Test selection (
maven-surefire-plugin):<includes>/<excludes>(Ant globs such as**/*IT.java),<groups>/<excludedGroups>(JUnit tags),<argLine>and<systemPropertyVariables>.-Dtest=...overrides includes/excludes, as in Surefire. - Flags:
-DskipTests,-Dmaven.test.skip,-Dtest=...,-Dmaven.test.failure.ignore=true,-o(offline),-q(quiet),-X(verbose).-B -e -ntp -V -amare accepted and ignored.
-
Plugins and any goal that isn't in the list above; packagings other than
jar/pom(war, ear, maven-plugin, ...); projects without a readablepom.xml. -
Any flag jv doesn't know (
-T,-f, ...). -
POMs, including inherited parent POMs, that customise the build in ways jv doesn't reproduce by default:
sourceDirectory/testSourceDirectory,finalName;- non-default or filtered
resources/testResources; - compiler configuration other than release/source/target/encoding (
compilerArgs,annotationProcessorPaths, ...); - surefire configuration beyond the list above (
forkCount,parallel, ...),%regex[...]/%ant[...]test patterns, and an<argLine>that uses@{...}late binding (jacoco); - any plugin
<executions>(shade, spring-boot repackage, flatten, jacoco, enforcer, ...).
jv prints which setting caused the delegation.
jv contains native implementations for a few common plugins: spring-boot-maven-plugin (repackage),
maven-shade-plugin (fat jars, relocation, resource merging), flatten-maven-plugin,
maven-enforcer-plugin, jacoco-maven-plugin, plus resource filtering with property interpolation and
generator hooks. Because they are not yet checked against Maven as thoroughly as the core build, builds that
declare them are delegated to mvn by default.
To run them natively, set JV_IGNORE_BUILD_CONFIG=1. jv then builds natively even when the POM has settings
it doesn't reproduce, and prints a warning naming them:
JV_IGNORE_BUILD_CONFIG=1 jv packageCompare the result with mvn package before trusting it, especially for fat jars.
jv does not re-implement the Groovy/Kotlin DSL. It asks a real Gradle (the wrapper if present) once for a
JSON model through an init script (source dirs, resolved compile/runtime/test classpaths, javac and test
settings), caches it in .jv/gradle-model.json keyed by the build scripts, gradle.properties, version
catalogs and -P/-D arguments, then compiles, tests and packages natively. Later runs do not start Gradle at
all until those inputs change.
- Native:
clean,compile,test,package(likebuild: it runs the tests),classpath.classpathnames exactly one project (-pl, or the project you are in). - Through Gradle (the bridge): everything else, and any build the model flags as not reproducible
(Kotlin/Groovy/Scala, war, annotation processors, several source dirs, TestNG, extra tasks wired into
compileJava, non-core plugins, a project-level dependency cycle).JV_GRADLE_ORACLE=0always uses the bridge;JV_IGNORE_BUILD_CONFIG=1builds natively despite the warnings. - Bridge translation:
compile→classes,test→test,package→build,install→publishToMavenLocal,tree→dependencies,-DskipTests→-x test,-o→--offline,-X→--info,-pl a,b→:a:<task> :b:<task>;-s settings.xmlis dropped (it has no Gradle equivalent). Gradle's exit code is returned unchanged. - Safety:
jv cleanrefuses to delete a build directory that is, or contains, the project or its sources (a script can setbuildDir = projectDir); nothing is deleted if any target looks wrong.
These read the resolved dependency graph of a Maven project (they need a pom.xml) and never build anything.
jv explain <dependency> shows every path to a dependency in the resolved tree, then everything that
declares it and which version each one asked for. That makes version conflicts that Maven's "nearest wins"
rule hides easy to spot. The argument is artifactId, groupId:artifactId or groupId:artifactId:version.
$ jv explain libc
demo:app:1.0
org.acme:liba:1.0 -> org.acme:libc:1.0
declared by:
org.acme:liba:1.0 wants 1.0
org.acme:libb:1.0 wants 2.0jv audit looks every resolved artifact up in the OSV database and prints findings
by severity (CVSS v3 scores are computed when an advisory has no label). It exits 0 when clean, 1 when
there are findings and 2 when the audit itself could not run (offline, API down), so a failed audit never
looks like a clean one. -Daudit.failOn=high only fails on HIGH or CRITICAL, -Daudit.failOn=none never
fails, -Daudit.json=true prints JSON. JV_OSV_URL points at a mirror. -SNAPSHOT and unversioned artifacts
are skipped.
jv upgrade lists direct dependencies that have a newer release, from the repositories'
maven-metadata.xml: org.acme:liba 1.0 -> 2.0.0 (major). Pre-releases are not offered unless you are on
one, -jre/-android style variants are kept, and artifacts that are in no repository are skipped.
-Dupgrade.all=true includes transitive dependencies. It only reports; it never edits your POM.
jv init creates pom.xml, a sample class and test, and a .gitignore in the current directory
(-DartifactId defaults to the directory name). It refuses to run if a pom.xml or Gradle build is already
there and never overwrites anything.
An optional jv.toml next to a module's pom.xml overrides POM settings for jv without touching the POM:
[build]
release = 21 # javac --release (also: source, encoding)
[test]
includes = ["**/*Test.java", "**/*IT.java"]
excludes = ["**/Slow*.java"]
groups = "fast" # JUnit tags to run (also: excludedGroups)
argLine = "-Xmx1g" # JVM arguments for the test runSettings in jv.toml win over the POM, and -D on the command line wins over both. Unknown keys produce a
warning (so typos don't pass silently) and a syntax error stops the build with the line number. [[plugin]]
and [[generator]] tables are recognised but not run yet; jv stops with an error instead of silently
skipping them. jv.toml is ignored when a build is delegated to mvn (jv says so), and it does not apply to
Gradle projects.
Set JV_CACHE_URL to share downloaded artifacts between machines or CI jobs through a jv-cache server:
jv-cache -dir /var/cache/jv -addr :8080 -token s3cret # on the cache host
JV_CACHE_URL=http://cache.internal:8080 JV_CACHE_TOKEN=s3cret jv testjv asks the cache before downloading an artifact and uploads what it downloaded. The cache can't poison a
build: entries are keyed by the SHA-256 of the checksum the origin repository publishes, and a cached file is
only used when its own SHA-1 matches that checksum (otherwise jv downloads it normally). Artifacts without a
published checksum are never shared. A cache that is down or slow only costs a download. Use https (or a
trusted network): the token and traffic are not protected over plain http, and jv warns when you do.
jv lsp runs a small server over stdin/stdout for editors and scripts. It reads one JSON request per line
and writes one JSON response per line ({"result": ...} or {"error": "..."}):
$ echo '{"method":"projectInfo","params":{}}' | jv lsp
{"result":{"artifact":"hello","group":"demo","modules":[{"group":"demo","artifact":"hello","version":"1.0","dir":"/work/hello"}],"version":"1.0"}}| Method | Params | Result |
|---|---|---|
classpath |
{"scope": "compile" | "runtime" | "test"} (default test) |
{"classpath": [...]} (null when the project has no dependencies) |
sourceRoots |
none | {"main": [...], "test": [...]} |
projectInfo |
none | {"group", "artifact", "version", "modules": [...]} |
Resolution is cached and refreshed automatically when a module's pom.xml changes.
- Credentials from
settings.xmlare only sent to Maven Central, settings mirrors, and repositories declared in the project's own POMs, never to repositories that only a dependency's POM declared. - Plain-
httprepositories declared in POMs are ignored (as in Maven 3.8.1+);localhostis allowed and anhttpmirror configured insettings.xmlis honoured. - Downloads are verified against the repository's
.sha1when it provides one (-Xshows artifacts that couldn't be verified). That detects corruption, not a malicious repository;JV_REQUIRE_CHECKSUMS=1refuses artifacts without a checksum. - Encrypted passwords: a server
<password>of the form{...}is decrypted with the master password from~/.m2/settings-security.xml(itself encrypted, as written bymvn --encrypt-master-password). If decryption fails at build time jv warns once and sends no credentials rather than a wrong password. The decrypted password lives in memory only and is never logged.
| Variable | Meaning |
|---|---|
JV_REPO |
local repository (overrides settings.xml; relative paths are resolved at startup) |
JV_JUNIT_VERSION |
junit-platform-console-standalone version to fetch (default 1.10.2) |
JV_IGNORE_BUILD_CONFIG |
build natively even if the POM/Gradle model has settings jv doesn't reproduce (also enables the experimental plugins) |
JV_GRADLE_ORACLE |
0 = never use the native Gradle path, always run Gradle itself |
JV_REQUIRE_CHECKSUMS |
refuse artifacts whose repository provides no .sha1 |
JV_JOBS |
number of modules compiled at once in a multi-module build (default: one per CPU) |
JV_CACHE_URL / JV_CACHE_TOKEN |
shared cache server and its bearer token |
JV_OSV_URL |
OSV API base URL used by jv audit (default https://api.osv.dev) |
JV_JAVAC_WORKER |
1 = compile through a persistent javac worker process (experimental, off by default; it helps repeated compiles in one session and costs a JVM start otherwise; falls back to plain javac if it can't start) |
JAVA_HOME |
JDK used for javac/java (wins over PATH, like Maven and Gradle) |
-f/--fileis not handled natively (it is delegated tomvn); a non-default POM is only supported as a<module>entry.- Password decryption reads
~/.m2/settings-security.xmlonly and covers server passwords, not proxy passwords. - Surefire: only Ant-style
includes/excludes; a pattern without a leading**/matches the default package only (as in Surefire). An<argLine>that still contains${...}after interpolation drops those tokens. - Compilation is whole-module, not per-class.
- Independent modules compile in parallel, so compiler output and log lines of different modules can
interleave; tests, packaging and
installrun one module at a time in dependency order (like Maven). Results are the same for successful builds; failure behaviour can differ slightly. - TestNG is not supported natively; test discovery uses JUnit Platform engines found on the classpath.
- Classpath order follows Maven's tree order but is not guaranteed identical in every edge case (the set of jars is).
These pieces exist in the repository but are not connected to the jv command yet, so don't rely on them:
[[plugin]]and[[generator]]injv.toml: a subprocess plugin protocol exists (plugin/), but plugins are not downloaded and nothing runs them; generators are not run either (jv refuses such ajv.toml).jv-workspace.toml(model/): parsed, never used.- A static
build.gradle.ktsparser (gradle/ktsparse) to avoid starting Gradle at all. - A local content-addressable artifact store (
resolver/cacache.go). - Running the experimental plugins natively by default, once they are covered by Maven-parity tests.
./bench.sh times jv against mvn, mvnd and Gradle on a generated project or one of your own:
./bench.sh --jv ./jv --runs 7 --warmup 2 --out bench-results.md
./bench.sh --project path/to/your/project --no-gradleSee ./bench.sh (the header comment) for all options. Speed-ups depend heavily on the project and on how
much of it builds natively, so measure your own.
./test-all.sh is the single entry point (see --help):
| Tier | What runs |
|---|---|
--quick |
gofmt, go vet, go test -race, cross-compiles for linux/darwin/windows (no network) |
| default | quick + end-to-end scenarios against Maven Central: incremental builds, tests passing/failing, scopes, exclusions, profiles, reactor builds, install, offline mode, delegation, and Gradle (with a stub gradle, plus a real-Gradle run when Gradle is installed) |
--full |
default + heavier dependency graphs (hadoop, hibernate, netty, aws sdk) + a corpus of real open-source projects |
--corpus-only |
just the corpus |
If mvn is installed, every classpath check also diffs jv against mvn dependency:build-classpath for the
compile, runtime and test scopes, and the corpus compares compile output with Maven's. That parity check is
the real definition of "correct" for this tool. --corpus-file FILE takes
name|git url|ref|subdir|cp|build lines.
Bug reports and pull requests are welcome; see CONTRIBUTING.md. Run
./test-all.sh --quick before sending a change, and ./test-all.sh for anything that affects resolution or
builds. Report security problems privately (see SECURITY.md).
jv is free software, licensed under the GNU General Public License, version 3 only
(SPDX: GPL-3.0-only); see LICENSE. Copyright mxreal64, 2026. Every source file
carries an SPDX header. Contributions are accepted under the same license.