Skip to content

jdt2jar: offline JTD-to-JAR compiler CLI + minimal distroless container #147

@simbo1905

Description

@simbo1905

Goal

Build jdt2jar: a CLI tool + minimal container that compiles JTD schemas into standalone validator JARs at build time, eliminating the need for a JDK 24+ runtime.

User Story

# On any machine with Docker (or natively with JDK 24+)
docker run --rm -v $(pwd)/schemas:/schemas -v $(pwd)/out:/out \
  ghcr.io/simbo1905/jdt2jar:latest \
  /schemas/user.jtd.json --output /out/user-validator.jar

# The output JAR runs on JDK 21+ with zero ClassFile API dependency
java -jar /out/user-validator.jar --validate /data/payload.json

CLI Interface

jdt2jar <schema.json> [options]

Options:
  --output <path>       Output JAR path (default: <schema-name>-validator.jar)
  --package <name>      Java package for generated classes (default: jtd.generated)
  --class <name>        Validator class name (default: SchemaValidator)
  --main                Include a main() for standalone CLI validation
  --runtime <version>   Target bytecode version (default: 21)
  --include-sources     Also output generated .java files alongside the JAR
  --help                Show help

Generated JAR Contents

user-validator.jar
├── META-INF/MANIFEST.MF
├── jtd/generated/SchemaValidator.class      # implements JtdValidator
├── jtd/generated/SchemaValidator$*.class    # helper classes
├── jtd/generated/SchemaValidator.java       # (optional, --include-sources)
└── jtd/schema.json                          # embedded original schema

Usage Patterns

1. Library Integration

// Generated class implements JtdValidator
JtdValidator validator = new jtd.generated.SchemaValidator();
JtdValidationResult result = validator.validate(jsonPayload);
if (!result.isValid()) {
    for (var err : result.errors()) {
        System.err.println(err.instancePath() + ": " + err.schemaPath());
    }
}

2. Standalone CLI Validation

# JAR built with --main flag
java -jar user-validator.jar --validate payload.json
# Exit code 0 = valid, 1 = invalid, 2 = parse error

java -jar user-validator.jar --validate payload.json --format json
# Outputs RFC 8927 error pairs as JSON

3. CI/CD Pipeline

# GitHub Actions example
- name: Compile JTD validators
  run: |
    docker run --rm -v $PWD/schemas:/schemas -v $PWD/validators:/out \
      ghcr.io/simbo1905/jdt2jar:latest \
      /schemas/events.jtd.json --output /out/events-validator.jar --main

- name: Validate test fixtures
  run: java -jar validators/events-validator.jar --validate tests/fixtures/event.json

4. Maven Plugin (future)

<plugin>
  <groupId>io.github.simbo1905.json</groupId>
  <artifactId>jdt2jar-maven-plugin</artifactId>
  <executions>
    <execution>
      <phase>generate-sources</phase>
      <goals><goal>compile</goal></goals>
      <configuration>
        <schemaDir>${project.basedir}/schemas</schemaDir>
        <outputDir>${project.build.directory}/generated-classes</outputDir>
      </configuration>
    </execution>
  </executions>
</plugin>

Implementation Notes: Container Packaging Specification

Objective

Package the Java CLI tool jdt2jar as:

  • a minimal OCI container image
  • based on custom jlink runtime + Distroless Debian 13 ("trixie") base
  • suitable for Kubernetes, CI runners, ephemeral execution, rootless runtime
  • no shell or package manager in final image

Use: gcr.io/distroless/base-debian13:nonroot

Runtime Requirements

  • Java 21 target bytecode
  • jdt2jar packaged as fat jar or modular jar
  • CLI entrypoint: java -jar /app/jdt2jar.jar

Filesystem Layout

/
├── app/
│   └── jdt2jar.jar
├── jre/
│   ├── bin/java
│   └── lib/...
└── work/

Runtime user: uid=65532, gid=65532 (nonroot distroless default)

Build Strategy

Multi-stage Docker build:

  1. builder stage: full JDK → compile, tests, jdeps, jlink
  2. runtime stage: distroless Debian 13 → stripped runtime + application jar

Java Module Minimization

jdeps --ignore-missing-deps --recursive --multi-release 21 --print-module-deps /build/jdt2jar.jar

Expected minimum modules: java.base
Potential additions: java.logging, java.xml, java.naming, jdk.zipfs

jlink Requirements

jlink --add-modules "$MODULES" --strip-debug --compress=2 --no-header-files --no-man-pages --output /opt/jre

Base Image

FROM gcr.io/distroless/base-debian13:nonroot

Do not use Java distroless variants — runtime is supplied via jlink.

Container Constraints

  • No shell: all Docker directives must use JSON/vector form
  • TLS: distroless base includes CA certs
  • Writable paths: only /work is writable
  • Signal handling: JVM runs as PID 1, must handle SIGTERM/SIGINT
  • Logging: stdout/stderr only, no filesystem logging

Recommended JVM Flags

-XX:+UseContainerSupport
-XX:MaxRAMPercentage=75.0
-Djava.io.tmpdir=/work/tmp
-XX:+ExitOnOutOfMemoryError  (optional)

Dockerfile

# -------------------------------------------------
# Build stage
# -------------------------------------------------
FROM eclipse-temurin:24-jdk AS build
WORKDIR /build
COPY . .
RUN ./mvnw clean package -DskipTests
RUN cp target/jdt2jar.jar /build/jdt2jar.jar
RUN jdeps \
      --ignore-missing-deps \
      --recursive \
      --multi-release 21 \
      --print-module-deps \
      /build/jdt2jar.jar \
      > /build/modules.txt
RUN jlink \
      --add-modules $(cat /build/modules.txt) \
      --strip-debug \
      --compress=2 \
      --no-header-files \
      --no-man-pages \
      --output /opt/jre

# -------------------------------------------------
# Runtime stage
# -------------------------------------------------
FROM gcr.io/distroless/base-debian13:nonroot
WORKDIR /work
COPY --from=build /opt/jre /jre
COPY --from=build /build/jdt2jar.jar /app/jdt2jar.jar
ENTRYPOINT ["/jre/bin/java","-jar","/app/jdt2jar.jar"]

Expected Image Sizes

Variant Size
Full JDK image 350MB–500MB
Distroless Java runtime 120MB–180MB
jlink + distroless 35MB–80MB

Security Characteristics

Final image contains:

  • no package manager, no shell, no compiler, no debugger, no build tools
  • attack surface minimized to: glibc + JVM runtime modules + application code

CI Validation

# Verify runtime starts
docker run --rm jdt2jar --help

# Verify no shell exists
docker run --rm jdt2jar /bin/sh
# Expected: executable file not found

# Verify nonroot execution
docker run --rm jdt2jar id
# Expected: uid=65532(nonroot)

Optional Enhancements

  • SBOM generation via syft
  • Vulnerability scanning via grype
  • Multi-arch builds (linux/amd64, linux/arm64) via docker buildx

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions