JReleaser: Enter the Matrix

JReleaser v1.16.0 was recently released, bringing a new set of capabilities, one of them being the Matrix element. A matrix is a collection of key/value pairs that may be used to parameterize hooks & and the archive assembler. In the future a matrix may also be used to parameterize jlink and nativeImage assemblers, as well as distributions.

What are the advantages of having this new element as part of the DSL? Well let's say you'd like to release a project whose source language supports cross-platform compilation in a single node, say for example Go, Zig, or Rust. You'd typically define a set of os/arch targets, iterating over the set and passing each combination to the compiler.

In the case of Go one could do the following to generate 6 different executables:

$ GOOS=darwin  GOARCH=arm64 go build -o target/${GOOS}-${GOARCH}/ src/helloworld.go
$ GOOS=darwin  GOARCH=amd64 go build -o target/${GOOS}-${GOARCH}/ src/helloworld.go
$ GOOS=linux   GOARCH=arm64 go build -o target/${GOOS}-${GOARCH}/ src/helloworld.go
$ GOOS=linux   GOARCH=amd64 go build -o target/${GOOS}-${GOARCH}/ src/helloworld.go
$ GOOS=windows GOARCH=arm64 go build -o target/${GOOS}-${GOARCH}/ src/helloworld.go
$ GOOS=windows GOARCH=amd64 go build -o target/${GOOS}-${GOARCH}/ src/helloworld.go

Now, JReleaser is a release tool, not a build tool. However we can leverage the hooks element to define an inline script that may be invoked during a particular point of the assemble step, such as:

matrix:
  rows:
    - { goos: darwin,  goarch: arm64, platform: osx-aarch_64     }
    - { goos: darwin,  goarch: amd64, platform: osx-x86_64       }
    - { goos: linux,   goarch: arm64, platform: linux-aarch_64   }
    - { goos: linux,   goarch: amd64, platform: linux-x86_64     }
    - { goos: windows, goarch: arm64, platform: windows-aarch_64 }
    - { goos: windows, goarch: amd64, platform: windows-x86_64   }

hooks:
  script:
    before:
      - run: |
          echo "building ${GOOS}-${GOARCH}"
          go build -o target/${GOOS}-${GOARCH}/ src/helloworld.go
        applyDefaultMatrix: true
        verbose: true
        environment:
          GOOS: '{{ matrix.goos }}'
          GOARCH: '{{ matrix.goarch }}'
        filter:
          includes: ['assemble']

This ensures the compiler is invoked before any assembler is resolved, which results in all executables being compiled. Notice the use of the matrix element defining goos and goarch variables with the same values we previously used. Notice also the use of another variable named platform which JReleaser uses to uniquely identify a given artifact by its target platform. Next, we define an archive assembler that can create Zips and Tars archives:

assemble:
  archive:
    helloworld:
      active: ALWAYS
      formats: [ ZIP ]
      applyDefaultMatrix: true
      archiveName: '{{distributionName}}-{{projectVersion}}-{{ matrix.goos }}-{{ matrix.goarch }}'
      fileSets:
        - input: 'target/{{ matrix.goos }}-{{ matrix.goarch }}'
          output: 'bin'
          includes: [ 'helloworld{.exe,}' ]
        - input: '.'
          includes: [ 'LICENSE' ]

Notice that this assembler also makes use of the matrix element previously defined, resulting in 6 different archives being assembled. We can trigger compilation and assembly with a single command:

$ jreleaser assemble
[INFO]  JReleaser 1.16.0
[INFO]  Configuring with jreleaser.yml
[INFO]    - basedir set to /Users/aalmiray/dev/github/helloworld-go
[INFO]    - outputdir set to /Users/aalmiray/dev/github/helloworld-go/out/jreleaser
[INFO]  Reading configuration
[INFO]  git-root-search set to false
[INFO]  Loading variables from /Users/aalmiray/.jreleaser/config.toml
[INFO]  Validating configuration
[INFO]  Strict mode set to false
[INFO]  Project version set to 1.0.0-SNAPSHOT
[INFO]  Release is snapshot
[INFO]  Timestamp is 2025-01-09T20:36:00.94376+01:00
[INFO]  HEAD is at f283f1e
[INFO]  Platform is osx-x86_64
[INFO]  dry-run set to false
[INFO]  Executing before script hook(s): 1 in total
  [hooks] building darwin-arm64
  [hooks] building darwin-amd64
  [hooks] building linux-arm64
  [hooks] building linux-amd64
  [hooks] building windows-arm64
  [hooks] building windows-amd64
[INFO]  Assembling distributions
[INFO]    [assemble] Assembling all distributions
[INFO]      [archive] Assembling helloworld distribution
[INFO]      [archive] - helloworld-1.0.0-SNAPSHOT-darwin-arm64.zip
[INFO]      [archive] - helloworld-1.0.0-SNAPSHOT-darwin-amd64.zip
[INFO]      [archive] - helloworld-1.0.0-SNAPSHOT-linux-arm64.zip
[INFO]      [archive] - helloworld-1.0.0-SNAPSHOT-linux-amd64.zip
[INFO]      [archive] - helloworld-1.0.0-SNAPSHOT-windows-arm64.zip
[INFO]      [archive] - helloworld-1.0.0-SNAPSHOT-windows-amd64.zip
[INFO]  Writing output properties to out/jreleaser/output.properties
[INFO]  JReleaser succeeded after 2.587 s

