jgvariant-tool: New module.
Adds a command line tool that can read and (in the future) manipulate
GVariant-formatted files.
Change-Id: Icc92eb409a97e7cf72dfd7535f6a8b3587dd4a48
diff --git a/README.md b/README.md
index d01d141..caa9938 100644
--- a/README.md
+++ b/README.md
@@ -40,7 +40,31 @@
List<ExampleRecord> example = decoder.decode(ByteBuffer.wrap(bytes));
-## Installation
+## Command line tool
+
+The `jgvariant-tool` module contains a tool called `jgvariant` that can
+be used to manipulate [GVariant][]-formatted files from the command line.
+Its primary purpose is to enable the scripting of [OSTree][] repository
+management tasks.
+
+Usage example (dumping the contents of an [OSTree][] summary file):
+
+ $ jgvariant ostree summary read ./jgvariant-ostree/src/test/resources/ostree/summary
+
+You can build the tool either as a shaded JAR or as a native executable.
+
+To build and run a shaded JAR:
+
+ $ mvn package -pl jgvariant-tool -am -Pshade
+ $ java -jar /home/mulk/Arbeitskasten/jgvariant/jgvariant-tool/target/jgvariant-tool-*.jar
+
+To build and run a native executable:
+
+ $ mvn package -pl jgvariant-tool -am -Pnative
+ $ ./jgvariant-tool/target/jgvariant
+
+
+## Library installation
### Usage with Maven
diff --git a/jgvariant-parent/pom.xml b/jgvariant-parent/pom.xml
index 91acdfa..4a2c2f2 100644
--- a/jgvariant-parent/pom.xml
+++ b/jgvariant-parent/pom.xml
@@ -59,10 +59,12 @@
<failsafe-plugin.version>${surefire-plugin.version}</failsafe-plugin.version>
<flatten-plugin.version>1.5.0</flatten-plugin.version>
<jar-plugin.version>3.3.0</jar-plugin.version>
+ <jpackage-plugin.version>0.1.5</jpackage-plugin.version>
<maven-scm-plugin.version>2.0.1</maven-scm-plugin.version>
<maven-gpg-plugin.version>3.1.0</maven-gpg-plugin.version>
<maven-javadoc-plugin.version>3.6.3</maven-javadoc-plugin.version>
<maven-source-plugin.version>3.3.0</maven-source-plugin.version>
+ <native-plugin.version>0.9.23</native-plugin.version>
<nexus-staging-plugin.version>1.6.13</nexus-staging-plugin.version>
<spotless-plugin.version>2.41.1</spotless-plugin.version>
<surefire-plugin.version>3.2.2</surefire-plugin.version>
@@ -71,10 +73,13 @@
<apiguardian.version>1.1.2</apiguardian.version>
<errorprone.version>2.23.0</errorprone.version>
<google-java-format.version>1.15.0</google-java-format.version>
+ <guava.version>32.1.3-jre</guava.version>
<inject-resources.version>0.3.3</inject-resources.version>
<jetbrains-annotations.version>24.1.0</jetbrains-annotations.version>
<junit-jupiter.version>5.10.1</junit-jupiter.version>
<nullaway.version>0.10.18</nullaway.version>
+ <picocli.version>4.7.4</picocli.version>
+ <yasson.version>3.0.2</yasson.version>
<xz.version>1.9</xz.version>
</properties>
@@ -111,6 +116,27 @@
<version>${xz.version}</version>
</dependency>
+ <!-- Command line tooling -->
+ <dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli</artifactId>
+ <version>${picocli.version}</version>
+ </dependency>
+
+ <!-- JSON -->
+ <dependency>
+ <groupId>org.eclipse</groupId>
+ <artifactId>yasson</artifactId>
+ <version>${yasson.version}</version>
+ </dependency>
+
+ <!-- Guava -->
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <version>${guava.version}</version>
+ </dependency>
+
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
@@ -240,6 +266,18 @@
</executions>
</plugin>
+ <plugin>
+ <groupId>org.graalvm.buildtools</groupId>
+ <artifactId>native-maven-plugin</artifactId>
+ <version>${native-plugin.version}</version>
+ </plugin>
+
+ <plugin>
+ <groupId>com.github.akman</groupId>
+ <artifactId>jpackage-maven-plugin</artifactId>
+ <version>${jpackage-plugin.version}</version>
+ </plugin>
+
</plugins>
</pluginManagement>
diff --git a/jgvariant-tool/pom.xml b/jgvariant-tool/pom.xml
new file mode 100644
index 0000000..ee2b8a8
--- /dev/null
+++ b/jgvariant-tool/pom.xml
@@ -0,0 +1,239 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+
+SPDX-License-Identifier: GPL-3.0-or-later
+-->
+
+<project
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
+ xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+ <modelVersion>4.0.0</modelVersion>
+
+ <version>0.1.8-SNAPSHOT</version>
+
+ <artifactId>jgvariant-tool</artifactId>
+ <packaging>jar</packaging>
+
+ <name>JGVariant Command Line Tool</name>
+ <url>https://gerrit.benkard.de/plugins/gitiles/jgvariant</url>
+
+ <description>
+ GVariant command line tool.
+ </description>
+
+ <parent>
+ <groupId>eu.mulk.jgvariant</groupId>
+ <artifactId>jgvariant-parent</artifactId>
+ <version>0.1.8-SNAPSHOT</version>
+ <relativePath>../jgvariant-parent/pom.xml</relativePath>
+ </parent>
+
+ <dependencies>
+ <!-- JGVariant -->
+ <dependency>
+ <groupId>eu.mulk.jgvariant</groupId>
+ <artifactId>jgvariant-core</artifactId>
+ <version>0.1.8-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>eu.mulk.jgvariant</groupId>
+ <artifactId>jgvariant-ostree</artifactId>
+ <version>0.1.8-SNAPSHOT</version>
+ </dependency>
+
+ <!-- Annotations -->
+ <dependency>
+ <groupId>com.google.errorprone</groupId>
+ <artifactId>error_prone_annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.jetbrains</groupId>
+ <artifactId>annotations</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apiguardian</groupId>
+ <artifactId>apiguardian-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <!-- Command line tooling -->
+ <dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli</artifactId>
+ </dependency>
+
+ <!-- JSON -->
+ <dependency>
+ <groupId>org.eclipse</groupId>
+ <artifactId>yasson</artifactId>
+ </dependency>
+
+ <!-- Guava -->
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <configuration>
+ <annotationProcessorPaths>
+ <path>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli-codegen</artifactId>
+ <version>${picocli.version}</version>
+ </path>
+ </annotationProcessorPaths>
+ <compilerArgs>
+ <arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
+ </compilerArgs>
+ </configuration>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifest>
+ <mainClass>eu.mulk.jgvariant.tool.Main</mainClass>
+ </manifest>
+ </archive>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+
+ <profiles>
+ <profile>
+ <id>shade</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+
+ <profile>
+ <id>uberjar</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ <configuration>
+ <archive>
+ <manifest>
+ <mainClass>eu.mulk.jgvariant.tool.Main</mainClass>
+ </manifest>
+ </archive>
+ <descriptorRefs>
+ <descriptorRef>jar-with-dependencies</descriptorRef>
+ </descriptorRefs>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+
+ <profile>
+ <id>native</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.graalvm.buildtools</groupId>
+ <artifactId>native-maven-plugin</artifactId>
+ <extensions>true</extensions>
+ <executions>
+ <execution>
+ <id>build-native</id>
+ <goals>
+ <goal>compile-no-fork</goal>
+ </goals>
+ <phase>package</phase>
+ </execution>
+ <execution>
+ <id>test-native</id>
+ <goals>
+ <goal>test</goal>
+ </goals>
+ <phase>test</phase>
+ </execution>
+ </executions>
+ <configuration>
+ <debug>false</debug>
+ <fallback>false</fallback>
+ <buildArgs>
+ <arg>-O3</arg>
+ <arg>--strict-image-heap</arg>
+ </buildArgs>
+ <imageName>jgvariant</imageName>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+
+ <profile>
+ <id>jpackage</id>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.github.akman</groupId>
+ <artifactId>jpackage-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>jpackage</goal>
+ </goals>
+ <configuration>
+ <type>IMAGE</type>
+ <module>eu.mulk.jgvariant.tool/eu.mulk.jgvariant.tool.Main</module>
+ <modulepath>
+ <dependencysets>
+ <dependencyset>
+ <includeoutput>true</includeoutput>
+ <excludeautomatic>true</excludeautomatic>
+ </dependencyset>
+ </dependencysets>
+ </modulepath>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+
+</project>
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/Main.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/Main.java
new file mode 100644
index 0000000..64e375a
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/Main.java
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool;
+
+import static java.util.logging.Level.WARNING;
+
+import java.util.logging.Logger;
+import picocli.CommandLine;
+
+/**
+ * A command line tool to read and manipulate GVariant-formatted files.
+ *
+ * <p>Also provides ways to manipulate OSTree repositories.
+ */
+public final class Main {
+ static {
+ Logger.getGlobal().setLevel(WARNING);
+ }
+
+ public static void main(String[] args) {
+ int exitCode = new CommandLine(new MainCommand()).execute(args);
+ System.exit(exitCode);
+ }
+
+ private Main() {}
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/MainCommand.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/MainCommand.java
new file mode 100644
index 0000000..7b25dfc
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/MainCommand.java
@@ -0,0 +1,115 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool;
+
+import static java.util.logging.Level.*;
+
+import eu.mulk.jgvariant.ostree.Summary;
+import eu.mulk.jgvariant.tool.jsonb.*;
+import jakarta.json.bind.Jsonb;
+import jakarta.json.bind.JsonbBuilder;
+import jakarta.json.bind.JsonbConfig;
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.ByteBuffer;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.util.logging.Logger;
+import org.jetbrains.annotations.VisibleForTesting;
+import picocli.AutoComplete;
+import picocli.CommandLine;
+import picocli.CommandLine.*;
+
+@Command(
+ name = "jgvariant",
+ mixinStandardHelpOptions = true,
+ description = "Manipulate files in GVariant format.",
+ subcommands = {MainCommand.OstreeCommand.class, AutoComplete.GenerateCompletion.class})
+final class MainCommand {
+
+ private static final Logger LOG = Logger.getLogger("eu.mulk.jgvariant.tool");
+
+ private static final Jsonb jsonb =
+ JsonbBuilder.newBuilder()
+ .withConfig(
+ new JsonbConfig()
+ .withFormatting(true)
+ .withAdapters(ChecksumAdapter.INSTANCE)
+ .withSerializers(
+ ByteStringSerializer.INSTANCE,
+ ByteArraySerializer.INSTANCE,
+ SignatureSerializer.INSTANCE,
+ VariantSerializer.INSTANCE))
+ .build();
+
+ @Option(
+ names = {"-v", "--verbose"},
+ description = "Enable verbose logging.",
+ scope = CommandLine.ScopeType.INHERIT)
+ void setVerbose(boolean[] verbose) {
+ Logger.getGlobal()
+ .setLevel(
+ switch (verbose.length) {
+ case 0 -> WARNING;
+ case 1 -> INFO;
+ case 2 -> FINE;
+ default -> ALL;
+ });
+ }
+
+ @Command(
+ name = "ostree",
+ mixinStandardHelpOptions = true,
+ description = "Manipulate OSTree files.",
+ subcommands = {OstreeCommand.SummaryCommand.class})
+ static final class OstreeCommand {
+
+ @Command(
+ name = "summary",
+ mixinStandardHelpOptions = true,
+ description = "Manipulate OSTree summary files.")
+ static final class SummaryCommand extends BaseCommand {
+
+ @Command(mixinStandardHelpOptions = true)
+ void read(@Parameters(paramLabel = "<file>") File file) throws IOException {
+ LOG.fine(() -> "Reading file %s".formatted(file));
+ var fileBytes = ByteBuffer.wrap(Files.readAllBytes(fs().getPath(file.getPath())));
+ var decoder = Summary.decoder();
+ var thing = decoder.decode(fileBytes);
+ out().println(jsonb.toJson(thing));
+ }
+
+ SummaryCommand() {}
+ }
+
+ OstreeCommand() {}
+ }
+
+ @Command
+ abstract static class BaseCommand {
+
+ @Spec CommandLine.Model.CommandSpec spec;
+
+ @VisibleForTesting FileSystem fs = FileSystems.getDefault();
+
+ protected BaseCommand() {}
+
+ protected PrintWriter out() {
+ return spec.commandLine().getOut();
+ }
+
+ protected PrintWriter err() {
+ return spec.commandLine().getErr();
+ }
+
+ protected FileSystem fs() {
+ return fs;
+ }
+ }
+
+ MainCommand() {}
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteArraySerializer.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteArraySerializer.java
new file mode 100644
index 0000000..7f80440
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteArraySerializer.java
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import java.util.HexFormat;
+
+@SuppressWarnings("java:S6548")
+public final class ByteArraySerializer implements JsonbSerializer<byte[]> {
+
+ public static final ByteArraySerializer INSTANCE = new ByteArraySerializer();
+
+ private ByteArraySerializer() {}
+
+ @Override
+ public void serialize(
+ byte[] o, JsonGenerator jsonGenerator, SerializationContext serializationContext) {
+ jsonGenerator.write(HexFormat.of().formatHex(o));
+ }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteStringSerializer.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteStringSerializer.java
new file mode 100644
index 0000000..3537c3d
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ByteStringSerializer.java
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import eu.mulk.jgvariant.ostree.ByteString;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+
+@SuppressWarnings("java:S6548")
+public final class ByteStringSerializer implements JsonbSerializer<ByteString> {
+
+ public static final ByteStringSerializer INSTANCE = new ByteStringSerializer();
+
+ private ByteStringSerializer() {}
+
+ @Override
+ public void serialize(
+ ByteString o, JsonGenerator jsonGenerator, SerializationContext serializationContext) {
+ jsonGenerator.write(o.hex());
+ }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ChecksumAdapter.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ChecksumAdapter.java
new file mode 100644
index 0000000..58a8951
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/ChecksumAdapter.java
@@ -0,0 +1,27 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import eu.mulk.jgvariant.ostree.ByteString;
+import eu.mulk.jgvariant.ostree.Checksum;
+import jakarta.json.bind.adapter.JsonbAdapter;
+
+@SuppressWarnings("java:S6548")
+public final class ChecksumAdapter implements JsonbAdapter<Checksum, ByteString> {
+
+ public static final ChecksumAdapter INSTANCE = new ChecksumAdapter();
+
+ private ChecksumAdapter() {}
+
+ @Override
+ public ByteString adaptToJson(Checksum obj) throws Exception {
+ return obj.byteString();
+ }
+
+ @Override
+ public Checksum adaptFromJson(ByteString obj) throws Exception {
+ return new Checksum(obj);
+ }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/SignatureSerializer.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/SignatureSerializer.java
new file mode 100644
index 0000000..dca1044
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/SignatureSerializer.java
@@ -0,0 +1,24 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import eu.mulk.jgvariant.core.Signature;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+
+@SuppressWarnings("java:S6548")
+public final class SignatureSerializer implements JsonbSerializer<Signature> {
+
+ public static final SignatureSerializer INSTANCE = new SignatureSerializer();
+
+ private SignatureSerializer() {}
+
+ @Override
+ public void serialize(
+ Signature o, JsonGenerator jsonGenerator, SerializationContext serializationContext) {
+ jsonGenerator.write(o.toString());
+ }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/VariantSerializer.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/VariantSerializer.java
new file mode 100644
index 0000000..99ff553
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/jsonb/VariantSerializer.java
@@ -0,0 +1,43 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package eu.mulk.jgvariant.tool.jsonb;
+
+import com.google.common.primitives.Bytes;
+import eu.mulk.jgvariant.core.Signature;
+import eu.mulk.jgvariant.core.Variant;
+import jakarta.json.bind.serializer.JsonbSerializer;
+import jakarta.json.bind.serializer.SerializationContext;
+import jakarta.json.stream.JsonGenerator;
+import java.text.ParseException;
+import java.util.List;
+
+@SuppressWarnings("java:S6548")
+public final class VariantSerializer implements JsonbSerializer<Variant> {
+
+ public static final VariantSerializer INSTANCE = new VariantSerializer();
+
+ private final ByteArraySerializer byteArraySerializer = ByteArraySerializer.INSTANCE;
+
+ private final Signature byteArraySignature;
+
+ private VariantSerializer() {
+ try {
+ byteArraySignature = Signature.parse("ay");
+ } catch (ParseException e) {
+ // impossible
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public void serialize(Variant obj, JsonGenerator generator, SerializationContext ctx) {
+ if (obj.signature().equals(byteArraySignature)) {
+ byteArraySerializer.serialize(Bytes.toArray((List<Byte>) obj.value()), generator, ctx);
+ } else {
+ ctx.serialize(obj.value(), generator);
+ }
+ }
+}
diff --git a/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/package-info.java b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/package-info.java
new file mode 100644
index 0000000..da5c3ba
--- /dev/null
+++ b/jgvariant-tool/src/main/java/eu/mulk/jgvariant/tool/package-info.java
@@ -0,0 +1,8 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+@API(status = API.Status.INTERNAL)
+package eu.mulk.jgvariant.tool;
+
+import org.apiguardian.api.API;
diff --git a/jgvariant-tool/src/main/java/module-info.java b/jgvariant-tool/src/main/java/module-info.java
new file mode 100644
index 0000000..e66981c
--- /dev/null
+++ b/jgvariant-tool/src/main/java/module-info.java
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: © 2023 Matthias Andreas Benkard <code@mail.matthias.benkard.de>
+//
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+module eu.mulk.jgvariant.tool {
+ requires transitive eu.mulk.jgvariant.ostree;
+ requires com.google.common;
+ requires info.picocli;
+ requires jakarta.json;
+ requires jakarta.json.bind;
+ requires java.logging;
+ requires static com.google.errorprone.annotations;
+ requires static org.apiguardian.api;
+ requires static org.jetbrains.annotations;
+
+ opens eu.mulk.jgvariant.tool to
+ info.picocli;
+
+ exports eu.mulk.jgvariant.tool;
+}
diff --git a/pom.xml b/pom.xml
index be9fd26..1ab4b48 100644
--- a/pom.xml
+++ b/pom.xml
@@ -37,6 +37,8 @@
<module>jgvariant-core</module>
<module>jgvariant-ostree</module>
+ <module>jgvariant-tool</module>
+
<module>jgvariant-bom</module>
</modules>