From 7b48429a6c1deb2fa6e06b9e20abe5e4c632086a Mon Sep 17 00:00:00 2001 From: Colin Sharples Date: Sat, 26 Sep 2026 14:35:57 +1200 Subject: [PATCH 1/3] One build product per library, and a download page that knows which (#112) Neither application compiles against a symbology library any more (#76, #93), so which one it ships with was the only thing still hard-coded. Profiles live in jmsfx-viewer, since that is the level a deliverable is produced at, and both applications inherit whichever is active: mvn -Pstandard verify mvn -Phallux verify No activeByDefault, and naming none now fails. A default-activated profile switches off the moment any other profile is named, and #103 put `mvn -Prelease deploy` in the release workflow - so a defaulted standard would have silently deactivated during a release and published an application with no library in it, which starts and then answers 500 to everything. An enforcer rule turns that into an immediate failure that says what to pass instead. It does mean `mvn verify` alone no longer works, which is the deliberate cost. jmsfx-server's fat jar carries the library as a classifier - jmsfx-server-2.0.0-standard.jar - so a jar can be identified without remembering how it was built. deploy.sh now takes the library as its first argument and names that classifier exactly, rather than picking the newest jar, so a stale build from another library cannot be shipped by accident. `optional` is gone from the creator's library dependency. It was there to stop runtime scope reaching consumers, but #109 removed the last consumer, and the same declaration next door produced a server jar that started and 500'd - spring-boot:repackage leaves optional dependencies out of BOOT-INF/lib. Having it in one and not the other was a trap. The download page now offers Windows/macOS/Linux buttons for the library and version that instance is running, so a visitor to a hallux subdomain gets the hallux creator without having to recognise it in a list of nine files. The release workflow's packaging matrix crosses library with platform to produce them - six jobs now, nine when #81 brings jmsfx-battleorder in. Two things found while doing it, both of which would have shipped: **The manifest carries no Implementation-Version.** #112 assumed Spring Boot writes it, and it does not here: maven-jar-plugin writes those entries only when addDefaultImplementationEntries is set, which spring-boot-starter-parent does and jmsfx-parent does not. Reading it returned null on every real deployment while the unit tests, which pass a version in, looked perfectly healthy. Now uses spring-boot's build-info goal and Spring's BuildProperties. **A snapshot is not a release.** The live site is deployed straight from the working tree, so it reports 2.0.0-SNAPSHOT - for which no GitHub release exists, making every button a 404. A snapshot now falls back to the releases page, as does a missing version. Also here, because this work needed them: - the junit BOM and hamcrest's version were repeated verbatim in three poms; both now sit in jmsfx-parent and modules just name the artefacts - release.yml's header documented editing poms by hand because versions:set "has picked up drifted per-module versions before". The cause is #102: no module declares the aggregator as its parent, so the default parent-chain walk bumps the aggregator alone. It needs -DprocessAllModules=true, which is now written down where it is needed. mvn -Pstandard verify, -Phallux verify and -Pstandard,release verify all green. Both server jars checked at runtime: each reports its own library from /info/library and offers its own bundles on /download. Co-Authored-By: Claude Opus 5 --- .github/workflows/maven.yml | 6 +- .github/workflows/release.yml | 53 +++++++--- CLAUDE.md | 4 + README.md | 10 +- docs/fragments.md | 4 +- jmsfx-core/pom.xml | 12 --- jmsfx-parent/pom.xml | 30 ++++++ jmsfx-tools/jmsfx-generator/pom.xml | 12 --- jmsfx-viewer/jmsfx-creator/pom.xml | 31 +++--- jmsfx-viewer/jmsfx-server/deploy/README.md | 27 +++++- jmsfx-viewer/jmsfx-server/deploy/deploy.sh | 31 ++++-- jmsfx-viewer/jmsfx-server/pom.xml | 31 ++++-- .../ctgnz/jmsfx/server/CreatorDownloads.java | 95 ++++++++++++++++++ .../ctgnz/jmsfx/server/WebController.java | 23 ++++- .../main/resources/templates/download.html | 27 ++++-- .../jmsfx/server/CreatorDownloadsTest.java | 75 ++++++++++++++ jmsfx-viewer/pom.xml | 97 +++++++++++++++++++ library/jmsfx-standard/pom.xml | 12 --- 18 files changed, 481 insertions(+), 99 deletions(-) create mode 100644 jmsfx-viewer/jmsfx-server/src/main/java/io/github/ctgnz/jmsfx/server/CreatorDownloads.java create mode 100644 jmsfx-viewer/jmsfx-server/src/test/java/io/github/ctgnz/jmsfx/server/CreatorDownloadsTest.java diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 8fd6a84b..317e2a5f 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -30,4 +30,8 @@ jobs: # display is available, as on a bare ubuntu-latest runner. run: sudo apt-get update && sudo apt-get install -y xvfb - name: Build with Maven - run: xvfb-run --auto-servernum mvn -B clean verify + # -Pstandard names a library for the viewer applications, which since jmsfx#112 have no + # default - naming none is refused rather than producing an application with nothing to + # render with. CI covers the standard library; the per-library bundles are the release + # workflow's job. + run: xvfb-run --auto-servernum mvn -B clean verify -Pstandard diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2c42338c..ebc57303 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,12 +1,18 @@ # Publishes a GitHub Release for a tagged jmsfx version. # -# Tag a commit whose pom version has had -SNAPSHOT removed everywhere, and whose tag equals that version. Edit -# each module's pom.xml by hand rather than mvn versions:set - this reactor has picked up drifted per-module -# versions before when relying on the plugin - then: +# Tag a commit whose pom version has had -SNAPSHOT removed everywhere, and whose tag equals that version: # +# mvn versions:set -DnewVersion=1.2.0 -DprocessAllModules=true # git commit -am "Release 1.2.0" # git tag 1.2.0 && git push origin master 1.2.0 # +# processAllModules is not optional, and its absence is what made versions:set look unreliable here. Since +# jmsfx#102 split the parent from the aggregator, no module declares the aggregator as its parent - so the +# default behaviour, which walks the parent chain, changes the aggregator's own version and nothing else, and +# leaves a tree where one pom says 1.2.0 and nine say 1.1.0-SNAPSHOT. Verify before tagging: +# +# grep -rn "" --include=pom.xml . | grep -v target | grep SNAPSHOT +# # jmsfx isn't published to Maven Central yet (unlike foxglove) - this only builds jars and attaches them to a # GitHub Release. Once Central Portal publishing is set up here too, add a deploy step matching foxglove's # release.yml (central-publishing-maven-plugin, GPG signing, the release profile). @@ -74,8 +80,12 @@ jobs: # Tests already ran via maven.yml's own push-to-master trigger, before this commit was ever tagged - no need # to run them again here. + # -Pstandard because jmsfx#112 gives the applications one build product per library and no + # default - a build naming none is refused by an enforcer rule rather than quietly producing an + # application with no library in it. The library named here is the one whose artifacts go to + # Central, which is the standard APP-6E one. - name: Build - run: mvn -B --no-transfer-progress -DskipTests package + run: mvn -B --no-transfer-progress -DskipTests -Pstandard package # Tag only: a manual run carries a snapshot version, which the Portal will not take, and # publishing is not what a rehearsal is for. autoPublish is false in the pom, so this stages @@ -83,7 +93,11 @@ jobs: # can be dropped rather than having to be superseded. - name: Publish to the Maven Central Portal if: github.ref_type == 'tag' - run: mvn -B --no-transfer-progress -DskipTests -Prelease deploy + # Both profiles: -Prelease alone now fails the enforcer rule, which is the point of it. That + # is exactly the trap jmsfx#112 describes - naming a profile deactivates an activeByDefault + # one, so a defaulted library would have silently dropped out here and published an + # application that starts and then answers 500 to everything. + run: mvn -B --no-transfer-progress -DskipTests -Pstandard,release deploy env: MAVEN_USERNAME: ${{ secrets.CENTRAL_USERNAME }} MAVEN_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} @@ -104,13 +118,19 @@ jobs: # cross-compiling. Maven resolving JavaFX's native classifier from the build host # works in our favour here: each runner picks up its own platform's natives. package: - name: Package (${{ matrix.os }}) + name: Package (${{ matrix.library }}, ${{ matrix.target.platform }}) needs: build strategy: - # One platform failing should not deny the others their artifacts. + # One combination failing should not deny the others their artifacts. fail-fast: false + # A genuine cross product: every library on every platform, which is what jmsfx#112 means by + # one build product per library. Two libraries by three platforms is six jobs today, and + # jmsfx-battleorder makes it nine once jmsfx#81 can generate it - one word on the line below. matrix: - include: + library: + - standard + - hallux + target: - os: windows-latest platform: windows - os: macos-latest @@ -121,7 +141,7 @@ jobs: # decision about who can run the download, not a housekeeping detail. - os: ubuntu-24.04 platform: linux - runs-on: ${{ matrix.os }} + runs-on: ${{ matrix.target.os }} steps: # The brand submodule feeds jmsfx-server's stylesheet. Maven skips a missing # resource directory with only a warning, so without this the reactor builds @@ -152,7 +172,7 @@ jobs: - name: Build jmsfx-creator and collect its dependencies shell: bash - run: mvn -B --no-transfer-progress -DskipTests -pl :jmsfx-creator -am package + run: mvn -B --no-transfer-progress -DskipTests -P${{ matrix.library }} -pl :jmsfx-creator -am package - name: Stage the application jar alongside its dependencies shell: bash @@ -186,7 +206,7 @@ jobs: cd jmsfx-viewer/jmsfx-creator/target/dist ls -la . | sed 's/^/ /' find . -name "*.jar" | wc -l | xargs echo "jars in image:" - case "${{ matrix.platform }}" in + case "${{ matrix.target.platform }}" in windows) LAUNCHER="JMSFX Icon Creator/JMSFX Icon Creator.exe" ;; linux) LAUNCHER="JMSFX Icon Creator/bin/JMSFX Icon Creator" ;; macos) LAUNCHER="JMSFX Icon Creator.app/Contents/MacOS/JMSFX Icon Creator" ;; @@ -198,7 +218,7 @@ jobs: # Only Linux can do this: a virtual display is available there, where the Windows # and macOS runners cannot be relied on to have an interactive session. So macOS # in particular ships without ever having been seen to run - see issue #41. - if: matrix.platform == 'linux' + if: matrix.target.platform == 'linux' run: | sudo apt-get update && sudo apt-get install -y xvfb cd "jmsfx-viewer/jmsfx-creator/target/dist/JMSFX Icon Creator/bin" @@ -213,8 +233,11 @@ jobs: shell: bash run: | cd jmsfx-viewer/jmsfx-creator/target/dist - ARCHIVE="jmsfx-creator-${VERSION}-${{ matrix.platform }}" - case "${{ matrix.platform }}" in + # library then platform, matching CreatorDownloads in jmsfx-server - the download page + # builds these names to link to, and nothing checks that the two agree, so + # CreatorDownloadsTest spells them out in full. + ARCHIVE="jmsfx-creator-${VERSION}-${{ matrix.library }}-${{ matrix.target.platform }}" + case "${{ matrix.target.platform }}" in linux) tar czf "../${ARCHIVE}.tar.gz" "JMSFX Icon Creator" ;; @@ -233,7 +256,7 @@ jobs: if: github.ref_type != 'tag' uses: actions/upload-artifact@v7 with: - name: jmsfx-creator-${{ matrix.platform }} + name: jmsfx-creator-${{ matrix.library }}-${{ matrix.target.platform }} path: | jmsfx-viewer/jmsfx-creator/target/jmsfx-creator-*.zip jmsfx-viewer/jmsfx-creator/target/jmsfx-creator-*.tar.gz diff --git a/CLAUDE.md b/CLAUDE.md index 8951e253..9bcf470c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,10 @@ Modules are grouped by deliverable rather than flat (#102), which is also how th - `jmsfx-viewer/` - `jmsfx-creator`, `jmsfx-server`. A real parent pom: one capability, two front ends. - `library/` - `jmsfx-standard`, `jmsfx-hallux`, `jmsfx-battleorder`. A **plain directory, not a module** - these are exactly the lifecycles that should not move together. `jmsfx-battleorder` is documentation only until #81 can generate it, so it is not in `` yet. +**Every build must name a library**: `mvn -Pstandard verify` or `-Phallux` (#112). The profiles live in `jmsfx-viewer` and supply the runtime library both applications ship with; there is no `activeByDefault`, because naming any other profile would deactivate it silently, and an enforcer rule refuses a build that names none. `jmsfx-server`'s fat jar carries the library as a **classifier** - `jmsfx-server-2.0.0-standard.jar` - so the thin unclassified jar beside it is not the one to run. + +**`mvn versions:set` needs `-DprocessAllModules=true`.** Since #102 no module declares the root aggregator as its parent, so the default parent-chain walk bumps the aggregator alone and leaves every other pom behind. This is what made the plugin look unreliable here. + A dependency that crosses a group boundary resolves through `${jmsfx.core.version}` or `${jmsfx.standard.version}`, declared in `jmsfx-parent`; both default to `${project.version}`, so pinning one to a released version is what splits that lifecycle off. In-group dependencies still use `${project.version}` directly. Prefer `-pl :jmsfx-server` over `-pl jmsfx-viewer/jmsfx-server` - selecting by artifactId survives a directory move. ## Domain background diff --git a/README.md b/README.md index f8b09042..dff9c189 100644 --- a/README.md +++ b/README.md @@ -62,16 +62,20 @@ Requires **JDK 25** and Maven. JMSFX is not yet published to Maven Central, so b ```sh git clone --recurse-submodules https://github.com/ctgnz/jmsfx.git cd jmsfx -mvn install +mvn -Pstandard install ``` +`-Pstandard` names the symbology library the two applications ship with. There is deliberately no +default - a build that names none is refused rather than producing an application with nothing to +render with - so every `mvn` invocation here carries `-Pstandard` or `-Phallux`. + The `--recurse-submodules` matters: `jmsfx-server` takes its branding stylesheet from the [ctg-brand](https://github.com/ctgnz/ctg-brand) submodule, and Maven will quietly skip the missing directory rather than fail if it is absent. To run the web application locally: ```sh -mvn -pl :jmsfx-server -am verify -java -jar jmsfx-viewer/jmsfx-server/target/jmsfx-server-*.jar +mvn -Pstandard -pl :jmsfx-server -am verify +java -jar jmsfx-viewer/jmsfx-server/target/jmsfx-server-*-standard.jar ``` It serves on port 8080 by default. diff --git a/docs/fragments.md b/docs/fragments.md index 3e1be612..d95ef6ef 100644 --- a/docs/fragments.md +++ b/docs/fragments.md @@ -102,7 +102,7 @@ ends up dominated by changes that have nothing to do with the drawing. `FragmentNormaliser` puts it back: ``` -mvn -q -pl :jmsfx-generator exec:java \ +mvn -q -Pstandard -pl :jmsfx-generator exec:java \ -Dexec.mainClass=io.github.ctgnz.jmsfx.generator.FragmentNormaliser -Dexec.args=--apply ``` @@ -150,7 +150,7 @@ something that is not about its shape would only be noise. with its ancestors' attributes folded in, and re-run `FragmentMeasurer` to confirm no bounds moved. 4. Mirror it into the other tree. The shared fragments are byte-identical between `jmsfx-standard` and `hallux`, and are meant to stay that way. -5. `mvn verify`. +5. `mvn -Pstandard verify`. ## Related diff --git a/jmsfx-core/pom.xml b/jmsfx-core/pom.xml index 789ffaee..2da58ad4 100644 --- a/jmsfx-core/pom.xml +++ b/jmsfx-core/pom.xml @@ -11,17 +11,6 @@ ../jmsfx-parent/pom.xml - - - - org.junit - junit-bom - 6.1.3 - pom - import - - - @@ -39,7 +28,6 @@ org.hamcrest hamcrest - 3.0 test diff --git a/jmsfx-parent/pom.xml b/jmsfx-parent/pom.xml index 14940f1c..b743f86c 100644 --- a/jmsfx-parent/pom.xml +++ b/jmsfx-parent/pom.xml @@ -56,8 +56,32 @@ --> ${project.version} ${project.version} + ${project.version} + + + + + org.junit + junit-bom + 6.1.3 + pom + import + + + org.hamcrest + hamcrest + 3.0 + test + + + + release @@ -203,6 +227,12 @@ maven-source-plugin 3.3.0 + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + diff --git a/jmsfx-tools/jmsfx-generator/pom.xml b/jmsfx-tools/jmsfx-generator/pom.xml index 985d38a7..a8aee506 100644 --- a/jmsfx-tools/jmsfx-generator/pom.xml +++ b/jmsfx-tools/jmsfx-generator/pom.xml @@ -17,17 +17,6 @@ 2.22.2 - - - - org.junit - junit-bom - 6.1.3 - pom - import - - - - - io.github.ctgnz - jmsfx-standard - ${jmsfx.standard.version} - runtime - true - diff --git a/jmsfx-viewer/jmsfx-server/deploy/README.md b/jmsfx-viewer/jmsfx-server/deploy/README.md index 8b2fdfdf..ffcb69c3 100644 --- a/jmsfx-viewer/jmsfx-server/deploy/README.md +++ b/jmsfx-viewer/jmsfx-server/deploy/README.md @@ -57,8 +57,8 @@ deployed. From your workstation: ```sh -./deploy.sh ubuntu@ -# or: JMSFX_HOST=ubuntu@ ./deploy.sh +./deploy.sh standard ubuntu@ +# or: JMSFX_LIBRARY=standard JMSFX_HOST=ubuntu@ ./deploy.sh ``` It builds with `-am` so the `brand` submodule and upstream modules are @@ -78,3 +78,26 @@ The JVM is capped at 60% of instance RAM (`-XX:MaxRAMPercentage=60`). If it gets OOM-killed under load — visible as an abrupt restart in the journal with no stack trace — move up a plan rather than raising the percentage, since Caddy and the OS need the remainder. + +## One instance per library + +The library is the first argument and is never inferred, because there is one instance per library - +each on its own `*.ctg.co.nz` subdomain - and deploying the wrong one is completely silent: the site +comes up, serves icons, and is simply the wrong symbology. See jmsfx#112. + +`deploy.sh` builds with that library's profile and ships the jar carrying its classifier +(`jmsfx-server--.jar`). The unclassified jar beside it in `target/` is the thin one +Spring Boot repackaged from and will not run, which is why the script names the classifier exactly +rather than picking the newest jar. + +Each host holds one instance, so the paths on the box do not change per library - `/opt/jmsfx/` and +the `jmsfx-server` unit are the same everywhere. What distinguishes them is which host you deploy to. + +To confirm you got the one you meant, ask the running instance: + +```sh +curl https://.ctg.co.nz/info/library +# {"name":"Hallux","symbolSets":23} +``` + +That endpoint exists for exactly this - see jmsfx#111. diff --git a/jmsfx-viewer/jmsfx-server/deploy/deploy.sh b/jmsfx-viewer/jmsfx-server/deploy/deploy.sh index 9827b764..fc04e695 100755 --- a/jmsfx-viewer/jmsfx-server/deploy/deploy.sh +++ b/jmsfx-viewer/jmsfx-server/deploy/deploy.sh @@ -1,15 +1,24 @@ #!/usr/bin/env bash # # Build and ship jmsfx-server to the Lightsail instance. -# Run from your workstation: ./deploy.sh [user@host] +# Run from your workstation: ./deploy.sh [user@host] +# +# The library is named, never guessed: there is one instance per library, each on its own +# subdomain, and deploying the wrong one is silent. See jmsfx#112. # set -euo pipefail -TARGET="${1:-${JMSFX_HOST:-}}" -if [[ -z "${TARGET}" ]]; then - echo "usage: ./deploy.sh user@host (or set JMSFX_HOST)" >&2 +LIBRARY="${1:-${JMSFX_LIBRARY:-}}" +TARGET="${2:-${JMSFX_HOST:-}}" +if [[ -z "${LIBRARY}" || -z "${TARGET}" ]]; then + echo "usage: ./deploy.sh user@host (or set JMSFX_LIBRARY and JMSFX_HOST)" >&2 + echo " library is one of: standard, hallux" >&2 exit 1 fi +case "${LIBRARY}" in + standard|hallux) ;; + *) echo "unknown library '${LIBRARY}' - expected standard or hallux" >&2; exit 1 ;; +esac # Lightsail hands out its own key pair, so allow pointing at one rather than # requiring an ~/.ssh/config entry. @@ -28,13 +37,21 @@ if [[ ! -f "${REPO_ROOT}/brand/css/brand.css" ]]; then exit 1 fi -echo "==> building" +echo "==> building ${LIBRARY}" cd "${REPO_ROOT}" +VERSION=$(mvn -q -B help:evaluate -Dexpression=project.version -DforceStdout -pl :jmsfx-server 2>/dev/null | tail -n1) # verify, not package: spring-boot:repackage is bound to post-integration-test, # so package alone leaves a thin jar that will not run. -mvn -B -pl :jmsfx-server -am verify +mvn -B -P"${LIBRARY}" -pl :jmsfx-server -am verify -JAR=$(ls -t "${REPO_ROOT}"/jmsfx-viewer/jmsfx-server/target/jmsfx-server-*.jar | grep -v '\.original$' | head -1) +# The fat jar is the one carrying the library classifier; the unclassified jar beside it is the +# thin one Spring Boot repackaged from, and it will not run. Naming the classifier exactly also +# means a stale jar from another library cannot be picked up by accident. +JAR="${REPO_ROOT}/jmsfx-viewer/jmsfx-server/target/jmsfx-server-${VERSION}-${LIBRARY}.jar" +if [[ ! -f "${JAR}" ]]; then + echo "no ${LIBRARY} jar at ${JAR}" >&2 + exit 1 +fi echo "==> shipping $(basename "${JAR}") ($(du -h "${JAR}" | cut -f1))" # Upload beside the live jar, then swap and restart, so a failed transfer diff --git a/jmsfx-viewer/jmsfx-server/pom.xml b/jmsfx-viewer/jmsfx-server/pom.xml index aa438c3e..17838cd9 100644 --- a/jmsfx-viewer/jmsfx-server/pom.xml +++ b/jmsfx-viewer/jmsfx-server/pom.xml @@ -30,12 +30,9 @@ jmsfx-core ${jmsfx.core.version} - - io.github.ctgnz - jmsfx-standard - runtime - ${jmsfx.standard.version} - + + ${jmsfx.library} diff --git a/jmsfx-viewer/jmsfx-server/src/main/java/io/github/ctgnz/jmsfx/server/CreatorDownloads.java b/jmsfx-viewer/jmsfx-server/src/main/java/io/github/ctgnz/jmsfx/server/CreatorDownloads.java new file mode 100644 index 00000000..97ce1569 --- /dev/null +++ b/jmsfx-viewer/jmsfx-server/src/main/java/io/github/ctgnz/jmsfx/server/CreatorDownloads.java @@ -0,0 +1,95 @@ +package io.github.ctgnz.jmsfx.server; + +import java.util.List; +import java.util.Locale; + +import io.github.ctgnz.jmsfx.IconLibrary; + +/** + * The Icon Creator bundles this server instance should offer: the three platforms, for the one library it is running. + *

+ * The download page used to be the same page everywhere - a link to the releases page and a table saying which filename to look for. With an instance per library, each on its own + * subdomain, that asks the visitor to know something they came here to be told: someone on a hallux subdomain wants the hallux creator, and should not have to recognise that from + * a list of nine files. + *

+ * Naming a bundle needs two facts about this instance, and neither is a build-time constant any more. The library comes from {@link IconLibrary#getName()} (jmsfx#111, which exists + * for exactly this), and the version from Spring's {@code BuildProperties} - so a 2.0.0 server offers the 2.0.0 creator rather than whatever happens to be newest. + *

+ * Deliberately not the manifest's {@code Implementation-Version}, which is the obvious guess and is simply absent here: maven-jar-plugin writes those entries only when + * {@code addDefaultImplementationEntries} is set, which {@code spring-boot-starter-parent} does and {@code jmsfx-parent} does not. The fat jar carries no {@code Implementation-*} + * at all, so reading it returned null on every real deployment - while every unit test that passed a version in looked perfectly healthy. + *

+ * A version can still be absent, when there is no build-info on the classpath, and a snapshot is present but is not a release. Neither is an error and neither must look like one: + * with nothing to name, {@link #versioned()} is false and the page falls back to the releases page, as it did before. See jmsfx#112. + */ +public record CreatorDownloads(String library, String version, List bundles) { + + private static final String RELEASES = "https://github.com/ctgnz/jmsfx/releases"; + + /** + * One platform's bundle. + * + * @param system + * what a reader calls the platform - this is button text, not an identifier + * @param platform + * the token in the filename, matching the release workflow's packaging matrix + * @param filename + * the asset name, or null when the version is unknown + * @param url + * where to get it, or the releases page when the version is unknown + */ + public record Bundle(String system, String platform, String filename, String url) { + } + + /** + * Windows and macOS ship as zip; Linux as tar.gz, which preserves the launcher's permission bits. Kept in step with the {@code Archive the image} step in release.yml - the + * workflow writes these names and this reads them, and nothing checks that they agree. + */ + private static final List PLATFORMS = List.of(new String[] { + "Windows", "windows", "zip" + }, new String[] { + "macOS", "macos", "zip" + }, new String[] { + "Linux", "linux", "tar.gz" + }); + + /** Takes the library's name rather than the library, and the version rather than a way of finding one, because that is all this needs to name a file. */ + public static CreatorDownloads of(String libraryName, String version) { + // The classifier is the lowercased library name, which is how the profile ids and the + // bundle filenames spell it - "Standard" here is "standard" there. + String library = libraryName.toLowerCase(Locale.ROOT); + List bundles = PLATFORMS.stream() + .map(platform -> bundle(library, version, platform[0], platform[1], platform[2])) + .toList(); + return new CreatorDownloads(library, version, bundles); + } + + private static Bundle bundle(String library, String version, String system, String platform, String extension) { + if (!isRelease(version)) { + return new Bundle(system, platform, null, RELEASES + "/latest"); + } + String filename = String.format("jmsfx-creator-%s-%s-%s.%s", version, library, platform, extension); + return new Bundle(system, platform, filename, String.format("%s/download/%s/%s", RELEASES, version, filename)); + } + + /** + * Whether this instance can name the release its bundles came from, and so offer them directly. + *

+ * False in the two cases where it cannot, both of which are normal rather than faults. Running from exploded classes - any run from an IDE - leaves no manifest to read a + * version from. And a snapshot is not a release: the live site is deployed straight from the working tree, so it reports something like {@code 2.0.0-SNAPSHOT}, for which no + * GitHub release exists and every button would be a 404. Offering the releases page instead is worse than a direct download and much better than a dead link. + */ + public boolean versioned() { + return isRelease(version); + } + + private static boolean isRelease(String version) { + return version != null && !version.isBlank() && !version.endsWith("-SNAPSHOT"); + } + + /** The releases page, for the cases the buttons cannot cover. */ + public String releasesUrl() { + return RELEASES; + } + +} diff --git a/jmsfx-viewer/jmsfx-server/src/main/java/io/github/ctgnz/jmsfx/server/WebController.java b/jmsfx-viewer/jmsfx-server/src/main/java/io/github/ctgnz/jmsfx/server/WebController.java index c54265dc..87652f4b 100644 --- a/jmsfx-viewer/jmsfx-server/src/main/java/io/github/ctgnz/jmsfx/server/WebController.java +++ b/jmsfx-viewer/jmsfx-server/src/main/java/io/github/ctgnz/jmsfx/server/WebController.java @@ -1,5 +1,7 @@ package io.github.ctgnz.jmsfx.server; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.info.BuildProperties; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -14,6 +16,16 @@ @Controller public class WebController { + /** + * Absent when nothing put build-info on the classpath. ObjectProvider rather than a required bean so that stays a page that offers the releases page instead of an application + * that will not start. + */ + private final ObjectProvider buildProperties; + + WebController(ObjectProvider buildProperties) { + this.buildProperties = buildProperties; + } + @GetMapping({ "/" }) @@ -35,10 +47,19 @@ public String browseSymbolSets() { return "entity-list"; } + /** + * The bundles offered here are the ones for the library this instance is running, so a visitor to a hallux subdomain gets the hallux creator without having to know that is + * what they want. See jmsfx#112. + */ @GetMapping({ "/download" }) - public String downloadCreator() { + public String downloadCreator(Model model) { + String version = buildProperties.getIfAvailable() == null ? null + : buildProperties.getObject() + .getVersion(); + model.addAttribute("downloads", CreatorDownloads.of(IconLibrary.discover() + .getName(), version)); return "download"; } diff --git a/jmsfx-viewer/jmsfx-server/src/main/resources/templates/download.html b/jmsfx-viewer/jmsfx-server/src/main/resources/templates/download.html index c4d7c9c0..836a5f60 100644 --- a/jmsfx-viewer/jmsfx-server/src/main/resources/templates/download.html +++ b/jmsfx-viewer/jmsfx-server/src/main/resources/templates/download.html @@ -18,13 +18,28 @@

Icon Creator for the desktop
Each download carries its own Java runtime, so there is nothing else to install - no JDK, and no JavaFX.

-
- Downloads on GitHub + + + + + +

+ These are the standard builds, version + 2.0.0 - matching this site, so the icons you compose + on the desktop are the ones you see here. +

- + + @@ -35,17 +50,17 @@
Icon Creator for the desktop
- + - + - + diff --git a/jmsfx-viewer/jmsfx-server/src/test/java/io/github/ctgnz/jmsfx/server/CreatorDownloadsTest.java b/jmsfx-viewer/jmsfx-server/src/test/java/io/github/ctgnz/jmsfx/server/CreatorDownloadsTest.java new file mode 100644 index 00000000..b65d7367 --- /dev/null +++ b/jmsfx-viewer/jmsfx-server/src/test/java/io/github/ctgnz/jmsfx/server/CreatorDownloadsTest.java @@ -0,0 +1,75 @@ +package io.github.ctgnz.jmsfx.server; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; + +import org.junit.jupiter.api.Test; + +/** + * These names have to match the ones release.yml writes, and nothing enforces that: the workflow builds the files and this reads them, with no shared definition between the two. + * So the expected names are spelled out in full here rather than rebuilt from the same parts, which would agree with itself whatever it did. + */ +class CreatorDownloadsTest { + + @Test + void namesTheBundlesForItsOwnLibraryAndVersion() { + CreatorDownloads downloads = CreatorDownloads.of("Standard", "2.0.0"); + + assertThat(downloads.bundles() + .stream() + .map(CreatorDownloads.Bundle::filename) + .toList(), + contains("jmsfx-creator-2.0.0-standard-windows.zip", "jmsfx-creator-2.0.0-standard-macos.zip", "jmsfx-creator-2.0.0-standard-linux.tar.gz")); + } + + @Test + void linksToTheReleaseTheVersionNames() { + assertThat(CreatorDownloads.of("Hallux", "2.1.0") + .bundles() + .getFirst() + .url(), is("https://github.com/ctgnz/jmsfx/releases/download/2.1.0/jmsfx-creator-2.1.0-hallux-windows.zip")); + } + + /** The library name is capitalised on IconLibrary and lowercase in a filename, which is also how the Maven profile and the jar classifier spell it. */ + @Test + void lowercasesTheLibraryName() { + assertThat(CreatorDownloads.of("BattleOrder", "2.0.0") + .library(), is("battleorder")); + } + + /** Running from exploded classes - an IDE - leaves no manifest to read, which is not an error. */ + @Test + void fallsBackToTheReleasesPageWithNoVersion() { + CreatorDownloads downloads = CreatorDownloads.of("Standard", null); + + assertThat(downloads.versioned(), is(false)); + assertThat(downloads.bundles() + .getFirst() + .filename(), is(nullValue())); + assertThat(downloads.bundles() + .getFirst() + .url(), is("https://github.com/ctgnz/jmsfx/releases/latest")); + } + + /** + * The live site is deployed from the working tree, so it reports a snapshot version - and there is no GitHub release by that name, which would make every button a 404. + */ + @Test + void treatsASnapshotAsNotAReleaseAtAll() { + CreatorDownloads downloads = CreatorDownloads.of("Standard", "2.0.0-SNAPSHOT"); + + assertThat(downloads.versioned(), is(false)); + assertThat(downloads.bundles() + .getFirst() + .url(), is("https://github.com/ctgnz/jmsfx/releases/latest")); + } + + @Test + void treatsABlankVersionAsAbsent() { + assertThat(CreatorDownloads.of("Standard", " ") + .versioned(), is(false)); + } + +} diff --git a/jmsfx-viewer/pom.xml b/jmsfx-viewer/pom.xml index 2482b9b8..d85d3e43 100644 --- a/jmsfx-viewer/pom.xml +++ b/jmsfx-viewer/pom.xml @@ -27,4 +27,101 @@ jmsfx-server + + + + standard + + standard + + + + io.github.ctgnz + jmsfx-standard + ${jmsfx.standard.version} + runtime + + + + + hallux + + hallux + + + + io.github.ctgnz + jmsfx-hallux + ${jmsfx.hallux.version} + runtime + + + + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + + + require-a-library + validate + + enforce + + + + + jmsfx.library + No symbology library was named, so this build has nothing to render with. + +Name one on the command line: + + mvn -Pstandard verify the APP-6E library + mvn -Phallux verify the hallux extension + +There is deliberately no default - see the comment above this rule, and jmsfx#112. + + + + + + + + + diff --git a/library/jmsfx-standard/pom.xml b/library/jmsfx-standard/pom.xml index f2d3cf30..130228c7 100644 --- a/library/jmsfx-standard/pom.xml +++ b/library/jmsfx-standard/pom.xml @@ -11,17 +11,6 @@ ../../jmsfx-parent/pom.xml - - - - org.junit - junit-bom - 6.1.3 - pom - import - - - @@ -41,7 +30,6 @@ org.hamcrest hamcrest - 3.0 test From 982d07c60ad6449b0545f4f799004b0af44c28d1 Mon Sep 17 00:00:00 2001 From: Colin Sharples Date: Sat, 26 Sep 2026 15:53:28 +1200 Subject: [PATCH 2/3] Stop the aggregator inheriting jmsfx-parent (#102 follow-up) Noticed while explaining the two-pom layout: the root aggregator declared jmsfx-parent as its parent, and had no use for it. It has no sources, so nothing it inherited acted on anything - and the block was a second place jmsfx-parent's version had to be written, which is half of what splitting the parent from the aggregator was meant to stop. jmsfx-parent is still listed as a module, because this reactor builds it. That is a different relationship from inheritance, and conflating the two is what made the layout confusing to read. Five modules now declare jmsfx-parent as their parent, and nothing else references its version. mvn -Pstandard clean verify and -Pstandard,release verify both green - the release profile still activates from the modules that define it, and every javadoc and sources jar still builds. Co-Authored-By: Claude Opus 5 --- pom.xml | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index ed368fa6..a90fdfc0 100644 --- a/pom.xml +++ b/pom.xml @@ -7,22 +7,17 @@ JMSFX - Joint Military Symbology on JavaFX A JavaFX implementation of NATO APP-6E military symbology: a generated domain model of the standard, and the SVG composition that draws a symbol from it. - - io.github.ctgnz - jmsfx-parent - 2.0.0-SNAPSHOT - jmsfx-parent/pom.xml - - org.sonatype.central central-publishing-maven-plugin @@ -199,7 +200,6 @@ jmsfx-viewer jmsfx-creator jmsfx-server - jmsfx-hallux
Pick the archive matching your system from the release page.What each button gives you.Pick the archive matching your system from the release page.
System
Windows...-windows.zip...-windows.zip 64-bit. Unzip, then run JMSFX Icon Creator.exe.
macOS...-macos.zip...-macos.zip Apple Silicon only Untested
Linux...-linux.tar.gz...-linux.tar.gz 64-bit. Extract, then run bin/JMSFX Icon Creator.