Files
chris.giteaandClaude Opus 5.5 ded9907bcb perf(ui): weave @FxThread at build time, keep the agent as a safety net
The load-time Byte Buddy agent cost ~420 ms on every launch before the
splash screen could show. FxThreadPlugin now weaves @FxThread at build time
(byte-buddy-maven-plugin, process-classes) with the same inlined advice and
method matcher as the agent, and marks each woven class @FxThreadWoven.

FxThreadAgent.install() only attaches when the application's classes run
from a directory (an IDE run), where an incremental compiler may have
overwritten woven classes, and then weaves only the classes without the
marker, so nothing is woven twice; -Dpholio.fxthread.agent=always|never
overrides the choice. A Maven-built jar starts with no agent: the splash's
first paint moves from ~0.83 s to ~0.47 s.

The measurements behind this are in .claude/plans/startup-time-analysis.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
2026-09-30 18:10:48 -04:00

973 lines
54 KiB
XML

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.1</version>
<relativePath/>
</parent>
<groupId>org.icroco.pholio</groupId>
<artifactId>pholio</artifactId>
<version>2026.9.0</version>
<name>Pholio</name>
<description>High-volume desktop photo and media library manager</description>
<properties>
<archunit.version>1.5.0</archunit.version>
<!--
Extra themes for AtlantaFx, from DLSC. Pinned to a release present in the local
repository so an offline build keeps working; Maven Central carries newer ones.
-->
<atlantafx-themes.version>1.9.0</atlantafx-themes.version>
<atlantafx.version>2.1.0</atlantafx.version>
<!--
Bytecode instrumentation for @FxThread (org.icroco.pholio.ui.common): woven at build time
by FxThreadPlugin (byte-buddy-maven-plugin below), with FxThreadAgent as a load-time safety
net for classes run from a directory, which an IDE may have recompiled unwoven — see
FxThread's own javadoc.
-->
<byte-buddy.version>1.18.12</byte-buddy.version>
<commons-codec.version>1.22.1</commons-codec.version>
<commons-imaging.version>1.0.0-alpha6</commons-imaging.version>
<!--
Scene-graph inspector, wired in by DevTools and dormant unless pholio.dev-tools.enabled is
set. Built for Java 21 / JavaFX 23; Pholio runs it on 26 in classpath mode, so it relies on
binary backward compatibility. DevTools catches a linkage failure rather than letting one
reach the user.
-->
<devtoolsfx.version>1.0.1</devtoolsfx.version>
<errorprone.version>2.50.0</errorprone.version>
<exec-maven-plugin.version>3.6.4</exec-maven-plugin.version>
<gatherers4j.version>0.14.0</gatherers4j.version>
<gemsfx.version>4.4.5</gemsfx.version>
<ikonli.version>12.4.0</ikonli.version>
<jSystemThemeDetector.version>3.9.1</jSystemThemeDetector.version>
<!--
JavaFX 26 is compiled for Java 24 (class file major 68), so a JDK >= 24 is required at runtime
regardless. The local toolchain is Zulu 26.0.1.fx; drop this to 25 if a CI runner or packaging
target only has the LTS available.
-->
<java.version>26</java.version>
<!--
Pinned to 26.0.1 to match the JavaFX modules bundled in the Zulu FX JDK. In classpath mode
the JDK's javafx.* modules are resolved into the boot layer and shadow these jars, so
keeping the versions identical avoids a split-version mismatch. The jars are still required
for packaging on JDKs that do not bundle JavaFX.
-->
<javafx.version>27</javafx.version>
<!-- ========================== Packaging =========================== -->
<jdeploy-maven-plugin.version>1.0.8</jdeploy-maven-plugin.version>
<!--
The jDeploy CLI release that builds the native bundles. Kept in step with the version pinned in
.github/workflows/jdeploy.yml on purpose: the CLI that generates package.json fields locally and the
one that assembles the installers in CI must agree, or a locally-synced descriptor can carry keys the
release build does not understand.
-->
<jdeploy.cli.version>6.1.5</jdeploy.cli.version>
<jilt.version>1.9.2</jilt.version>
<!--
Micro-benchmarks live under src/test, test-scoped, run explicitly — see
FileHasherBenchmark's javadoc for how — never as part of `mvn test`.
-->
<jmh.version>1.37</jmh.version>
<lombok.version>1.18.48</lombok.version>
<!-- caffeine, h2, flyway and jackson versions come from the Spring Boot 4.1.0 BOM. -->
<main.class>org.icroco.pholio.PholioApplication</main.class>
<!-- Spring Boot 4.1's BOM does not manage MapStruct, so it is pinned here directly. -->
<mapstruct.version>1.7.0.Beta2</mapstruct.version>
<maven-compiler-plugin.version>3.15.0</maven-compiler-plugin.version>
<maven-enforcer-plugin.version>3.6.3</maven-enforcer-plugin.version>
<metadata-extractor.version>2.21.0</metadata-extractor.version>
<nullaway.version>0.14.0</nullaway.version>
<!--
Local face/animal recognition (YuNet detection, SFace embedding, YOLOX-Nano animal
detection) runs on Microsoft's own ONNX Runtime Java API directly, not through Deep
Java Library: every model here needs fully custom pre/post-processing (letterboxing,
anchor decoding, NMS, 5-point face alignment), so DJL's Translator/ZooModel machinery
would add an abstraction layer with nothing left for it to actually do. This jar bundles
native libraries for every major desktop platform in one artifact — no per-OS classifier,
no runtime download.
-->
<onnxruntime.version>1.21.1</onnxruntime.version>
<picocli.version>4.7.7</picocli.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<sortpom-maven-plugin.version>4.0.0</sortpom-maven-plugin.version>
<threeten-extra.version>1.10.0</threeten-extra.version>
<twelvemonkeys.version>3.15.2</twelvemonkeys.version>
<versions-maven-plugin.version>2.22.0</versions-maven-plugin.version>
<!-- BSD-3-Clause. The real RDF/XMP packet builder/parser (mwg-rs face regions, dc:subject) — Commons
Imaging has no XMP support of its own, only EXIF/TIFF. -->
<xmpcore.version>6.1.11</xmpcore.version>
</properties>
<dependencies>
<?SORTPOM IGNORE?>
<!-- ============================ Spring ============================ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-jdbc</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<!--
spring-boot-starter-validation is deliberately absent. Hibernate Validator probes the classpath
for JavaFX at startup to register its value extractors, which loads ~25 javafx.base interfaces
into a headless CLI run for no benefit. Nothing uses Bean Validation yet; if
@ConfigurationProperties constraints are wanted later, add it back knowing this side effect.
Note the qualifier "for no benefit": the CLI does now load javafx.base, because PreferenceItem
holds its value in a javafx.beans ObjectProperty so the settings view, the theme manager and the
locale service can all react to a change without an event bus in between. That is a paid-for
trade, not an accident. javafx.base is pure Java and needs no graphics stack; what must stay out
of the headless path is javafx.graphics and above, which is what ArchitectureRulesTest's
infrastructureTouchesOnlyJavaFxProperties and cliIsFreeOfJavaFx enforce.
-->
<!-- ========================= Persistence ========================== -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
<!--
As of Boot 4.1, Flyway's autoconfiguration (used by the test datasource, where routing is off) no
longer ships inside spring-boot-autoconfigure — it is this separate module. The production path,
one migration per library pool, calls org.flywaydb.core.Flyway directly and does not need it, but
the plain single-DataSource path (tests, and any future run with routing disabled) does.
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-flyway</artifactId>
</dependency>
<!-- ===================== Preferences (YAML) ======================= -->
<!-- Jackson 3 (tools.jackson.*) — the default in Spring Boot 4.x. -->
<dependency>
<groupId>tools.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>
<!-- ============================ JavaFX ============================ -->
<!-- Platform classifier is resolved automatically by the openjfx POMs. -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-graphics</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- javafx-swing provides SwingFXUtils, needed to bridge ImageIO <-> FX Image. -->
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-swing</artifactId>
<version>${javafx.version}</version>
</dependency>
<!-- ======================== UI toolkit =========================== -->
<dependency>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-base</artifactId>
<version>${atlantafx.version}</version>
</dependency>
<dependency>
<!-- Twenty-five more AtlantaFx themes, all implementing atlantafx.base.theme.Theme. -->
<groupId>com.dlsc.atlantafx</groupId>
<artifactId>themes</artifactId>
<version>${atlantafx-themes.version}</version>
<exclusions>
<!--
This artifact is built against atlantafx-base 2.0.1. The direct declaration above already
wins by proximity, but excluding it states the intent and makes a future version bump fail
loudly rather than silently downgrade the base. AppThemeTest loads all 32 themes, which is
what proves the cross-version binary compatibility this relies on.
-->
<exclusion>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-base</artifactId>
</exclusion>
<!-- SASS sources, used to build those themes and of no use at runtime. -->
<exclusion>
<groupId>io.github.mkpaz</groupId>
<artifactId>atlantafx-styles</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.github.mkpaz</groupId>
<artifactId>devtoolsfx-gui</artifactId>
<version>${devtoolsfx.version}</version>
<!--
devtoolsfx-connector declares its own test libraries at compile scope, so JUnit 6 and AssertJ
(~2.8 MB) would otherwise ship in the application. None of its classes reference either.
-->
<exclusions>
<exclusion>
<groupId>org.junit.jupiter</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>org.junit.platform</groupId>
<artifactId>*</artifactId>
</exclusion>
<exclusion>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-javafx</artifactId>
<version>${ikonli.version}</version>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-feather-pack</artifactId>
<version>${ikonli.version}</version>
</dependency>
<dependency>
<!-- Material2AL: the home/chevron icons on StatusBar's breadcrumbs. -->
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-material2-pack</artifactId>
<version>${ikonli.version}</version>
</dependency>
<dependency>
<groupId>org.kordamp.ikonli</groupId>
<artifactId>ikonli-materialdesign2-pack</artifactId>
<version>${ikonli.version}</version>
</dependency>
<dependency>
<groupId>com.github.dansoftowner</groupId>
<artifactId>jSystemThemeDetector</artifactId>
<version>${jSystemThemeDetector.version}</version>
</dependency>
<dependency>
<groupId>com.ginsberg</groupId>
<artifactId>gatherers4j</artifactId>
<version>${gatherers4j.version}</version>
</dependency>
<!-- ==================== Imaging & metadata ======================= -->
<dependency>
<groupId>com.drewnoakes</groupId>
<artifactId>metadata-extractor</artifactId>
<version>${metadata-extractor.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-imaging</artifactId>
<version>${commons-imaging.version}</version>
</dependency>
<dependency>
<groupId>com.adobe.xmp</groupId>
<artifactId>xmpcore</artifactId>
<version>${xmpcore.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-webp</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-tiff</artifactId>
<version>${twelvemonkeys.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons-codec.version}</version>
</dependency>
<!-- ==================== Local ML (recognition) ==================== -->
<dependency>
<groupId>com.microsoft.onnxruntime</groupId>
<artifactId>onnxruntime</artifactId>
<version>${onnxruntime.version}</version>
</dependency>
<!-- =========================== Caching =========================== -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!--
Brings spring-context-support (CaffeineCacheManager) and CacheAutoConfiguration, which builds
that manager from spring.cache.* in application.yaml once spring.cache.type=caffeine is set —
see CacheConfig for why nothing here hand-wires a CacheManager bean the way PersistenceConfiguration
and ExecutorConfig do for the datasource and thread pools.
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- ============================= CLI ============================= -->
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>${picocli.version}</version>
</dependency>
<!-- ======================= Code generation ======================= -->
<!--
Version managed by the Spring Boot 4.1.0 BOM (1.18.46).
`provided` keeps Lombok off the runtime classpath while leaving it on both the main and test compile
paths. It does not, on its own, keep it out of the packaged application: spring-boot:repackage bundles
provided-scope artifacts too, so the jar needs a matching <excludes> — see spring-boot-maven-plugin
below. The annotations are erased at compile time and the processor itself is supplied through
maven-compiler-plugin's annotationProcessorPaths, so nothing here is needed at runtime.
-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<version>${lombok.version}</version>
</dependency>
<!-- The @Builder annotation itself, source-retention only — the processor is wired below, next to Lombok's. -->
<dependency>
<groupId>cc.jilt</groupId>
<artifactId>jilt</artifactId>
<version>${jilt.version}</version>
<scope>provided</scope>
</dependency>
<!-- Maps entities to domain records and back; the processor is wired below, next to Lombok's. -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.threeten</groupId>
<artifactId>threeten-extra</artifactId>
<version>${threeten-extra.version}</version>
</dependency>
<dependency>
<groupId>com.dlsc.gemsfx</groupId>
<artifactId>gemsfx</artifactId>
<version>${gemsfx.version}</version>
</dependency>
<!-- =================== Bytecode instrumentation =================== -->
<!--
Compile scope, not provided: FxThreadInterceptor's Advice annotations are compiled into its method
metadata, and FxThreadAgent needs both artifacts at actual runtime to self-attach and weave —
this is not a build-time-only tool.
-->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${byte-buddy.version}</version>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
<version>${byte-buddy.version}</version>
</dependency>
<!-- ============================ Tests =========================== -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>${archunit.version}</version>
<scope>test</scope>
</dependency>
<!--
Micro-benchmarks (FileHasherBenchmark). jmh-generator-annprocess is also wired as an
annotation processor below — without that path, @Benchmark methods compile but JMH's Runner
finds nothing at run time, since the generated *_jmhTest/*_jmhType classes it actually invokes
never get produced.
-->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>test</scope>
</dependency>
<?SORTPOM RESUME?>
</dependencies>
<build>
<!--
A version-free jar name. jDeploy points at the executable jar by path from package.json, so a name
carrying the version would go stale on every release and have to be re-synced before the descriptor
was usable. The installed Maven artifact keeps its normal versioned coordinates either way.
-->
<finalName>pholio</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!--
The repackaged jar is what jDeploy ships. Its manifest carries Main-Class=JarLauncher and
Start-Class=${main.class}, which is how the native launcher finds the entry point: jDeploy has
no main-class setting of its own, it runs the jar. So this mainClass is the single declaration
of the entry point for the packaged application as well as for `java -jar`.
-->
<configuration>
<mainClass>${main.class}</mainClass>
<!--
Lombok is compile-time only, but repackage bundles provided-scope artifacts by default, so
without this the installer ships 2 MB of annotation processor that can never run.
-->
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
<!-- Same reason as Lombok: provided scope, source-retention annotations only (1.6 MB). -->
<exclude>
<groupId>cc.jilt</groupId>
<artifactId>jilt</artifactId>
</exclude>
</excludes>
<!--
OpenJFX jars are not shipped either (~9 MB). Every runtime this jar is meant for already
carries JavaFX as modules, which shadow class-path jars anyway: the JDK 26 FX the README
requires, the jlink'ed runtime of the jpackage installers, jDeploy's FX JRE. They were
also only ever the build machine's own natives (mac-aarch64 when built on a Mac), so they
could never have served another OS. Tests and IDE runs still get them from the Maven
classpath; only the repackaged jar drops them.
-->
<excludeGroupIds>org.openjfx</excludeGroupIds>
<!--
FxThreadAgent.install() self-attaches Byte Buddy at startup (see that class's own
javadoc for why this is a runtime agent rather than build-time weaving). Left nested
the way every other dependency is, `java -jar` on this repackaged jar throws
ClassNotFoundException: net.bytebuddy.dynamic.Nexus the moment any org.icroco.pholio.*
class loads after the agent installs: Byte Buddy's self-attach writes its own helper
jars to real temp files when it can locate itself as one, and a nested jar-in-a-jar
entry (BOOT-INF/lib/byte-buddy*.jar) is not one. Only java -jar is affected — the
plain classpath the IDE and the test JVM both run from already has these as real
files, which is exactly why this never showed up until jdeploy's packaged installer
(built from this same repackaged jar) was actually run. requiresUnpack is Spring
Boot's documented fix for precisely this class of problem: it flags these two entries
so the loader extracts them to a real temp directory before Byte Buddy ever looks for
them, at the one-time cost of that extraction on first launch.
-->
<requiresUnpack>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-agent</artifactId>
</dependency>
</requiresUnpack>
</configuration>
<executions>
<!-- META-INF/build-info.properties: build.version is shown on the splash screen (SplashPreloader). -->
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>${maven-enforcer-plugin.version}</version>
<executions>
<execution>
<id>enforce-maven</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireMavenVersion>
<version>3.9</version>
</requireMavenVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<!--
Lombok has to be named here, not just declared as a dependency. Since JDK 23 javac no
longer discovers annotation processors on the compile classpath, so a bare dependency
silently does nothing and every @Getter class fails with "variable not initialized in the
default constructor". Declaring the path also keeps the processor set explicit rather than
whatever happens to be on the classpath.
lombok.version comes from the Spring Boot BOM, so the processor and the dependency can
never drift apart.
-->
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!--
Lombok and MapStruct both claim the same annotation-processing round; without this
binding artifact MapStruct generates mappers against the class as javac sees it before
Lombok's getters/setters/builders exist, silently emitting empty mapping methods.
-->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</path>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.version}</version>
</path>
<path>
<groupId>com.uber.nullaway</groupId>
<artifactId>nullaway</artifactId>
<version>${nullaway.version}</version>
</path>
<!-- Generates FileHasherBenchmark's *_jmhType_*/*_jmhTest runner classes. -->
<path>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
</path>
<path>
<groupId>cc.jilt</groupId>
<artifactId>jilt</artifactId>
<version>${jilt.version}</version>
<!-- Check Maven Central for the latest version -->
</path>
</annotationProcessorPaths>
<compilerArgs>
<!--
this-escape is disabled deliberately. Every JavaFX view here extends a layout
container and configures itself in its constructor, which the check flags by
design; the pattern is idiomatic JavaFX and the alternative (two-phase init on
every view) trades a theoretical warning for real lifecycle bugs. Deprecation
warnings stay on and are suppressed case by case.
-->
<arg>-Xlint:all,-serial,-processing,-this-escape</arg>
<arg>-XDcompilePolicy=simple</arg>
<arg>-XDshould-stop.ifError=FLOW</arg>
<arg>-Xplugin:ErrorProne
-Xep:NullAway:ERROR
-XepOpt:NullAway:AnnotatedPackages=org.icroco.pholio
-XepOpt:NullAway:CustomNullableAnnotations=org.jspecify.annotations.Nullable
-XepOpt:NullAway:CheckOptionalEmptiness=true
-XepOpt:NullAway:AcknowledgeRestrictiveAnnotations=true
-XepExcludedPaths:.*/generated-(test-)?sources/.*</arg>
<!--
The pattern above now covers both target/generated-sources (Lombok/MapStruct,
main) and target/generated-test-sources (JMH's *_jmhType_*/*_jmhTest runners
for FileHasherBenchmark, test) — JMH's generated code lands in the same
package as the benchmark class it's generated from, so without the latter it
would otherwise be subject to the same NullAway:ERROR as hand-written code.
-->
<!-- Required for Java 16+ to allow ErrorProne to access JDK internals -->
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>
<arg>-J--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>
<arg>-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>
</compilerArgs>
</configuration>
</plugin>
<!-- <plugin>-->
<!-- <groupId>org.apache.maven.plugins</groupId>-->
<!-- <artifactId>maven-compiler-plugin</artifactId>-->
<!-- <version>3.13.0</version>-->
<!-- <configuration>-->
<!-- <source>17</source>-->
<!-- <target>17</target>-->
<!-- <compilerArgs>-->
<!-- <arg>-XDcompilePolicy=simple</arg>-->
<!-- <arg>-Xplugin:ErrorProne-->
<!-- -Xep:NullAway:ERROR-->
<!-- -XepOpt:NullAway:AnnotatedPackages=com.yourpackage-->
<!-- -XepOpt:NullAway:CustomNullableAnnotations=org.jspecify.annotations.Nullable-->
<!-- -XepOpt:NullAway:CheckOptionalEmptiness=true-->
<!-- </arg>-->
<!-- &lt;!&ndash; Required for Java 16+ to allow ErrorProne to access JDK internals &ndash;&gt;-->
<!-- <arg>-J&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED</arg>-->
<!-- <arg>-J&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED</arg>-->
<!-- <arg>-J&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED</arg>-->
<!-- <arg>-J&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED</arg>-->
<!-- <arg>-J&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED</arg>-->
<!-- <arg>-J&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED</arg>-->
<!-- <arg>-J&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED</arg>-->
<!-- <arg>-J&#45;&#45;add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED</arg>-->
<!-- <arg>-J&#45;&#45;add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED</arg>-->
<!-- <arg>-J&#45;&#45;add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED</arg>-->
<!-- </compilerArgs>-->
<!-- &lt;!&ndash; Allows -J JVM arguments to be passed directly to javac &ndash;&gt;-->
<!-- <fork>true</fork>-->
<!-- <annotationProcessorPaths>-->
<!-- <path>-->
<!-- <groupId>org.projectlombok</groupId>-->
<!-- <artifactId>lombok</artifactId>-->
<!-- <version>1.18.30</version>-->
<!-- </path>-->
<!-- <path>-->
<!-- <groupId>com.google.errorprone</groupId>-->
<!-- <artifactId>error_prone_core</artifactId>-->
<!-- <version>2.26.0</version>-->
<!-- </path>-->
<!-- <path>-->
<!-- <groupId>com.uber.nullaway</groupId>-->
<!-- <artifactId>nullaway</artifactId>-->
<!-- <version>0.10.26</version>-->
<!-- </path>-->
<!-- </annotationProcessorPaths>-->
<!-- </configuration>-->
<!-- </plugin>-->
<plugin>
<!--
Weaves @FxThread at build time (org.icroco.pholio.ui.common.FxThreadPlugin) into target/classes,
at process-classes: after maven-compiler-plugin (Lombok, ErrorProne and NullAway are done), before
tests and packaging, so the tests and the repackaged jar both run woven classes. A jar-launched
application then needs no load-time agent (~420 ms off the time to splash screen); FxThreadAgent
only steps in for classes run from a directory, which an IDE may have recompiled unwoven.
-->
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy-maven-plugin</artifactId>
<version>${byte-buddy.version}</version>
<configuration>
<transformations>
<transformation>
<plugin>org.icroco.pholio.ui.common.FxThreadPlugin</plugin>
</transformation>
</transformations>
</configuration>
<executions>
<execution>
<goals>
<goal>transform</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<!--
Redirects config, data and cache under target/. PreferenceService writes on every
property change and LibraryService changes two of them at context startup, so
without this a test run would rewrite the developer's own preferences.yaml and
create libraries in their real profile.
-->
<pholio.home>${project.build.directory}/pholio-home</pholio.home>
<!--
HeaderBar is a JavaFX preview API. GuiBootstrap sets this before launching; the
tests that build the header bar need it for the same reason.
-->
<javafx.enablePreview>true</javafx.enablePreview>
</systemPropertyVariables>
</configuration>
</plugin>
<plugin>
<groupId>com.github.ekryd.sortpom</groupId>
<artifactId>sortpom-maven-plugin</artifactId>
<version>${sortpom-maven-plugin.version}</version>
<configuration>
<sortDependencyManagement>false</sortDependencyManagement>
<sortProperties>true</sortProperties>
<sortDependencies>groupId,artifactId</sortDependencies>
<sortDependencyExclusions>groupId,artifactId</sortDependencyExclusions>
<createBackupFile>false</createBackupFile>
<expandEmptyElements>false</expandEmptyElements>
<!--
nrOfIndentSpace, not indentSize: the latter is not a parameter of this plugin, so it was
silently ignored and every `mvn package` reflowed the file to the two-space default,
undoing 071d29b. Keeping the schemaLocation attribute on its own line is part of the same
formatting.
-->
<nrOfIndentSpace>4</nrOfIndentSpace>
<indentAttribute>schemaLocation</indentAttribute>
</configuration>
<executions>
<execution>
<goals>
<goal>sort</goal>
</goals>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>${versions-maven-plugin.version}</version>
<configuration>
<ruleSet>
<!-- zero or more elements -->
<ignoreVersions>
<ignoreVersion>
<type>regex</type>
<version>.+-(alpha|beta|jdk5).+</version>
</ignoreVersion>
</ignoreVersions>
</ruleSet>
</configuration>
</plugin>
<!--
No execution bound to any phase: benchmarks never run as part of `mvn test` or `mvn
package`, only when explicitly invoked. classpathScope=test puts src/test/java (where
FileHasherBenchmark lives) on the classpath without needing a separate benchmarks module.
exec:java runs org.openjdk.jmh.Main in-process by reflection, so it never sets the real
java.class.path system property JMH's own fork launcher reads to relaunch the child JVM a
proper benchmark run forks per iteration — the fork then fails with
"ClassNotFoundException: org.openjdk.jmh.runner.ForkedMain". Fine for a quick, non-forked
sanity check (pass -f 0, and see the "Non-forked runs" warning JMH itself prints):
mvn -o test-compile exec:java -Dexec.args="FileHasherBenchmark -f 0"
For a real, forked measurement, build a genuine classpath and invoke java directly instead:
mvn -o dependency:build-classpath -Dmdep.outputFile=/tmp/pholio-bench-cp.txt -Dmdep.includeScope=test
java -cp "target/classes:target/test-classes:$(cat /tmp/pholio-bench-cp.txt)" \
org.openjdk.jmh.Main FileHasherBenchmark
-->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${exec-maven-plugin.version}</version>
<configuration>
<classpathScope>test</classpathScope>
<mainClass>org.openjdk.jmh.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<!--
Keeps package.json in step with the POM before the native bundles are built.
Behind a profile rather than in the default build because the plugin does its work by shelling out to
`npm pkg set` for every field, so it makes Node a hard requirement of the phase it runs in. A plain
`mvn package` must stay buildable on a machine that has never seen npm; the release workflow activates
this profile, and so should anyone about to run the jDeploy CLI by hand.
What it synchronises: version, name, description and jdeploy.title from the POM, jdeploy.jar from the
build output, jdeploy.javaVersion from ${java.version}, and jdeploy.javafx from the presence of the
org.openjfx dependencies. Everything else in package.json is hand-maintained and left untouched.
-->
<profile>
<id>jdeploy</id>
<build>
<plugins>
<plugin>
<groupId>ca.weblite</groupId>
<artifactId>jdeploy-maven-plugin</artifactId>
<version>${jdeploy-maven-plugin.version}</version>
<configuration>
<jdeployVersion>${jdeploy.cli.version}</jdeployVersion>
</configuration>
<executions>
<execution>
<id>sync-package-json</id>
<goals>
<goal>sync-package-json</goal>
</goals>
<!--
Declared after spring-boot-maven-plugin's repackage, which is also bound to
`package`: same-phase executions run in declaration order, and profile plugins
come last, so the jar the descriptor points at already exists.
-->
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<!--
Cuts a release: ./mvnw -Prelease validate
Six steps, bound to `validate` in declaration order, the non-Maven ones done by
tools/release/Release.java (a single-file program, JDK only):
1. bump - version becomes YYYY.M.N: same month as the current version -> N+1, otherwise
build 0 of the current month. Refuses uncommitted changes unless
-Drelease.allowDirty=true, since the tag must describe the source that was built.
2. build - a second, fresh Maven runs `verify`. Maven reads the POM version once, at startup,
so this run - not the one executing the profile - is the one that sees the new
version (manifest Implementation-Version, `pholio -V`).
3. notes - distrib/release_note/release-note-<version>.md: the Conventional Commits since the
previous v<version> tag (last 30 commits for the first release), grouped by type
then scope, duplicate first lines merged.
4. publish - GitHub release v<version> in ${release.github.repo}, described by that note, with
pholio-<version>.jar, its .sha256 and the note as assets, through the gh CLI (must
be logged in). Only the tag and the assets go there: that repository's main branch
is the orphan distrib/ worktree, never source.
5. tag - commits pom.xml alone and tags it v<version>, locally only - nothing is pushed.
6. distrib - commits the release note alone in distrib/ and pushes it to ${release.github.repo}.
A failing step stops the chain; the POM is then left on the new version and the next run simply
skips that number.
-->
<profile>
<id>release</id>
<properties>
<release.allowDirty>false</release.allowDirty>
<release.distrib.dir>${project.basedir}/distrib</release.distrib.dir>
<release.github.repo>Imag-In/Pholio</release.github.repo>
<release.script>${project.basedir}/tools/release/Release.java</release.script>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${exec-maven-plugin.version}</version>
<executions>
<execution>
<id>release-bump</id>
<goals>
<goal>exec</goal>
</goals>
<phase>validate</phase>
<configuration>
<executable>${java.home}/bin/java</executable>
<arguments>
<argument>${release.script}</argument>
<argument>bump</argument>
<argument>${project.basedir}/pom.xml</argument>
<argument>${release.allowDirty}</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>release-build</id>
<goals>
<goal>exec</goal>
</goals>
<phase>validate</phase>
<configuration>
<executable>${project.basedir}/mvnw</executable>
<workingDirectory>${project.basedir}</workingDirectory>
<arguments>
<argument>-B</argument>
<argument>verify</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>release-notes</id>
<goals>
<goal>exec</goal>
</goals>
<phase>validate</phase>
<configuration>
<executable>${java.home}/bin/java</executable>
<arguments>
<argument>${release.script}</argument>
<argument>notes</argument>
<argument>${project.basedir}/pom.xml</argument>
<argument>${release.distrib.dir}/release_note</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>release-publish</id>
<goals>
<goal>exec</goal>
</goals>
<phase>validate</phase>
<configuration>
<executable>${java.home}/bin/java</executable>
<arguments>
<argument>${release.script}</argument>
<argument>publish</argument>
<argument>${project.basedir}/pom.xml</argument>
<argument>${project.build.directory}/${project.build.finalName}.jar</argument>
<argument>${release.github.repo}</argument>
<argument>${release.distrib.dir}/release_note</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>release-tag</id>
<goals>
<goal>exec</goal>
</goals>
<phase>validate</phase>
<configuration>
<executable>${java.home}/bin/java</executable>
<arguments>
<argument>${release.script}</argument>
<argument>tag</argument>
<argument>${project.basedir}/pom.xml</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>release-distrib</id>
<goals>
<goal>exec</goal>
</goals>
<phase>validate</phase>
<configuration>
<executable>${java.home}/bin/java</executable>
<arguments>
<argument>${release.script}</argument>
<argument>distrib</argument>
<argument>${project.basedir}/pom.xml</argument>
<argument>${release.distrib.dir}</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>