Distribution & Release
August 25, 2026 · View on GitHub
How each of this project's artifacts is (or will be) built, published, and consumed, and how to wire the whole thing into CI. One Rust core, four delivery channels — there is no single "deploy", because each binding targets a different package ecosystem.
Status (2026-08): CI is in place.
.github/workflows/builds + tests the engine and every binding (ci.yml) and publishes on tags (crate-release.yml→ crates.io,cli-release.yml→ GitHub Releases,python-release.yml→ PyPI); theJenkinsfiledeploys the Java FFM binding to GBIF Nexus. The Java module ships a thin main JAR plus one cdylib JAR per platform (§3), so it is consumable off-checkout. The Java binding is released at0.2.0and auto-deploys snapshots to GBIF Nexus on every push tomain; crates.io, PyPI and GitHub Releases have all published0.2.0and re-publish from acrate-v*/py-v*/cli-v*tag, CRAN alone is still unwired (seeRELEASE.md). This engine has replaced the pure-Java parser:org.gbif:name-parserended at4.2.0, the contract lives on asorg.gbif:name-parser-api(5.x, API-only), and the ChecklistBank backend has completed its cutover.
1. Artifact status at a glance
| Artifact | Path | Channel | Coordinates / name | Status |
|---|---|---|---|---|
| Rust core library | crates/nameparser | crates.io | gbif-name-parser (lib nameparser) | published — 0.2.0 |
| Native CLI | crates/nameparser-cli | GitHub Releases | nameparser-cli binaries | released — cli-v0.2.0, 4 targets |
| Java FFM binding | bindings/java | repository.gbif.org (Jenkins) | org.gbif.nameparser:name-parser-rust (+ per-arch classifier JARs) | LIVE — released 0.2.0; 0.2.1-SNAPSHOT auto-deploys |
| Python binding | crates/nameparser-py | PyPI | dist gbif-name-parser, import nameparser | published — 0.2.0 (pip install gbif-name-parser) |
| R binding | bindings/r | GitHub (install_github), later CRAN | pkg nameparser | complete; CRAN submission not yet wired |
Every binding except the pure-Rust CLI wraps the nameparser-ffi cdylib
(crates/nameparser-ffi) — so packaging that native library correctly is the cross-cutting
problem, addressed in §3.
2. Per-channel distribution
2.1 Rust core + CLI
- Library →
cargo publishto crates.io. The crate is packagegbif-name-parser0.2.0(Apache-2.0) with lib namenameparser(so dependents keepuse nameparser::). The manifest already carriesdescription/repository/keywords/categoriesand is publishable (cargo publish --dry-run -p gbif-name-parserpasses); acrate-v*tag publishes it viacrate-release.yml(OIDC Trusted Publishing — no stored token). - CLI → build
cargo build --release -p nameparser-cliper target and attach the stripped binaries to a GitHub Release (nameparser-cli-<version>-<target>.tar.gz,.zipon Windows). No package manager needed; users download and run.
2.2 Java FFM binding — the one that matters for GBIF/CoL
The module is a deliberately standalone Maven project (see the comment atop
bindings/java/pom.xml): it does not inherit org.gbif:motherpom because motherpom
enforces maven.compiler.release=17, and java.lang.foreign (FFM/Panama) needs the
compiler release at 22. Current coordinates:
org.gbif.nameparser:name-parser-rust:0.2.0-SNAPSHOT (packaging: jar)
It compiles org.gbif.nameparser.rust.NameParserRust implements org.gbif.nameparser.api.NameParser,
so it is a drop-in for any code already written against the NameParser interface — the
whole point of the FFM binding, and the basis for the Phase-5 backend cutover.
Prerequisites to be a real Maven dependency:
- Bundle the native library (§3) — ✅ DONE.
Ffi.resolveLibPath()now extracts the cdylib from a JAR resource (native/${os.detected.classifier}/) when no-Dnameparser.ffi.lib/$NAMEPARSER_FFI_LIBoverride is set, somvn packageproduces a self-contained JAR (verified loading the bundled lib with no override). The-D/env dev escape hatches still work. - Java 22+ at runtime. FFM downcalls are a restricted method; the module already opts in
with
--enable-native-access=ALL-UNNAMED, but the language level requires JDK 22+. A Java 17 service cannot load this JAR until it upgrades (tracked as the Phase-5 cutover prerequisite).
Release-readiness — done, and the versioning model:
- ✅
name-parser-apipinned to the released5.0.1; a GBIF Nexus<repositories>block resolves it (this standalone POM has no motherpom to supply it). The Javaname-parserreference-impl / oracle was removed at 5.0.0 (api-only), so it is no longer a test dependency. The api is an independently versioned dependency — the stable contract — not this module's own version. - ✅ Version =
0.2.0released (0.2.1-SNAPSHOTonmain) — the Java FFM binding shares the Rust engine's version (the Cargo[workspace.package]version at the repo root), released in lockstep with the CLI/Python/R bindings: one version across every binding ⇒ the same engine. It is not tied to thename-parser-apiversion it implements (an implementation versioning independently from its api is normal — cf. logback-classic vs slf4j-api). The bindings sit at0.xwhile new and gathering real-use feedback, and will graduate to the product's5.xline once stable (a deliberate one-time re-baseline, in lockstep). The JAR manifest also stampsRust-Engine-Version+ the git SHA, so the exact engine is always readable from the artifact. - ✅
<distributionManagement>forrepository.gbif.org(interim, until the reactor move). The<server>credential ids in the deployer's~/.m2/settings.xmlmust matchgbif-release/gbif-snapshot— adjust to your actual ids. - Remaining before an actual
mvn deploy: confirm those credential ids; optionally add themaven-source-plugin/maven-javadoc-plugin/maven-gpg-plugin+ Sonatype/Central publishing plugin if Maven Central sync is wanted (GBIF-internal consumers resolve from Nexus, so this is skippable).
2.3 Python binding
- CI:
.github/workflows/python-release.yml(uses PyO3/maturin-action) buildsmanylinux(x86_64 + aarch64), macOS (arm64; Intel macOS dropped — macos-13 runners retiring, sdist is the Intel fallback), and Windows wheels plus an sdist, each embedding the compiled Rust extension. Because the crate builds an abi3 (abi3-py39) wheel, one wheel per platform covers CPython 3.9+ — no per-version matrix. - Publishes to PyPI under distribution name
gbif-name-parser(import staysnameparser; matches the Rust crate on crates.io) via Trusted Publishing (OIDC) — no PyPI token is stored anywhere. A tag-vs-pyproject.toml-version guard blocks a mismatched (and irreversible) upload. - Trigger: push a
py-v<version>tag. One-time setup before the first release: register the PyPI Trusted Publisher (ownergbif, reponame-parser-rust, workflowpython-release.yml, environmentpypi) and create thepypiGitHub environment. See the workflow header for details.
2.4 R binding
- Distribute from GitHub first:
remotes::install_github("gbif/name-parser-rust", subdir = "bindings/r"). This compiles the embedded Rust crate on the user's machine (they need a Rust toolchain;SystemRequirements: CargoinDESCRIPTIONdeclares it). - CRAN later: CRAN requires the Rust dependencies to be vendored into the source
tarball (
cargo vendorundersrc/rust/vendor/) and an offline, network-free build — a separate hardening step, deferred like the Python PyPI publish.
3. Packaging the native library (the cross-cutting problem)
Three bindings need libnameparser_ffi.* on the target machine. Java is the hard case because it
loads over FFM from a path. Ffi.resolveLibPath() extracts the cdylib from a classpath
resource native/${os.detected.classifier}/<libname> to a temp file and hands that to
SymbolLookup.libraryLookup(...) — with the -Dnameparser.ffi.lib / $NAMEPARSER_FFI_LIB
overrides and a repo-relative dev path as fallbacks. That resource works whether it sits in the
main JAR or (as we ship it) a per-platform classifier JAR on the consumer's classpath.
✅ IMPLEMENTED — classifier JARs (netty-tcnative style), for small JARs. The module publishes:
- a thin main JAR
name-parser-rust-<ver>.jar(~22 KB, Java classes only) whose manifest is stamped withImplementation-Version,Rust-Engine-Version(thegbif-name-parsercrate version), andRust-Engine-Git-Revision(the exact source SHA — see the versioning note in §2.2); - one per-platform classifier JAR
name-parser-rust-<ver>-<classifier>.jar(~1–1.6 MB, just that platform's cdylib undernative/<classifier>/) forlinux-x86_64,linux-aarch_64,osx-x86_64,osx-aarch_64(Apple Silicon), andwindows-x86_64.
A consumer depends on the main JAR + their platform's classifier (via os-maven-plugin's
${os.detected.classifier}), so nobody downloads five architectures they won't use (§5).
The cdylibs are cross-compiled with cargo-zigbuild (ci/build-cdylib.sh): one Linux CI agent
builds every target — zig cross-compiles cleanly to macOS/Windows because nameparser-ffi is pure
Rust (no C deps). The script bootstraps rustup + zig (self-contained tarball, no brew needed) and
stages each cdylib into native-staging/<classifier>/; the pom's maven-jar-plugin executions
package them with skipIfEmpty, so an agent that manages only its host platform still deploys a
valid subset. Verified end-to-end locally: 6 JARs, correct architectures (incl. Mach-O arm64), and
a stamped manifest.
4. CI / Jenkins pipeline
Because the native libraries must exist before the JAR and wheels are assembled, the pipeline
is multi-stage with a per-platform build matrix — not a single mvn deploy:
Stage 1 — native cdylib per platform
Jenkins matrix over agents { linux-x86_64, linux-aarch64, darwin-arm64, windows-x86_64 }
running: cargo build --release -p nameparser-ffi
(or cargo-zigbuild to cross-compile the Linux/Windows targets on one Linux agent;
macOS still wants a real mac agent). Stash each .so / .dylib / .dll.
Stage 2 — Java (needs JDK 22+)
Unstash all cdylibs into bindings/java/src/main/resources/native/**
mvn -f bindings/java/pom.xml deploy → repository.gbif.org
Stage 3 — Python
maturin-action (manylinux + macOS + Windows wheels + sdist) → gh-action-pypi-publish (OIDC) → PyPI
Stage 4 — R
R CMD build bindings/r ; R CMD check → GitHub release asset
Stage 5 — CLI
cargo build --release -p nameparser-cli per target → GitHub release
GBIF-specific notes
- The Java stage is well-trodden ground: the sibling
github.com/gbif/name-parseralready releases through GBIF Jenkins →repository.gbif.orgviaorg.gbif:motherpom:61and themaven-release-plugin. Reuse that infrastructure and the branch/tag conventions. - This module can't inherit motherpom (the
release=17enforcer), so it carries its own<distributionManagement>and — if Central sync is wanted — its own sources/javadoc/GPG/publish plugins (§2.2). - Follow the GBIF engineering handbook
for versioning (semver), issue/release management, and documentation conventions — it
applies to all
github.com/gbif/repos, including this one. - Trigger model: a multibranch pipeline that builds + tests on PRs, deploys
-SNAPSHOTonmain, and cuts a release on a version tag (maven-release-pluginfor the Java module).
Incremental path (do this first). Get Stage 2 — the Java artifact to
repository.gbif.org working before anything else. It is the one artifact GBIF/CoL
consumers resolve through Maven, and it reuses infrastructure you already operate. The other
three channels (PyPI, GitHub/CRAN, crates.io) are independent of Nexus and can follow one at
a time.
5. Consuming the parser
Java / Maven
The API artifact (interface + ParsedName model + enums only — no implementation), on
repository.gbif.org and Maven Central:
<dependency>
<groupId>org.gbif</groupId>
<artifactId>name-parser-api</artifactId>
<version>5.0.1</version>
</dependency>
The 5.0.x api is api-only: the pure-Java org.gbif:name-parser implementation
(NameParserGBIF / NameParserImpl) was removed at 5.0.0, and this Rust binding is now its
sole implementation. The last implementation release on the old, exception-throwing 4.x
interface is org.gbif:name-parser:4.2.0 — use it only if you cannot move to JDK 22+ yet.
If your build does not already resolve from GBIF's Nexus, add:
<repositories>
<repository>
<id>gbif-all</id>
<url>https://repository.gbif.org/content/groups/gbif</url>
</repository>
</repositories>
The Rust-backed FFM binding — a drop-in NameParser on JDK 22+. Add the thin main JAR
plus your platform's native classifier JAR (via os-maven-plugin, §3). 0.2.0 is the current
release; 0.2.1-SNAPSHOT deploys on every push to main:
<build><extensions>
<!-- sets ${os.detected.classifier}: linux-x86_64, osx-aarch_64, windows-x86_64, … -->
<extension>
<groupId>kr.motd.maven</groupId><artifactId>os-maven-plugin</artifactId><version>1.7.1</version>
</extension>
</extensions></build>
<dependencies>
<dependency> <!-- thin main JAR: Java + FFM loader -->
<groupId>org.gbif.nameparser</groupId>
<artifactId>name-parser-rust</artifactId>
<version>0.2.0</version>
</dependency>
<dependency> <!-- your platform's native cdylib -->
<groupId>org.gbif.nameparser</groupId>
<artifactId>name-parser-rust</artifactId>
<version>0.2.0</version>
<classifier>${os.detected.classifier}</classifier>
</dependency>
</dependencies>
// same interface as the pure-Java parser — swap the implementation, keep the callers.
NameParser parser = new org.gbif.nameparser.rust.NameParserRust();
ParsedName pn = parser.parse("Abies alba Mill.", Rank.SPECIES);
Python
pip install gbif-name-parser # once published to PyPI
import nameparser
pn = nameparser.parse("Abies alba Mill.")
R
# from GitHub (compiles the Rust crate; needs a Rust toolchain):
remotes::install_github("gbif/name-parser-rust", subdir = "bindings/r")
library(nameparser)
parse_names("Abies alba Mill.")
Rust CLI
# once binaries are on GitHub Releases:
curl -L .../nameparser-cli-<ver>-<target>.tar.gz | tar xz
./nameparser-cli parse --input=names.txt