Skip to content

Make native libraries and the jar identify their build (#211) - #213

Open
pietrygamat wants to merge 4 commits into
java-native:masterfrom
pietrygamat:version-info
Open

pietrygamat wants to merge 4 commits into
java-native:masterfrom
pietrygamat:version-info

Conversation

@pietrygamat

@pietrygamat pietrygamat commented Oct 19, 2026 •

Copy link
Copy Markdown
Collaborator

Addresses points 2–4 of #211 (point 1 went in with #212). Every commit stands alone and covers one point, so each is explained separately below and they can be reviewed one at a time.

The goal is the one stated in #211: with the jar and its natives built at different times, on different machines and for different targets, it should be easy to answer "which build is this binary from, and does it belong with this jar?", both before publishing a release and afterwards, once the artifacts are in users' hands.


1. Embed a build ident in every native library (#211, point 3)

Why. We have no reliable way to tell which source revision and which target a libjssc.so/.dylib/.dll was built from. File names and directories (linux_64, linux_arm64, …) are only as trustworthy as the packaging step that put the file there, and mistakes in that step are what I want to catch before a Maven Central release. A single line in a known format, readable on every platform with grep and without loading the library, answers this for any of our targets on whatever machine is doing the checking. This is the same idea as the package information that Fedora and other distros put into ELF objects, and as the old SCCS @(#) marker that what(1) still finds.

Contents and why each field is there:

  • version: what the native claims to be, so it can be checked against the jar.
  • rev: the source commit. CI passes the base revision explicitly, so the ident names the same commit as the Precompiled natives @<rev> commit rather than whatever happens to be checked out in the build container. A local build adds -dirty when native sources have uncommitted changes, so a hand-built binary can't pass for a clean one.
  • target: the directory the binary is meant for. This catches a binary copied into the wrong directory, which is the packaging error that is otherwise hardest to see.
  • origin: tells natives built by our pipeline (java-native/jssc) apart from those built by a fork's pipeline (which report the fork's own repository name) and from local builds (local).

There is deliberately no build timestamp, so the output stays reproducible after #212.

Rejected alternatives

  • A JNI function returning the same string. It only works when the library can be loaded, i.e. on the matching OS/arch. Checking the ARM, PPC or Windows binaries on a Linux x86 release machine is exactly the case that matters.
  • An ELF .note.package like the distros use. ELF-only. Mach-O and PE would each need a different mechanism and different tools. A plain named section holding a printable string works the same way for all three formats and can be found with grep -a.
  • Relying on getNativeLibraryVersion() alone. It has the same loading problem as a JNI function, and it carries the version only, with no revision or target.

2. Add a VERSIONINFO resource to the Windows natives (#211, point 2)

Why. On Windows, the version resource is where people and tools look for this information: Explorer's Details tab, (Get-Item jssc.dll).VersionInfo, software inventory and endpoint-security tools, installers deciding whether to overwrite a file. Our DLLs had none, so to all of these they looked like anonymous files. The ident from commit 1 is also copied into Comments, so the same build info is visible there too.

Company name and copyright come from the pom (<organization> and a new jssc.copyright property). That way they are kept in one place and come out identical whether the DLL is built through Maven or with plain cmake. While setting up a single copyright value, I also used it for the javadoc footer. The default footer named no holder, stamped the build year (so the output wasn't reproducible) and said "All rights reserved", which contradicts the LGPL. The original author is now credited in the pom as a contributor, so the credit doesn't depend on the copyright string alone.

3. Take the library version from the pom at build time (#211, point 4)

Why. SerialNativeInterface.libVersion was hardcoded and only updated by a sed + bot commit in cross-compile.yml before a release. After a release, the pom moves to the next SNAPSHOT but the constant doesn't, so on master the Java side and the natives report different versions (currently 2.10.4 vs 2.10.5). That is the mismatch the version check is meant to detect, so the check becomes useless. With the value taken from release.version project.version at build time, it can't drift any more, and the workflow no longer needs to rewrite sources and push commits on its own.

A unit test checks that the constant really was filtered (i.e. it is a plain x.y.z, not a placeholder). This is a first, small piece of point 5.

In a fresh checkout, an IDE will report SerialNativeInterface as not compiling (LibraryVersion cannot be resolved) until Maven has generated the class once, e.g. with mvn generate-sources or any regular build. After that, the IDE picks up target/generated-sources/java-templates and the error goes away. I couldn't avoid this: templating-maven-plugin only filters a separate source root (src/main/java-templates), and it offers no clean way to filter a single class in place under src/main/java, which would have kept a compilable placeholder there.

Rejected alternatives

  • Reading the version at runtime from a packaged resource (a filtered .properties file or the jar manifest). This adds I/O and a failure mode (missing or repackaged resource, shaded jars, unusual classloaders) to code that runs while the native library loads. A compile-time constant can't go missing.
  • Keeping the sed step and adding a second one after the release. That fixes the symptom, still keeps a version in source control that has to be kept in sync by automation, and still produces bot commits.

EDIT: After some experiments, decided to read the version from a classpath resource at loadtime, rather than hardocde a constant in build-time, because: a) any load-time reading problems also affect the binary, so were are either save or already screwed anyway ; b) Maven/Ant and IDEs really hate the idea of compile time templating, and they all add a lot of friction; c) classpath resource can be read easily from renamed jar, so it's a bonus.

4. Drop the version qualifier in the CMake pom fallback

Why. Maven passes CMake release.version (2.10.5-SNAPSHOT → 2.10.5), and since commit 3 that is also what the Java side holds. A plain cmake build without Maven fell back to reading the raw pom version, so its natives reported 2.10.5-SNAPSHOT (from getNativeLibraryVersion(), in the ident and in VERSIONINFO) and didn't match the jar they were tested with. Applying the same stripping in the fallback makes both build paths produce the same version.

4. Use exact pom.xml version for both native binaries and java-side library version

Why: Since #102 we maintained the versions reported by library using getLibraryVersion and getNativeLibraryVersion using a major.minor.stamp format, and not aligning it correctly with actual pom.version. Whith the versions being now read from pom at build time, that's generally no longer necessary, with one specific exception - the pre-release cross-compile rebuild. For that, I added an option to manually overwrite the version of native library.


Not in this PR

Point 5 of #211 beyond the unit test above, i.e. a pipeline check that fails a release when a native's ident (version/target) doesn't match the jar or its directory. It builds on commits 1 and 3, so I'd rather settle the ident format here first and add the check in a follow-up.

In order to be able to identify a binary library built for any architecture,
and prevent errors when packaging during release, every native now
carries one ident line in a section of its own:

 @(#)JSSC-IDENT version=${VERSION} rev=${REV} target=${LIB_DIR} origin=${ORIGIN}

cross-compile.yml passes the revision and the repository it runs in
(origin=java-native/jssc upstream, a fork reports its own name) through
-Djssc.build.rev/-Djssc.build.origin. A local build takes the revision
from git, appends -dirty when native sources have uncommitted changes,
and says origin=local.

Read it without loading the library:
- grep -a -o 'JSSC-IDENT [[:print:]]*' <lib>      (any platform)
- readelf -p .jssc libjssc.so
- otool -s __TEXT __jssc libjssc.dylib     (or: what libjssc.dylib)
- objdump -s -j .jssc jssc.dll
The DLLs had no version resource, so Explorer, PowerShell
((Get-Item jssc.dll).VersionInfo) and inventory tools showed nothing.

FILEVERSION/PRODUCTVERSION are the numeric form of the version
(2.10.5 -> 2,10,5,0).

CompanyName and LegalCopyright are read by CMake from the pom
(<organization> and the jssc.copyright property), so maven and plain
cmake builds produce the same resource.

The same copyright replaces the default javadoc footer, which named
no holder, stamped the build year and said "All rights reserved".
The original author is credited as a pom contributor.
@tresf

tresf commented Oct 20, 2026

Copy link
Copy Markdown

@pietrygamat thanks for the hard work on this. What I don't completely understand is (what appears to be) the blind removal of -SNAPSHOT as I would expect this to be maintained for builds from our local PCs:

# Drop any qualifier (2.10.5-SNAPSHOT -> 2.10.5) like release.version in pom.xml,
# so getNativeLibraryVersion() matches the Java side's LibraryVersion
string(REGEX REPLACE "-.*$" "" DETECTED_VERSION "${DETECTED_VERSION}")

I haven't tested any of the code locally.

With regards to the maven-template plugin, I am vehemently against anything that complicates the build process. I appreciate the list of pro's and cons there.

it offers no clean way to filter a single class in place under src/main/java, which would have kept a compilable placeholder there.

This opinionated/prescriptive maven (convention over configuration) system is why I constantly butt heads with other maven advocates over complicated yet overly limited plugin usage. Had I written this feature, I'd probably just write an ant task to do this for us because maven just refuses to put any custom logic in our hands with the base maven product requiring plugins upon plugins for basic tasks like this. CMake and ant are both better in this regard, in my opinion because they allow us to have fine-grained control over things like command line parsing, pattern matching, etc.

I do have some outstanding maven regrets such as switching to native-lib-loader, so this shortcoming is just another nail in the coffin. /rant

That said, I won't hold up this PR based on this one plugin's usage.

@pietrygamat

Copy link
Copy Markdown
Collaborator Author

Regarding removal of -SNAPSHOT: the CMAKE has a main entrypoint that is triggered by ant/maven and this flow sets the variable JSSC_VERSION in ant:

            <!-- Set the JSSC version to parameter -->
            <arg line="-DJSSC_VERSION=&quot;${release.version}&quot;" />

We explicitly set the release version, because we wanted C api to match Java API on the x.y.z format - note SerialNativeInterface does not use SNAPSHOT now. And the -SNAPSHOT qualifier's purpose is for maven to keep -SNAPSHOTS separate from releases in local maven repository - we don't want user running mvn clean install to permanently overwrite our artifact with their "dirty" version.

The fallback in the CMake on current master is triggered only when rebuilding outside of java toolset, and since no ant that would set the version is involved - it reads it from pom.xml, and reads whatever is there, so the binary version resulting from that would NOT match the version reported by SerialInterface - that's a corner case I patch here. Not really major, we don't rely on artifacts build this way, but a smell.

Regarding ant task - that's great idea that never occurred to me. I'll try to rewrite it this way, because the maven limitation in this particular case irks me too.

@tresf tresf mentioned this pull request Oct 20, 2026
@tresf

tresf commented Oct 20, 2026 •

Copy link
Copy Markdown

We explicitly set the release version, because we wanted C api to match Java API on the x.y.z format - note SerialNativeInterface does not use SNAPSHOT now. And the -SNAPSHOT qualifier's purpose is for maven to keep -SNAPSHOTS separate from releases in local maven repository - we don't want user running mvn clean install to permanently overwrite our artifact with their "dirty" version.

Please forgive my misunderstanding here, but when I call mvn clean install it's imperative to install to -SNAPSHOT as I'm testing it locally prior to submitting the patch upstream. I'm not sure what is meant by "their dirty version" in this context, as a local build is always a dirty version, so the -SNAPSHOT is required.

Note that -SNAPSHOT is a Maven-ism, but it's also semver-compliant. For example 2.2.6-SNAPSHOT < 2.2.6 which is important for version comparison. Furthermore, it illustrates to a user using this artifact that it's not a formal release. It's probably a misunderstanding on my part, but I can't think of a reason to remove -SNAPSHOT from a local any builds.

@pietrygamat

Copy link
Copy Markdown
Collaborator Author

The SNAPSHOT is stripped only from a native binary, the java jar retains the SNAPSHOT, of course. A 2.10.5-SNAPSHOT jar comes with 2.10.5 jssc.so. Since we never distribute naked binaries that's not a problem, right?

@tresf

tresf commented Oct 20, 2026

Copy link
Copy Markdown

The SNAPSHOT is stripped only from a native binary, the java jar retains the SNAPSHOT, of course. A 2.10.5-SNAPSHOT jar comes with 2.10.5 jssc.so. Since we never distribute naked binaries that's not a problem, right?

Ah, that makes more sense... I still don't understand the purpose of this though. Don't we want to retain this information in the native binary too?

@pietrygamat

Copy link
Copy Markdown
Collaborator Author

I think the main reason NOT to mark native libraries as snapshots is because of how we store them. The compiled natives are currently source, not the output of the releasing build (the one that actually creates a package we publish to Maven Central and to Github releases. We need to have them committed to src before, and only set pom.xml version to non-snapshot version for exactly one git revision that the CI is creating and tagging.

Alternatives I see (all out of scope of this review);

  • Create an environment where all different natives can be build in a single execution + pom.xml/build.xml rewrite so all profiles can be executed together rather than one at a time.
  • Keep binaries in separate artifact, publish to Maven Central independently (say jssc-slim for only java, jssc-linux64 for only .so, and so on) and repackage that artifacts into the final jar where no compilation happens, just repackage - I'd see it as a realistic goal, and I believe this has been raised before.
  • Accept current compromise.

@tresf

tresf commented Oct 21, 2026

Copy link
Copy Markdown

I agree that your three proposals are out of scope. I do not agree that binaries should be missing an indication of unstableness. I have a fundamental philosophy that we should never cater a build system too much to a CI because then we'll only ever have CIs producing binaries and that's not how the lifecycle of a project works.

For example, if I have to patch my own copy of a DLL or SO, I should be able to tell at a glance that it's a version I built. I'm uncertain how the PR still offers this convenience. I'm not trying to minimize the problems that it brings though, so I'll test the code locally and put some thought into it.

@tresf

tresf commented Oct 21, 2026

Copy link
Copy Markdown

Ok, it appears SNAPSHOT was stripped back in #102, so we've stopped indicating that binaries are snapshotted. This is a regression, but one that's 5-years old. Ugh.

@pietrygamat

pietrygamat commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

I'm uncertain how the PR still offers this convenience

That is the whole point here - the marker added in this PR adds very exact information - version (x.y.x, as in "compatible with release "x.y.z", an API level if you will) + exact git revision the code was built from + -dirty to indicate the revision may be misleading, because C code was modified locally + origin indicating that it was build in our CI, or someone else's + target architecture.

Would you want that info returned to java runtime as well?

@tresf

tresf commented Oct 21, 2026 •

Copy link
Copy Markdown

So yeah there are two workflows and we seem to have failed our users by making our maven environment assume we're always running from CI. This is not a huge-deal in practice because 99.9% of the time most users will simply take our precompiled binaries and move along, but this fails our users when they're working from a local copy.

  1. Our local workflow should always prefer the versioning x.x.x-SNAPSHOT
  2. Our master branch should always prefer the versioning x.x.x-SNAPSHOT
  3. Only in the event of a release trigger, should we remove the -SNAPSHOT from the version information. This should be a flag or boolean variable that's only set during the release cycle of the build system.

What's not immediately obvious is how we'd ever get to 1. or 2. scenarios. Perhaps I'm missing something, but we shouldn't expect users to specify a snapshot is being created.... users should always create snapshotted versions. There have been many times that I've had to bundle a custom version of JSSC with my application and I rely on the -SNAPSHOT in the filename. I see no valid argument that the native libs shouldn't do the same.

That said, this is a 5-year old regression in behavior so it's completely reasonable to tackle this as a separate PR outside of this PR, but I don't think it's out of scope, I just think it's something that was overlooked when the automated release system was built.

I guess my point is that the explicit removal of -SNAPSHOT should never be welcomed anywhere in the build-system by default, so my original concern is still valid in the scope of this PR and if we clobber this value, we should explicitly comment that it's due to a 5-year code regression, not by design.

@tresf

tresf commented Oct 21, 2026

Copy link
Copy Markdown

because C code was modified locally

Hmm... how are we making any such claim? In Java with exact Java versions, code ~= binary is a relatively true statement, but with C++, two different compilers will create drastically different binaries. To the same point a different commit hash doesn't really mean that the C++ code has changed. I understand that your dirty marker is to help say "something changed since release" but it's not semver compliant and franky, I don't even understand the benefit of it over -SNAPSHOT (except the historical fact that we accidentally refactored it out 5 years ago).

@tresf

tresf commented Oct 21, 2026 •

Copy link
Copy Markdown

Would you want that info returned to java runtime as well?

Yeah... I think it's sane to be able to do a semantic version compare on 1.2.3 vs 1.2.4-SNAPSHOT vs 1.2.4.

Personally -- if I have to handle any versioning nuances -- I always do something like (Version).isGreaterThanOrEquivelentTo(1, 2, 4).

  • Version version = Version.parse("1.2.4-SNAPSHOT"): will flag pre-release semver and return false
  • Version version = Version.parse("1.2.4"): will return true.

(zafarkhaja/jsemver used in the above example, but any semver-compliant lib should behave identically)

To be fair, I've never had to do this at the native-binary level with JSSC specifically, but -SNAPSHOT was used to keep this versioning in alignment with the JSSC Java codebase. An argument could be made that these versions shouldn't even be related (since they represent two different things) but that's probably a separate conversation. I believe the pre-release information (e.g. -SNAPSHOT or similar) belongs in the version string when applicable unless it poses technical difficulties.

@pietrygamat

pietrygamat commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Only in the event of a release trigger, should we remove the -SNAPSHOT

OK, I will make that change. I'll revert cmake to read version from pom as is, and add a paramter to maven / ant build so there is a way to overwrite the version written into the bin, and we'll use it in a release event. You are right, it makes more sense anyway - as long as the compile-natives is run, and is small enough to fit in this PR.

Hmm... how are we making any such claim? ... I understand that your dirty marker is to help say "something changed since release"

No, it means the source code in src/main/cpp or cmakelists.txt in current workdir at the build time is different than the committed version - lines 199-201 of CMakeLists.txt will check using git if its available - it's meant to detect local uncommitted changes and mark the build as built from unknown sources, that can't really be traced to any specific revision.

@tresf

tresf commented Oct 21, 2026

Copy link
Copy Markdown

No, it means the source code in src/main/cpp or cmakelists.txt in current workdir at the build time is different than the committed version - lines 199-201 of CMakeLists.txt will check using git if its available - it's meant to detect local uncommitted changes and mark the build as built from unknown sources, that can't really be traced to any specific revision.

I had completely missed this (I saw it in my original review but it slipped my mind). This claim now makes perfect sense. Thanks for explaining.

@pietrygamat

Copy link
Copy Markdown
Collaborator Author

I'd probably just write an ant task to do this

IDEs do not pick ant tasks or maven templates cleanly - I either end up with missing classes or duplicate classes until run a build with command line tools. I guess at this point it costs nothing to just add a new classpath resource file with the version hard-coded at build time and just read it. It's slight deviation I wanted to avoid - returning a constant vs reading a file, but that class is already loading resources (libs), so it should be ok.

Now both getLibraryVersion and getNativeLibraryVersion use version exactly as stated in pom.

@tresf

tresf commented Oct 22, 2026 •

Copy link
Copy Markdown

I'd probably just write an ant task to do this

IDEs do not pick ant tasks or maven templates cleanly - I either end up with missing classes or duplicate classes until run a build with command line tools. I guess at this point it costs nothing to just add a new classpath resource file with the version hard-coded at build time and just read it. It's slight deviation I wanted to avoid - returning a constant vs reading a file, but that class is already loading resources (libs), so it should be ok.

Now both getLibraryVersion and getNativeLibraryVersion use version exactly as stated in pom.

Yeah editing source on-the-fly is messy no matter how we do it. In cmake + IDE land this is rather normal and the IDEs usually have good support for this. In mvn + IDE, I'm not so sure. I use ant for other projects and IntelliJ is pretty bad about ant support these days as they've removed the default ant plugin, I assume this is shared partially in your sentiments. It does at least prompt to install the ant-run plugin on first-run but I'm not sure how long the IDE will continue to do this.

I think the current solution is good however it has a few areas of improvement.

  • "unknown" is not semver compliant, wherease "0.0.0-UNKNOWN" is. I would recommending that the fall-back value is kept as semver-compliant so we never break parsers. This same pattern is used in CMakeLists.txt for similar reasons:
    # Fallback to parsing pom.xml if not provided
    set(JSSC_VERSION "0.0.0-UNKNOWN")
  • I believe current logic reads the embedded resource every time the API is called. I would generally move this to the class constructor instead so that continous calls to this API can use the cached value
  • With regards to on-the-fly editing of a .java file versus reading in a .properties file, I don't have much feedback to offer there, both have their downsides. The .java file is more resilient to file swaps but causes issues with version control. The .properties file is a convenient way to read this at runtime. Due to the nature of maven, some of this information is redundantly stored in META-INF/maven/io.github.java-native/jssc/pom.properties incase we wanted to move this value to a truly runtime-derived value. The way that C++ handles this issue is it uses header-guard so you can't import something more than once. To avoid the .gitignore type issues, I think Java could probably take a lesson from C++ here and allow a sort of "prefer this .java file over this .java file". I know this can be done with ant, but it will likely make the design even more complicated than it already is. I think I'd probably use a .java source swap add the .java file to .gitignore. This seems the most succinct, but your strategy is more declarative so it's easier to read a year from now.

I guess that's a lot to say that I don't think there's an easy way to tackle this problem. It does sort of make your old maven way look much cleaner in hindsight, but that's largely due to the fact that the IDEs are abandoning ant, not because its technique is any less cursed.

SerialNativeInterface.libVersion was hardcoded and only synced by a sed
step in the cross-compile workflow, so after a release it lagged behind
the pom version. Move it to a LibraryVersion class that reads the
version from jssc/jssc-version.properties, which maven fills in
through resource filtering.

getLibraryVersion() reports the jar's version as is, qualifier included
(2.10.5-SNAPSHOT). The test compares it with the version surefire passes
from the pom, and is skipped when run outside maven.

Drop the now unnecessary workflow step.
The natives reported release.version (2.10.5-SNAPSHOT -> 2.10.5) from
getNativeLibraryVersion(), in the ident and in VERSIONINFO, so a
snapshot build could not be told apart from a release. They now get the
pom version as is, from maven and from a plain cmake build alike, and
match getLibraryVersion().

Two ant properties override it:
- -Djssc.native.version=X sets it explicitly
- -Djssc.native.release=true drops the qualifier (2.10.5)

The release switch builds release natives while the pom still says
-SNAPSHOT, ahead of release:prepare. cross-compile.yml exposes it as a
"release" input, off by default.
@pietrygamat

Copy link
Copy Markdown
Collaborator Author

unknown" is not semver compliant, whereas "0.0.0-UNKNOWN" is.

👍

I believe current logic reads the embedded resource every time the API is called

Correction, it's read once in private static final String libVersion = LibraryVersion.get();, so during static class init. The API method reads the stored value, and the get() method is inaccessible to user code (package private) and we have no need to call it elsewhere.

META-INF/maven/io.github.java-native/jssc/pom.properties

Pretty sure it could not be read before packaging, so it's invisible to IDE, junit tests and gets lost for downstream projects shading classes. Let's keep away.

old maven way look much cleaner in hindsight
(curren) strategy is more declarative so it's easier to read

OK, 😨 🔫 gun to your head, which will you pick? :
A) build-time swap with the need to run compilation with maven, before IDE recognizes generated classes (dev experience change)
B) reading property on load (app behavior change - my current pick).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants