You've spent some time working on that Rust project of yours and are now ready to post a release. It's quite straight forward to build and upload binaries as assets to a Git release but let's face it, we can't expect consumers to find the release page, select the appropriate binary for their platform, download the file, and configure their environment settings to point to the new binary. Well yes, some people like to do that but most prefer to use a tool instead, something that automates the steps just described, something like a package manager such as Homebrew, Snapcraft, Chocolatey, and others.
These package managers have a set of requirements such as binaries (or zipballs/tarballs) available at a public URL (the Git release page being a perfect candidate) but might as well be an S3 bucket or a JFrog Artifactory URL. They also require checksums (SHA-256 being the most popular option), as well as additional metadata such as license, project website, icons, etc. Supporting each one of these packaging options for your project may become a tedious task. This is where JReleaser comes in to save the day.
JReleaser and Rust
Even though JReleaser is a tool born in the Java world it may be used with non Java projects, such as Rust based projects. There are a few considerations to be followed in order to use them together.
To begin with JReleaser follows a set of platform definitions such as "osx-x86_64" and "linux-aarch64" whereas Rust prefers a different set such as "x86_64-apple-darwin" and "aarch64-unknown-linux-gnu". This is not a problem at all as we can use JReleaser's platform replacements feature to map from one set to the other.
Configuring Platforms
Say we'd like to build binaries for the 3 major platforms supported by GitHub runners that is, linux, windows, and macOS. Using the default configuration file jreleaser.yml
would lead to these mappings:
platform:
replacements:
'osx-x86_64': 'x86_64-apple-darwin'
'linux-x86_64': 'x86_64-unknown-linux-gnu'
'windows-x86_64': 'x86_64-pc-windows-msvc'
If YAML is not to your liking then you may switch to TOML and write down the configuration in jreleaser.toml
.
Next, JReleaser does not build the binaries, it's only concerned with releasing them thus you continue to use the same Cargo instructions as you've had so far. If you want you may cross compile binaries or run the build in specific nodes. What's important is that at the end of the build a set of binaries should be ready for JReleaser to continue with its responsibilities. For this reason we only have to list the binaries we'd like to see released.
Assembling Distributions
Binaries must be part of what's know as a distribution, the one we need for Rust binaries is of type BINARY. Distributions of this type follow a particular file structure which is usually packaged as a archives (zipballs or tarballs):
.
├── LICENSE
├── README
└── bin
└── executable
You may use Cargo to create such structure and archive or let JReleaser perform that task for you. If you choose the latter then you gain the option to create such archives in a platform independent manner. Say we have a "helloworld" application that produces a "helloworld[.exe]" executable (depending on the platform of course) that we'd like to package alongside the project's LICENSE file. This is what would be needed in the jreleaser.yml
config file to acchieve this goal:
assemble:
archive:
helloworld:
active: ALWAYS
formats: [ ZIP ]
attachPlatform: true
fileSets:
- input: 'target/release'
output: 'bin'
includes: [ 'helloworld{.exe,}' ]
- input: '.'
includes: [ 'LICENSE' ]
This configuration, along side the platform replacements defined earlier should produce a set of binaries like these ones when running on GitHub Actions:
- helloworld-<version>-x86_64-apple-darwin.zip
- helloworld-<version>-x86_64-pc-windows-msvc.zip
- helloworld-<version>-x86_64-unknown-linux-gnu.zip
The value of <version> will be set as part of the GitHub workflow execution as we'll see in a moment. Listing the binaries inside a distribution can be done in this way:
distributions:
helloworld:
type: BINARY
executable:
windowsExtension: exe
artifacts:
- path: '{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-x86_64-apple-darwin.zip'
platform: 'osx-x86_64'
- path: '{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-x86_64-unknown-linux-gnu.zip'
platform: 'linux-x86_64'
- path: '{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-x86_64-pc-windows-msvc.zip'
platform: 'windows-x86_64'
Mind that even though the archives follow the Rust platform conventions we still have to use the Java conventions as values for the platform:
properties, just so that JReleaser may perform proper validation and configure selected package managers as needed.
Finishing the Configuration
There are 2 additional blocks that need to be configured: project metadata such as name, license, authors, etc; release and changelog configuration. The whole file could end up looking like this:
jreleaser.yml
environment:
properties:
artifactsDir: out/jreleaser/assemble/helloworld/archive
project:
name: helloworld
description: HelloWorld in Rust
longDescription: HelloWorld in Rust
website: https://github.com/aalmiray/helloworld-rust
authors:
- Andres Almiray
license: MIT
extraProperties:
inceptionYear: 2021
platform:
replacements:
'osx-x86_64': 'x86_64-apple-darwin'
'linux-x86_64': 'x86_64-unknown-linux-gnu'
'windows-x86_64': 'x86_64-pc-windows-msvc'
release:
github:
name: helloworld-rust
overwrite: true
changelog:
formatted: ALWAYS
format: '- {{commitShortHash}} {{commitTitle}}'
preset: conventional-commits
contributors:
format: '- {{contributorName}}{{#contributorUsernameAsLink}} ({{.}}){{/contributorUsernameAsLink}}'
assemble:
archive:
helloworld:
active: ALWAYS
formats: [ ZIP ]
attachPlatform: true
fileSets:
- input: 'target/release'
output: 'bin'
includes: [ 'helloworld{.exe,}' ]
- input: '.'
includes: [ 'LICENSE' ]
distributions:
helloworld:
type: BINARY
executable:
windowsExtension: exe
artifacts:
- path: '{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-x86_64-apple-darwin.zip'
platform: 'osx-x86_64'
- path: '{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-x86_64-unknown-linux-gnu.zip'
platform: 'linux-x86_64'
- path: '{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-x86_64-pc-windows-msvc.zip'
platform: 'windows-x86_64'
Full sources for this particular project can be found at https://github.com/aalmiray/helloworld-rust. You may have noticed the environment:
block and the use of a Mustache template named artifactsDir
in the artifact paths. We need these two items as the artifacts will be assembled by separate runners on GitHub Actions, uploaded to a common directory to be collected, then downloaded by another GitHub workflow job and be released. The collecting path is different as the assembly path. If you were to run cross compilation on a single node and skip artifact collection then these 2 settings wouldn't be needed.
Adding a GitHub Workflow
Speaking of the GitHub workflow, you may have JReleaser trigger on any particular condition such as pushing a tag, or a workflow dispatch, or any other kind of event. For demo purposes the helloworld application uses a push on the main
branch:
.github/workflows/early-access.yml:
name: EarlyAccess
# Build on every push to main
on:
push:
branches: [ main ]
jobs:
build:
strategy:
fail-fast: true
matrix:
os: [ ubuntu-latest, macOS-latest, windows-latest ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
# Configure the Rust toolchain
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- uses: actions-rs/cargo@v1
with:
command: build
args: --release --all-features
# Read project version from file
- name: Version
id: version
uses: juliangruber/read-file-action@v1
with:
path: VERSION
trim: true
# Assemble the zipball
- name: Assemble
uses: jreleaser/release-action@v2
with:
version: early-access
arguments: assemble
env:
JRELEASER_PROJECT_VERSION: ${{ steps.version.outputs.content }}
JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Upload archive
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: artifacts
path: |
out/jreleaser/assemble/helloworld/archive/*.zip
- name: JReleaser output
if: always()
uses: actions/upload-artifact@v2
with:
name: jreleaser-${{ matrix.os }}
path: |
out/jreleaser/trace.log
out/jreleaser/output.properties
# Release all archives
release:
needs: [ build ]
runs-on: ubuntu-latest
steps:
# Checkout with full history
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
# Download all collected archives
- name: Download artifacts
uses: actions/download-artifact@v2
# Read project version from file
- name: Version
id: version
uses: juliangruber/read-file-action@v1
with:
path: VERSION
trim: true
# Release it!
- name: Release
uses: jreleaser/release-action@v2
with:
version: early-access
arguments: release -PartifactsDir=artifacts -PskipArchiveResolver
env:
JRELEASER_PROJECT_VERSION: ${{ steps.version.outputs.content }}
JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: JReleaser output
if: always()
uses: actions/upload-artifact@v2
with:
name: jreleaser-release
path: |
out/jreleaser/trace.log
out/jreleaser/output.properties
Now all that's left is to push commits to the main
branch and have a release pop up after a few minutes. The release may look like this https://github.com/aalmiray/helloworld-rust/releases/tag/early-access
Adding Package Managers
But what about those package managers we mentioned earlier? Well those may be added to the distribution. If your application follows established convention then there's little that needs to be configured. Here's for example how to enable a multi-platform Homebrew formula that is, a formula that can install binaries on macOS and Linux:
distributions:
helloworld:
type: BINARY
executable:
windowsExtension: exe
artifacts:
- path: '{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-x86_64-apple-darwin.zip'
platform: 'osx-x86_64'
- path: '{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-x86_64-unknown-linux-gnu.zip'
platform: 'linux-x86_64'
- path: '{{artifactsDir}}/{{distributionName}}-{{projectVersion}}-x86_64-pc-windows-msvc.zip'
platform: 'windows-x86_64'
# Just add this block!!
brew:
active: ALWAYS
multiPlatform: true
Then switch from invoking the release
command to the full-release
command and you should be in business, it's just that easy. There are of course more options that you may set depending on your needs. Head to the JReleaser Guide to discover more about them.
Keep on coding!
Image by Ulrike Leone from Pixabay