Skip to content

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

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions