The uika CLI is the engine every
build-tool integration runs. The plugins
fetch it, derive its arguments from the build, and print what it reports, so a
project with a supported build tool never has to invoke it by hand.
Use it directly when there is no plugin for your build, when you want to check
a hand-assembled classpath with check, or when you are reproducing what a
plugin ran. A plugin knows each module's resolved classpath, the release it
compiles for, and where its build outputs are. A command line knows none of the
three until you pass it.
The CLI is one jar, uika-cli-<version>.jar, attached to every GitHub
release and published to Maven
Central as net.exoego.uika:uika-cli:<version>:jvm@jar. It needs Java 17 or
newer. A JRE is enough, except for --jdk-release and a JDK upgrade check,
which read the JDK API from the JDK named by UIKA_JDK, else JAVA_HOME. They
never use the JVM running the CLI. Run it as
java -jar uika-cli-<version>.jar <command>. The examples below spell that
uika, which is the alias to set:
$ alias uika='java -jar /path/to/uika-cli-<version>.jar'The jar re-runs each command in a second JVM tuned for a short run. Options
given to java itself, such as -Xmx, do not reach that JVM. Pass memory
options through JAVA_TOOL_OPTIONS instead, or set UIKA_NO_RELAUNCH=1 to stay
in the JVM you started.
uika <command> --help lists every flag. The recipes below cover the ones
worth explaining.
Lists breaking changes between two versions of one library. Each line opens
with the change kind, which is also what --json puts in its kind field, so
jq '.breaking_changes[] | select(.kind == "class_became_final")' selects
those lines. The same snake_case kinds are what
--exclude-file
rules match on.
$ uika diff guava-22.0.jar guava-23.0-rc1.jar
...
CLASS BECAME FINAL com/google/common/collect/BoundType
...
METHOD ACCESS NARROWED com/google/common/collect/Iterators$ConcatenatedIterator.<init> (Ljava/util/Iterator;)V (public -> package-private)
...
FIELD REMOVED com/google/common/graph/GraphConstants.EDGE_CONNECTING_NOT_IN_GRAPH Ljava/lang/String;
...
breaking changes: 93 (classes: 26, methods: 61, fields: 6)Finds uses of those breaking changes across classpath JARs and your build output. This is the command no build-tool integration exposes, because it takes the compared pair and the classpath as arguments rather than reading them from a resolved build.
$ uika check --old kotlinx-coroutines-core-jvm-1.7.1.jar \
--new kotlinx-coroutines-core-jvm-1.11.0.jar \
--classpath ktor-io-jvm-2.3.13.jar:kotlin-stdlib-2.2.20.jar \
--app build/classes/kotlin/main
checked kotlinx-coroutines-core-jvm-1.7.1.jar -> kotlinx-coroutines-core-jvm-1.11.0.jar against 3 scan targets
--------------------------------------------------------------------------------
💥 reachable from the application (likely to break)
--------------------------------------------------------------------------------
❌ kotlinx.coroutines.EventLoopKt.processNextEventInCurrentThread()
method removed, throws NoSuchMethodError at first call
used by 1 class:
io.ktor.utils.io.jvm.javaio.BlockingAdapter (ktor-io-jvm-2.3.13.jar)
scanned 1346 classes: ❌ 1 broken (of which 💥 1 reachable, ⚠️ 0 not proven reachable)--oldand--newname the compared pair. Both are repeatable, so several changed libraries can be checked in one run.--classpathtakes the transitive dependencies,:-separated and repeatable. A path that does not exist, here or in--app, is skipped with a warning, so check the scan-target count on the first line of the report. A missing--oldor--newfile is an error.--apptakes your own build outputs, as class directories or JARs. They are the roots the reachability ranking walks from. Without them nothing is labelled⚠️ , so every violation counts as 💥 except a 💤 latent one.--classpath-filereads a dump written by any of the plugins and adds its artifacts and build outputs to the scan targets. It is more accurate than a hand-assembled classpath and reduces unverified references, so prefer it whenever a build can produce one.
Compares two dumps and checks every artifact whose version changed. This is what every plugin's check task runs.
$ uika upgrade-check --before /tmp/before.json --after /tmp/after.jsonThe report is the same one the README shows.
--merged-classpathchecks the union of all modules' classpaths as one flat classpath, instead of checking each module against its own resolution. It is also the automatic fallback for dumps that carry no per-module artifact data. Every plugin exposes it, since per-module checking costs one scan per module and a large monorepo may want the union instead.
Prints the API surface extracted from a JAR or directory. A debugging aid, and unrelated to the classpath dumps the plugins write.
$ uika dump some.jar-
--fail-ondecides the exit code only. The report is printed the same way regardless. -
--exclude-fileis repeatable, and rules from every file given are merged. -
--class-load-logis repeatable and takes text evidence, or a directory of it. The CLI does not decode JFR, which is why the plugins convert recordings before invoking it. It skips a recording, with a warning when the recording is passed directly. Without a plugin, convert a recording by hand:jfr print --json --events jdk.ClassLoad rec.jfr \ | jq -r '.recording.events[].values.loadedClass.name | select(startswith("[") | not) | "[class,load] \(.)"'
That yields tagged class-load lines the CLI reads (classes only, no triggers; the tag keeps default-package names accepted, and the filter drops array classes).
-
--draft-exclude-filewrites draft exclude rules from that evidence. It requires--class-load-log. -
--jdk-release Nlayers the JDK API of release N under the resolution scope, so hierarchy escapes into the JDK conclude instead of counting as unverified. Without it, a library class that adds a final override of a method it inherits from the JDK is neither reported nor counted. It is opt-in here and defaults to on in every plugin, because a plugin knows the release the build compiles for and the CLI does not. The API is read from$UIKA_JDKwhen set, else$JAVA_HOME. -
--jsonprints the report as JSON instead of text. No plugin exposes it. -
--verdicts-json <path>writes every reference verdict (ok, unknown, broken) to a file as JSON Lines, for checking uika's answers against a real JVM. It is not a report. It ignores--exclude-file, repeats a reference once per call site, and leaves out breaks that no single reference carries, such as a subclass of a class that became final or a stale service-provider entry. No plugin exposes it either.
--jdk-release-old N --jdk-release-new M makes the JDK upgrade itself the
compared pair, which supplies both sides, so --old and --new must be
omitted. Passing them alongside is rejected rather than ignored. A JDK API your
classpath still references and release M dropped is then reported like any
other removal.
$ uika check --jdk-release-old 11 --jdk-release-new 17 --classpath app.jar
checked JDK 11 -> JDK 17 against 1 scan target
❌ java.rmi.activation.ActivationGroup
class removed, throws NoClassDefFoundError at first use
used by 1 class:
UsesRemoved (app.jar)
scanned 1 classes: ❌ 1 brokenBoth releases are read from the one JDK uika finds, so checking an upgrade to the JDK you now run needs only that JDK. That JDK cannot be older than release M. The JDK API layer covers the two stub sources and why only a JDK 22 or later shows a class that became sealed.
From a build tool this needs no flag at all. Each dump records the API release
the application runs on, per module, which is what lets upgrade-check see the
move and check it on its own. See Checking a JDK
upgrade.
0 clean, 1 violations found per --fail-on, 2 error. Errors always exit
2 regardless of --fail-on.