As you can see the hook triggers first and compiles the code. Then the archive assembler performs its duties and creates 6 different archives. We can check that executables were compiled and placed at their respective locations:

$ tree target
target
├── darwin-amd64
│   └── helloworld
├── darwin-arm64
│   └── helloworld
├── linux-amd64
│   └── helloworld
├── linux-arm64
│   └── helloworld
├── windows-amd64
│   └── helloworld.exe
└── windows-arm64
    └── helloworld.exe

6 directories, 6 files

What's left now is to release the project using the release command:

$ jreleaser release
[INFO]  JReleaser 1.16.0
[INFO]  Configuring with jreleaser.yml
[INFO]    - basedir set to /Users/aalmiray/dev/github/helloworld-go
[INFO]    - outputdir set to /Users/aalmiray/dev/github/helloworld-go/out/jreleaser
[INFO]  Reading configuration
[INFO]  git-root-search set to false
[INFO]  Loading variables from /Users/aalmiray/.jreleaser/config.toml
[INFO]  Validating configuration
[INFO]  Strict mode set to false
[INFO]  Project version set to 1.0.0-SNAPSHOT
[INFO]  Release is snapshot
[INFO]  Timestamp is 2025-01-09T20:39:16.281631+01:00
[INFO]  HEAD is at f283f1e
[INFO]  Platform is osx-x86_64
[INFO]  dry-run set to false
[INFO]  Generating changelog
[INFO]  Storing changelog: out/jreleaser/release/CHANGELOG.md
[INFO]  Calculating checksums for distributions and files
[INFO]    [checksum] out/jreleaser/assemble/helloworld/archive/helloworld-1.0.0-SNAPSHOT-linux-arm64.zip.sha256
[INFO]    [checksum] out/jreleaser/assemble/helloworld/archive/helloworld-1.0.0-SNAPSHOT-linux-amd64.zip.sha256
[INFO]    [checksum] out/jreleaser/assemble/helloworld/archive/helloworld-1.0.0-SNAPSHOT-darwin-arm64.zip.sha256
[INFO]    [checksum] out/jreleaser/assemble/helloworld/archive/helloworld-1.0.0-SNAPSHOT-darwin-amd64.zip.sha256
[INFO]    [checksum] out/jreleaser/assemble/helloworld/archive/helloworld-1.0.0-SNAPSHOT-windows-arm64.zip.sha256
[INFO]    [checksum] out/jreleaser/assemble/helloworld/archive/helloworld-1.0.0-SNAPSHOT-windows-amd64.zip.sha256
[INFO]  Cataloging artifacts
[INFO]    Cataloging is not enabled. Skipping
[INFO]  Signing distributions and files
[INFO]    [sign] Signing is not enabled. Skipping
[INFO]  Deploying Maven artifacts
[INFO]    [maven] Deploying is not enabled. Skipping
[INFO]  Uploading distributions and files
[INFO]    [upload] Uploading is not enabled. Skipping
[INFO]  Releasing to https://github.com/jreleaser/helloworld-go@main
[INFO]   - uploading checksums_sha256.txt
[INFO]   - uploading helloworld-1.0.0-SNAPSHOT-darwin-amd64.zip
[INFO]   - uploading helloworld-1.0.0-SNAPSHOT-darwin-arm64.zip
[INFO]   - uploading helloworld-1.0.0-SNAPSHOT-linux-amd64.zip
[INFO]   - uploading helloworld-1.0.0-SNAPSHOT-linux-arm64.zip
[INFO]   - uploading helloworld-1.0.0-SNAPSHOT-windows-amd64.zip
[INFO]   - uploading helloworld-1.0.0-SNAPSHOT-windows-arm64.zip
[INFO]  Writing output properties to out/jreleaser/output.properties
[INFO]  JReleaser succeeded after 0.596 s

Which creates a release on the target GitHub repository: jreleaser/helloworld-go.

The split between build and release lets you iterate the build/assembly as many times as needed. You're free to build as you wish, target artifacts may be generated in any way given project requirements and constraints. But once they have been generated the tool can release them as long as the list of artifacts has been configured. JReleaser assemblers can automatically populate matching distributions which is why you may not see an explicit list in the configuration file of the Go example, but there is such list in jreleaser/helloworld-pascal because cross-platform compilation is not enabled in that example.

If you're curious about the usage of the matrix element for cross-platform compilation then check these examples:

https://github.com/jreleaser/helloworld-go
https://github.com/jreleaser/helloworld-zig
https://github.com/jreleaser/helloworld-rustx
https://github.com/jreleaser/helloworld-csharp
https://github.com/jreleaser/helloworld-deno
https://github.com/jreleaser/helloworld-bun

As always, feedback is welcomed, feel free to file a ticket or start a discussion topic.

Keep on coding!

Image by Neo_Artemis from Pixabay

Liked it? Take a second to support aalmiray on Patreon!
Become a patron at Patreon!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

ˆ Back To Top