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..a6457465 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 @@ -130,22 +154,23 @@ Portal and publishing stays a deliberate step, so a bad staging can be dropped rather than having to be superseded. - Only the parent, jmsfx-core and jmsfx-standard belong on Central: the API a consumer - compiles against and the generated library it renders with. The parent is easy to - overlook and resolution breaks for everyone without it. The rest are excluded - - creator, editor and server are applications, delivered as bundles and a deployment. + What goes to Central is jmsfx-parent, jmsfx-core and the libraries: the API a + consumer compiles against, and the generated libraries they render with. The parent + is easy to overlook and resolution breaks for everyone without it. The rest are + excluded - creator, editor and server are applications, delivered as bundles and a + deployment, not things anyone resolves as a dependency. The generator stays unpublished: jmsfx#102 settled on a monorepo rather than split library repositories, so every library that generates itself does so from the generator in the same tree and never resolves it as a dependency. If a library ever does live elsewhere, jmsfx-generator and jmsfx-tools come off this list together. - jmsfx-hallux is excluded deliberately, and the exclusion is load-bearing rather - than tidy-up. jmsfx#106 brought it into this reactor after 1.5.0 shipped, so the - release profile has never run with it present; without an exclusion the next - -Prelease deploy would publish it to Central, and a publication to Central cannot - be withdrawn. Whether an extension library belongs there is a decision that has - not been taken. Until it is, it stays off. + jmsfx-hallux publishes too, which was an open question until it was settled + deliberately rather than by default: jmsfx#106 brought it into this reactor after + 1.5.0 shipped, so the release profile had never run with it present, and it was + held out until someone decided. An extension library is a library - a consumer + renders with it exactly as they would with jmsfx-standard - so Central is where + it belongs. --> org.sonatype.central central-publishing-maven-plugin @@ -175,7 +200,6 @@ jmsfx-viewer jmsfx-creator jmsfx-server - jmsfx-hallux @@ -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 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 - -
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.