Imported from ogerardin/xpman (
AGENTS.md). Install upstream withnpx skills add ogerardin/xpman. Copyright stays with the author.
AGENTS.md
Build & Test Commands
# Full build (all modules, no tests — as CI does)
mvn -B -DskipTests clean package
# Run tests (only xpman-api and xpman-fx have tests; xpman-fx-dist doesn't)
mvn test
# Single-module test
mvn test -pl xpman-api
# Single test class
mvn test -pl xpman-api -Dtest=ParserTest
-
Installers are opt-in.
xpman-fx-distskips its 8 installer-producing plugins by default (<installers.skip>true</installers.skip>inxpman-fx-dist/pom.xml). CI activates them with-Pinstallers(or-Dbuild.installers=true). Without it, the repackaged uber-jar is still produced but no.deb/.rpm/.dmg/.pkg/.exe/.msi. -
The build uses Maven CI-friendly versions (
${revision}, local default in the root POM) — see Versioning below. CI always overrides it with-Drevisioncomputed byscripts/ci-version.sh. -
Many xpman-api tests access a real X-Plane installation discovered via auto-detection of common install locations (or optionally
XPMAN_XPLANE_ROOTenv var). Tests use@EnableOnLocalXPlane/@EnableOnLocalXPlane11/@EnableOnLocalXPlane12/@EnableOnAircraftPresent/@EnableOnSceneryPresentto skip when requirements aren't met. -
There is no lint or typecheck step beyond the compiler. The project previously had SonarCloud/Codacy but both have been removed.
Architecture
3 Maven modules (build order):
| Module | JPMS Module | Purpose |
|---|---|---|
xpman-api |
xpman.api |
Pure Java API — domain model, file parsers, inspection framework, install logic |
xpman-fx |
xpman.fx |
JavaFX desktop UI — FXML views, controllers, wizards, custom cell factories |
xpman-fx-dist |
(none) | Distribution packaging — repackaged uber-jar → platform installers (.dmg/.pkg, .exe/.msi, .deb/.rpm) via jpackage; the macOS DMG is built with dmgbuild |
- Java 25 source/target (
maven.compiler.release=25); requires JDK 25 to build. - JPMS is enforced — both xpman-api and xpman-fx have
module-info.java. Most dependencies are explicit JPMS modules; onlypetitparser-coreis a filename-based automatic module (declared under// filename-based automoduleinxpman-api/module-info.java). Zip handling uses the JDK's ownjava.util.zip. - No Spring DI container.
XPlane(folder)is the root object that manually constructs all managers.spring-expression(currently 7.0.8) is used only for SpEL evaluation in cell factories — it is the only SpEL consumer. The Spring Boot Maven plugin is used only for repackaging into an uber-jar (JarLauncher), NOT for a Spring app. - Lombok is heavily used:
@Data,@Slf4j,@Getter(lazy=true),@SneakyThrows,@Delegate,@Builder,@UtilityClass. - Gson for JSON (not Jackson). User config persisted to the dotfile
~/.xpman. - FXML for all UI views — controllers follow naming convention matching the FXML file. Resource root is
xpman-fx/src/main/resources/fxml/. IntrospectionHelper.getBestSubclassInstance()uses ClassGraph to scan the classpath for specialized domain subclasses at runtime (e.g.ZiboMod738 extends Aircraft).
Architecture Patterns
- Manager pattern: Every domain aggregate (aircraft, plugins, scenery, navdata, tools) has a
Manager<T>base with lazy loading, event dispatch (LOADING/LOADED), andreload(). - Inspection framework:
Inspection<T>is a functional interface composed via.and(). Domain objects implementInspectable. - InstallSource/InstallTarget: Strategy pattern for archive-based installation with auto-detection of archive content type.
- Custom event system: Lightweight
EventDispatcher<E>withEventListener<E>— no framework. - Platform polymorphism: The
Platforminterface is extended with default methods for platform-specific behavior (e.g.getCandidateInstallBaseFolders); eachMacPlatform/WindowsPlatform/LinuxPlatformoverrides as needed. Always prefer adding a method to thePlatforminterface overif/elseon platform type.
Style Policies
- Zero boilerplate. Aim for concise code; leverage Lombok, records, and language features to eliminate ceremony.
- Polymorphism over conditionals. Always prefer polymorphism to
switch/if-elsewhen it makes sense — dispatch via interfaces, enums with behavior (e.g. a label-bearing enum rendered directly instead of a switch in the cell factory), or strategy pattern. - Java idioms. Use
Optional, streams,sealedtypes, pattern matching, and other modern Java conventions where they improve clarity. - Functional style. Prefer functional programming where possible — e.g. streams instead of loops, method references over lambdas, immutable collections.
- JavaFX bindings. Use property bindings instead of manual listener/callback wiring wherever the relationship is declarative.
- Javadoc on all classes. Keep it concise and helpful — one-liner purpose, no
@author, no redundant@param/@returnnoise. Skip Javadoc on self-evident records/enums.
JPMS Notes
xpman-fxopens packages tojavafx.base,javafx.fxml,spring.expression, andcom.google.gsonfor reflection access.com.ogerardin.xpman.scenery_organizeris opened unqualified (to all modules, including the unnamed module) so SpEL'sReflectivePropertyAccessorcansetAccessibleonLibrarySceneryClasswhen running under IntelliJ's classpath layout (where Spring jars land in the unnamed module). A qualifiedopens ... to spring.expressionis insufficient there.- The runtime
--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controls(ControlsFX compatibility) is passed via the jpackage<javaOptions>inxpman-fx-dist/pom.xml(per-OS profile). The legacyxpman.l4j.iniwas removed with the launch4j migration. - IDE dev runs need the same flag manually: ControlsFX's
Wizard.readSettings()reflectively callsParent.getChildren(), so an IntelliJ (or plainjava) modular launch ofcom.ogerardin.xpman.XPmanFXmust add--add-opens=javafx.graphics/javafx.scene=org.controlsfx.controlsto its VM options — otherwise navigating a wizard fails withInaccessibleObjectException. jpackage builds already include it.
macOS DMG (dmgbuild)
- The macOS DMG is built with dmgbuild (a Python tool), not jpackage: the mac profile's
dmgbuildexec-maven-plugin (phasepackage) runsxpman-fx-dist/assets/mac/dmgbuild/build-dmg.shfrom the.appimage produced by jpackage. The build is strict — if dmgbuild fails, the Maven build fails. build-dmg.shcreates a Python venv atxpman-fx-dist/target/.dmgbuild-venv(PEP 668 blocks system pip) and installs dmgbuild there on first run; it finds Homebrew Pythons (python3.14…python3.10) or/usr/local/bin/python3.{13,12}.- DMG appearance is defined by
assets/mac/dmgbuild/settings.pyand reproduced pixel-perfect from the old jpackage DMG (window rect, icon grid positions, icon/text size, background, volume icon).background.tiffandVolumeIcon.icnsare committed byte-identical artifacts. settings.pyisexec'd by dmgbuild with the options dict as its namespace:defines(from-D key=valueon the CLI) is available, but__file__is NOT — always usedefines["SCRIPT_DIR"]for paths, andbuild-dmg.shmust pass-D SCRIPT_DIR="${SCRIPT_DIR}".- Do not add an
@2xbackground sibling next tobackground.tiff— dmgbuild would invoketiffutiland produce a different file than the committed one.
Versioning
- Tag-based, git-authoritative. A Git tag
X.Y.Z(strict semver) pushed tomainis the version. No tag → a snapshotX.Y.(Z+1)-SNAPSHOTwhereX.Y.Zis the highest semver tag (fallback base1.0.0→1.0.1-SNAPSHOT). scripts/ci-version.shis the single source of truth: given an optional tag argument it prints/exportsREVISION,GH_VERSION, andIS_SNAPSHOT. It can be executed or sourced, but sourcing requires bash (usesBASH_SOURCE; fails under zsh — CI uses bash). It usesgit tag --sort=version:refname, so it needs a checkout with tags (fetch-depth: 0on GHA).GH_VERSIONis the same asREVISIONfor releases; for snapshots it appends-sha.XXXXXXXfor unique GitHub release tags.- Maven CI-friendly versions: the root POM defines the local default
<revision>(currently1.0.1-SNAPSHOT); all modules and inter-module dependencies reference${revision}. CI always overrides with-Drevision="$REVISION"— the POM default is only used for local builds. Maven ignores+...build metadata, soGH_VERSIONis only used for GitHub Release tags, never passed to Maven. - Releases: on a semver-tag push, GitHub Actions publishes a release (tag
X.Y.Z) after deleting all pre-releases; on every tag-lessmainpush it publishes the same snapshot as a pre-release (--prerelease). Snapshot releases use-sha.XXXXXXXin the tag (GH_VERSION) so each commit gets a unique tag; release titles remainX.Y.(Z+1)-SNAPSHOT (YYYY-MM-DD). Same-day pre-releases with the same title are deduplicated — only the latest survives. Snapshot installer artifacts get-SNAPSHOTinserted before the extension (.deb/.rpm/.dmg/.pkg/.exe/.msi); the repackaged jars already carry-SNAPSHOTviafinalName. - Post-release bump: after tagging
X.Y.Z, bump the root POM<revision>toX.Y.(Z+1)-SNAPSHOTin a follow-up commit.
CI & Dependencies
- GitHub Actions is the sole CI and release publisher (see
.github/workflows/build.yml), building Linux, macOS, and Windows on everypush(any branch or tag) and onpull_request. Thereleasejob only fires onmainpushes and semver-tag pushes (refs/heads/mainorrefs/tags/*): on every tag-lessmainpush it creates/recreates the GitHub release: thereleasejob (afterlinux/windows/mac) downloads the artifacts, computes the version viascripts/ci-version.sh(no JDK 25 needed there), and runsgh release create "$VERSION" ... --generate-noteswith delete-then-recreate semantics (--cleanup-tag, tag =$VERSION, same as the oldghr -delete); snapshots additionally get--prerelease. Same-day pre-releases with the same title are deduplicated — only the latest survives. On a semver-tag push, it first deletes all pre-releases before creating the full release. All CI build steps pass-Drevision="$REVISION"(computed by the script from$GITHUB_REF_NAME) and-Pinstallers, and checkouts usefetch-depth: 0sogit tagworks. Runner toolchains (Maven, JDK 25, ImageMagick, Inno Setup, WiX, Python) are pre-installed on the images; JDK 25 is wired via runner env vars (JAVA_HOME_25_X64/JAVA_HOME_25_arm64) written toGITHUB_ENV/GITHUB_PATH(theenvcontext does not expose them). Never use[skip ci]/[ci skip]markers: they skip all CIs. - Dependabot manages version bumps (labels:
dependencies,java). - pecoff4j dependency comes from Jitpack repository (needed for reading Windows PE executables on non-Windows platforms).
Testing
- JUnit Jupiter 5 + Hamcrest + Mockito (xpman-api only).
TimingExtensionlogs test method execution times.- Tests that need a real X-Plane install use
@EnableOnLocalXPlane*annotations (incom.ogerardin.test.util) to skip automatically when no matching X-Plane installation is found;@EnableOnAircraftPresentand@EnableOnSceneryPresentadditionally gate on specific add-on files. - Test resources include sample X-Plane files (ACF, OBJ, scenery_packs.ini, server lists) in
xpman-api/src/test/resources/.