mirror of
https://github.com/praktimarc/kst4contest.git
synced 2026-10-19 05:42:58 +02:00
Build with Gradle in two modules, and free core of JavaFX
Etappen 1 and 2 of the Compose migration (43 commits). The Maven build, the single source tree and the JPMS module-info went together. core now holds the domain and app-desktop the user interface, both built by Gradle, packaged through jpackage from Gradle, with SpotBugs and PMD wired in. JavaFX moves to the classpath, which is why the launcher is Main and not the Application subclass. The PR check, the nightly artifacts, the release and the AUR packages all build from Gradle; the macOS DMG points at its output. Verified on Linux, macOS and Windows. core then loses JavaFX entirely. It observes through kst4contest.observe -- ObservableValue, ObservableRoster, and UiDispatcher as the one boundary a worker thread crosses to touch the interface. The controller collections become rosters and the visible station list a pure derivation rather than a FilteredList/SortedList chain, which is also what fixed two focus defects only a load test had exposed: the keyboard focus stays where the operator put it, and the send field keeps it after a row click. The rule that outlives this commit: ObservableRoster is the store and the list a view is bound to is a mirror of it, never the other way round.
This commit is contained in:
360 files changed
+6304
-2422
No files matched your search
@@ -1,128 +0,0 @@
|
||||
/*
|
||||
* Derives the jpackage --add-modules list from module-info.java so that the
|
||||
* packaging scripts never drift from the module descriptor again.
|
||||
*
|
||||
* Run as a single file source program, which behaves identically on the Linux,
|
||||
* macOS and Windows runners:
|
||||
*
|
||||
* java packaging/AddModules.java print the module list
|
||||
* java packaging/AddModules.java --verify-pom fail if pom.xml drifted
|
||||
*
|
||||
* Only platform modules are emitted. Third party requires such as jlayer are
|
||||
* skipped because they are supplied as ordinary jars on the class path, and
|
||||
* automatic modules cannot be linked into a runtime image at all. Test only
|
||||
* requires such as org.junit.jupiter.api are skipped for the same reason.
|
||||
*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class AddModules {
|
||||
|
||||
private static final Path DESCRIPTOR =
|
||||
Path.of("src", "main", "java", "module-info.java");
|
||||
|
||||
private static final Path POM = Path.of("pom.xml");
|
||||
|
||||
/** Matches "requires [transitive] [static] some.module;" in any order. */
|
||||
private static final Pattern REQUIRES = Pattern.compile(
|
||||
"requires\\s+((?:transitive\\s+|static\\s+)*)([A-Za-z0-9_.]+)\\s*;");
|
||||
|
||||
private static final Pattern ADD_MODULE =
|
||||
Pattern.compile("<addmodule>\\s*([A-Za-z0-9_.]+)\\s*</addmodule>");
|
||||
|
||||
private static final Pattern BLOCK_COMMENT =
|
||||
Pattern.compile("/\\*.*?\\*/", Pattern.DOTALL);
|
||||
|
||||
private static final Pattern LINE_COMMENT = Pattern.compile("//[^\\n]*");
|
||||
|
||||
private AddModules() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
boolean verifyPom = args.length > 0 && "--verify-pom".equals(args[0]);
|
||||
|
||||
Set<String> required = platformModules(read(DESCRIPTOR));
|
||||
if (required.isEmpty()) {
|
||||
fail("No platform modules found in " + DESCRIPTOR);
|
||||
}
|
||||
|
||||
if (!verifyPom) {
|
||||
System.out.println(String.join(",", required));
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> declared = new TreeSet<>();
|
||||
Matcher matcher = ADD_MODULE.matcher(read(POM));
|
||||
while (matcher.find()) {
|
||||
declared.add(matcher.group(1));
|
||||
}
|
||||
|
||||
if (declared.equals(required)) {
|
||||
System.out.println("pom.xml <addmodules> matches module-info.java ("
|
||||
+ required.size() + " modules)");
|
||||
return;
|
||||
}
|
||||
|
||||
Set<String> missing = new TreeSet<>(required);
|
||||
missing.removeAll(declared);
|
||||
Set<String> extra = new TreeSet<>(declared);
|
||||
extra.removeAll(required);
|
||||
|
||||
System.err.println("pom.xml <addmodules> drifted from module-info.java.");
|
||||
if (!missing.isEmpty()) {
|
||||
System.err.println(" missing in pom.xml: " + String.join(", ", missing));
|
||||
}
|
||||
if (!extra.isEmpty()) {
|
||||
System.err.println(" not required by module-info.java: "
|
||||
+ String.join(", ", extra));
|
||||
}
|
||||
System.err.println(" expected: " + String.join(",", required));
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
/** Returns the platform modules required by the given descriptor, sorted. */
|
||||
static Set<String> platformModules(String source) {
|
||||
String stripped = LINE_COMMENT.matcher(
|
||||
BLOCK_COMMENT.matcher(source).replaceAll(" ")).replaceAll(" ");
|
||||
|
||||
Set<String> modules = new TreeSet<>();
|
||||
Matcher matcher = REQUIRES.matcher(stripped);
|
||||
while (matcher.find()) {
|
||||
// "requires static" is a compile time only dependency and must not
|
||||
// be linked into the shipped runtime image.
|
||||
if (matcher.group(1).contains("static")) {
|
||||
continue;
|
||||
}
|
||||
String module = matcher.group(2);
|
||||
if (isPlatformModule(module)) {
|
||||
modules.add(module);
|
||||
}
|
||||
}
|
||||
return modules;
|
||||
}
|
||||
|
||||
private static boolean isPlatformModule(String module) {
|
||||
return module.startsWith("java.")
|
||||
|| module.startsWith("jdk.")
|
||||
|| module.startsWith("javafx.");
|
||||
}
|
||||
|
||||
private static String read(Path path) throws IOException {
|
||||
if (!Files.isRegularFile(path)) {
|
||||
fail("Not found: " + path.toAbsolutePath()
|
||||
+ " (run this from the repository root)");
|
||||
}
|
||||
return Files.readString(path);
|
||||
}
|
||||
|
||||
private static void fail(String message) {
|
||||
System.err.println(message);
|
||||
System.exit(2);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ arch=('x86_64')
|
||||
url="https://github.com/praktimarc/kst4contest"
|
||||
license=('GPL-3.0-only')
|
||||
depends=('gst-plugins-base' 'gst-plugins-good')
|
||||
makedepends=('java-environment=21' 'maven' 'git')
|
||||
makedepends=('java-environment=21' 'gradle' 'git')
|
||||
provides=('kst4contest')
|
||||
conflicts=('kst4contest' 'kst4contest-bin')
|
||||
source=("kst4contest::git+https://github.com/praktimarc/kst4contest.git")
|
||||
@@ -15,8 +15,8 @@ sha256sums=('SKIP')
|
||||
|
||||
pkgver() {
|
||||
cd "${srcdir}/kst4contest"
|
||||
BASE=$(grep -m1 '<version>' pom.xml \
|
||||
| sed 's/.*<version>\(.*\)<\/version>.*/\1/' | sed 's/[-.]nightly//')
|
||||
BASE=$(grep -m1 '^version=' gradle.properties \
|
||||
| cut -d= -f2 | sed 's/[-.]nightly//')
|
||||
printf '%s.r%s.g%s' "${BASE}" "$(git rev-list --count HEAD)" "$(git rev-parse --short HEAD)"
|
||||
}
|
||||
|
||||
@@ -26,22 +26,17 @@ build() {
|
||||
export JAVA_HOME=$(find /usr/lib/jvm -maxdepth 1 -name 'java-21-*' -type d | head -n 1)
|
||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||
|
||||
mvn -B -DskipTests package dependency:copy-dependencies \
|
||||
-DincludeScope=runtime \
|
||||
-DoutputDirectory=target/dist-libs
|
||||
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
./gradlew --offline -S :app-desktop:collectRuntime
|
||||
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
ADD_MODULES="$(grep -m1 '^jpackageAddModules=' gradle.properties | cut -d= -f2)"
|
||||
jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--icon packaging/icons/kst4contest.png \
|
||||
--input target/dist-libs \
|
||||
--input app-desktop/build/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--main-class kst4contest.view.Main \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest dist
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ arch=('x86_64')
|
||||
url="https://github.com/praktimarc/kst4contest"
|
||||
license=('GPL-3.0-only')
|
||||
depends=('gst-plugins-base' 'gst-plugins-good')
|
||||
makedepends=('java-environment=21' 'maven')
|
||||
makedepends=('java-environment=21' 'gradle')
|
||||
provides=('kst4contest')
|
||||
conflicts=('kst4contest-bin' 'kst4contest-git')
|
||||
source=("${pkgname}-${pkgver}.tar.gz::https://github.com/praktimarc/kst4contest/archive/refs/tags/v${pkgver}.tar.gz")
|
||||
@@ -19,17 +19,14 @@ build() {
|
||||
export JAVA_HOME=$(find /usr/lib/jvm -maxdepth 1 -name 'java-21-*' -type d | head -n 1)
|
||||
export PATH="${JAVA_HOME}/bin:${PATH}"
|
||||
|
||||
mvn -B -DskipTests package dependency:copy-dependencies \
|
||||
-DincludeScope=runtime \
|
||||
-DoutputDirectory=target/dist-libs
|
||||
|
||||
cp "$(ls -t target/praktiKST-*.jar | head -n 1)" target/dist-libs/app.jar
|
||||
./gradlew --offline -S :app-desktop:collectRuntime
|
||||
|
||||
mkdir -p dist
|
||||
# This PKGBUILD builds from a released source tarball, which may predate
|
||||
# packaging/AddModules.java. Older tarballs carry the same list in pom.xml,
|
||||
# which the build keeps in sync with module-info.java from v1.42.0 onwards.
|
||||
if [ -f packaging/AddModules.java ]; then
|
||||
# From the Gradle build onwards the list lives in gradle.properties. Older
|
||||
# tarballs still carry it in pom.xml or derive it via packaging/AddModules.java.
|
||||
if [ -f gradle.properties ]; then
|
||||
ADD_MODULES="$(grep -m1 '^jpackageAddModules=' gradle.properties | cut -d= -f2)"
|
||||
elif [ -f packaging/AddModules.java ]; then
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
else
|
||||
ADD_MODULES="$(sed -n 's:.*<addmodule>\(.*\)</addmodule>.*:\1:p' pom.xml | paste -sd,)"
|
||||
@@ -45,10 +42,9 @@ build() {
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
"${ICON_ARGS[@]}" \
|
||||
--input target/dist-libs \
|
||||
--input app-desktop/build/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--main-class kst4contest.view.Main \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--dest dist
|
||||
}
|
||||
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerate every raster icon from kst4contest.svg.
|
||||
#
|
||||
# The rasters are committed, so nothing in CI runs this — it exists so the
|
||||
# icons can be rebuilt reproducibly whenever the SVG changes. Requires
|
||||
# rsvg-convert (librsvg) and Python Pillow.
|
||||
#
|
||||
# packaging/icons/generate-icons.sh
|
||||
#
|
||||
# Outputs, all regenerated in place and mirrored into src/main/resources/icons
|
||||
# so the in-app window icon matches what the launcher shows:
|
||||
# kst4contest.png 256px, the size jpackage --icon feeds to every Linux
|
||||
# package and the size hicolor/256x256/apps expects
|
||||
# kst4contest.ico 16/24/32/48/64 as BMP, 128/256 as PNG (Windows convention)
|
||||
# kst4contest.icns ic07-ic14, the modern macOS set up to 1024px
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ICON_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${ICON_DIR}/../.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
for tool in rsvg-convert python3; do
|
||||
command -v "$tool" >/dev/null || { echo "Missing: $tool" >&2; exit 1; }
|
||||
done
|
||||
python3 -c "import PIL" 2>/dev/null || { echo "Missing: Python Pillow" >&2; exit 1; }
|
||||
|
||||
# The SVG asks for 'Space Grotesk' and falls back to Helvetica Neue, then Arial.
|
||||
# Whichever is installed is baked into the raster, so a machine with a different
|
||||
# font set produces visibly different icons. Warn rather than fail — Arial is
|
||||
# the author's own third choice and renders acceptably.
|
||||
if ! fc-list 2>/dev/null | grep -qi "space grotesk"; then
|
||||
echo "WARNING: 'Space Grotesk' is not installed; the KST wordmark will be"
|
||||
echo " rendered with a fallback font (checked: $(fc-match 'Space Grotesk' 2>/dev/null | head -1))."
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import struct, subprocess, os
|
||||
from PIL import Image
|
||||
|
||||
SRC, OUT = "packaging/icons/kst4contest.svg", "packaging/icons"
|
||||
_cache = {}
|
||||
|
||||
def render(size):
|
||||
if size not in _cache:
|
||||
p = f"/tmp/kst4contest-icon-{size}.png"
|
||||
subprocess.run(["rsvg-convert", "-w", str(size), "-h", str(size), SRC, "-o", p], check=True)
|
||||
_cache[size] = p
|
||||
return _cache[size]
|
||||
|
||||
# --- Linux -------------------------------------------------------------
|
||||
Image.open(render(256)).convert("RGBA").save(f"{OUT}/kst4contest.png", optimize=True)
|
||||
|
||||
# --- Windows -----------------------------------------------------------
|
||||
def bmp_entry(im):
|
||||
w, h = im.size
|
||||
px = im.load()
|
||||
hdr = struct.pack("<IiiHHIIiiII", 40, w, h * 2, 1, 32, 0, 0, 0, 0, 0, 0)
|
||||
rows = [bytes(bytearray().join(
|
||||
bytes((px[x, y][2], px[x, y][1], px[x, y][0], px[x, y][3])) for x in range(w)
|
||||
)) for y in range(h - 1, -1, -1)]
|
||||
mask = bytes(((w + 31) // 32) * 4 * h)
|
||||
return hdr + b"".join(rows) + mask
|
||||
|
||||
entries = []
|
||||
for size in [16, 24, 32, 48, 64]:
|
||||
entries.append((size, bmp_entry(Image.open(render(size)).convert("RGBA"))))
|
||||
for size in [128, 256]:
|
||||
entries.append((size, open(render(size), "rb").read()))
|
||||
|
||||
offset = 6 + 16 * len(entries)
|
||||
dir_, blobs = b"", b""
|
||||
for size, data in entries:
|
||||
dim = 0 if size == 256 else size
|
||||
dir_ += struct.pack("<BBBBHHII", dim, dim, 0, 0, 1, 32, len(data), offset)
|
||||
blobs += data
|
||||
offset += len(data)
|
||||
open(f"{OUT}/kst4contest.ico", "wb").write(struct.pack("<HHH", 0, 1, len(entries)) + dir_ + blobs)
|
||||
|
||||
# --- macOS -------------------------------------------------------------
|
||||
chunks = b""
|
||||
for t, size in [("ic11", 32), ("ic12", 64), ("ic07", 128), ("ic13", 256),
|
||||
("ic08", 256), ("ic14", 512), ("ic09", 512), ("ic10", 1024)]:
|
||||
data = open(render(size), "rb").read()
|
||||
chunks += t.encode("ascii") + struct.pack(">I", len(data) + 8) + data
|
||||
open(f"{OUT}/kst4contest.icns", "wb").write(b"icns" + struct.pack(">I", len(chunks) + 8) + chunks)
|
||||
PY
|
||||
|
||||
cp packaging/icons/kst4contest.{png,ico,icns} src/main/resources/icons/
|
||||
echo "Regenerated packaging/icons and src/main/resources/icons from kst4contest.svg"
|
||||
@@ -50,11 +50,8 @@ elif [ -n "${NOTARY_PROFILE:-}" ]; then
|
||||
fi
|
||||
|
||||
echo "==> Building JAR and collecting runtime dependencies"
|
||||
chmod +x mvnw
|
||||
./mvnw -B -DskipTests package \
|
||||
dependency:copy-dependencies -DincludeScope=runtime -DoutputDirectory=target/dist-libs
|
||||
JAR="$(ls -t target/praktiKST-*.jar | head -n 1)"
|
||||
cp "$JAR" target/dist-libs/app.jar
|
||||
chmod +x gradlew
|
||||
./gradlew -S :app-desktop:collectRuntime
|
||||
|
||||
# jpackage only accepts a numeric major[.minor[.patch]] as the macOS bundle
|
||||
# version, so a Maven qualifier like "-nightly" has to be trimmed off.
|
||||
@@ -67,17 +64,16 @@ echo "==> Version: $POM_VERSION -> bundle version $APP_VERSION"
|
||||
echo "==> Step 1/4: jpackage app-image (unsigned)"
|
||||
rm -rf dist
|
||||
mkdir -p dist
|
||||
ADD_MODULES="$(java packaging/AddModules.java)"
|
||||
ADD_MODULES="$(grep -m1 '^jpackageAddModules=' gradle.properties | cut -d= -f2)"
|
||||
|
||||
MACOSX_DEPLOYMENT_TARGET="13.0" jpackage \
|
||||
--type app-image \
|
||||
--name KST4Contest \
|
||||
--app-version "$APP_VERSION" \
|
||||
--icon packaging/icons/kst4contest.icns \
|
||||
--input target/dist-libs \
|
||||
--input app-desktop/build/dist-libs \
|
||||
--main-jar app.jar \
|
||||
--main-class kst4contest.view.Kst4ContestApplication \
|
||||
--module-path target/dist-libs \
|
||||
--main-class kst4contest.view.Main \
|
||||
--add-modules "$ADD_MODULES" \
|
||||
--mac-package-identifier "$BUNDLE_ID" \
|
||||
--mac-package-name KST4Contest \
|
||||
|
||||
Reference in new issue
Block a user