Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion beast-base/src/assembly/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,18 @@
<!-- Builds the installable BEAST.base package zip:
version.xml (the BEAST.base package descriptor, at repo root)
lib/beast-base.jar + its third-party runtime dependencies
examples/ example XML (+ nexus data) shown in BEAUti
beast-pkgmgmt is excluded: it is the bootstrap/launcher and is always
provided by the core distribution, so bundling it here would duplicate
the beast.pkgmgmt module on the module path. -->
the beast.pkgmgmt module on the module path.

Examples are shipped *inside the package* (not only in the release
bundle root) so that a BEAST.base point release can update them via the
package manager: seedBundledPackage()/the package manager extract them
to <user package dir>/BEAST.base/examples, which BeautiTabPane and
PackageHealthChecker discover by scanning getBeastDirectories(). The
curated subset mirrors what release/.../assemble-bundle.sh copies into
the bundle root. -->
<id>package</id>
<formats>
<format>zip</format>
Expand All @@ -20,6 +29,18 @@
</file>
</files>

<fileSets>
<fileSet>
<directory>${project.basedir}/src/test/resources/beast.base/examples</directory>
<outputDirectory>examples</outputDirectory>
<includes>
<include>*.xml</include>
<include>spec/*.xml</include>
<include>nexus/**</include>
</includes>
</fileSet>
</fileSets>

<dependencySets>
<dependencySet>
<outputDirectory>lib</outputDirectory>
Expand Down
17 changes: 14 additions & 3 deletions beast-base/src/main/java/beast/base/minimal/BeastMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ public static void printUsage(final Arguments arguments) {
private static void printVersion() {
Log.info("BEAST " + (new BEASTVersion()).getVersionString());
Log.info("---");
// The same package can be discovered through more than one directory on
// the package path (e.g. BEAST.base is found both in the user package dir
// and via the bundle-root version.xml). List each package once, noting
// where it was found; later duplicates are reported as skipped, but only
// in verbose mode.
java.util.Set<String> printed = new java.util.HashSet<>();
for (String jarDirName : PackageManager.getBeastDirectories()) {
File versionFile = new File(jarDirName + "/version.xml");
if (versionFile.exists()) {
Expand All @@ -77,9 +83,14 @@ private static void printVersion() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document doc = factory.newDocumentBuilder().parse(versionFile);
Element packageElement = doc.getDocumentElement();
Log.info.print(packageElement.getAttribute("name") + " v" + packageElement.getAttribute("version"));
Log.debug.print(" " + jarDirName);
Log.info.print("\n");
String nameAndVersion = packageElement.getAttribute("name") + " v" + packageElement.getAttribute("version");
if (printed.add(nameAndVersion)) {
Log.info.print(nameAndVersion);
Log.debug.print(" (found in " + jarDirName + ")");
Log.info.print("\n");
} else {
Log.debug.println("Skipping duplicate " + nameAndVersion + " found in " + jarDirName);
}
} catch (IOException| SAXException| ParserConfigurationException e) {
Log.err(e.getMessage());
}
Expand Down
17 changes: 14 additions & 3 deletions beast-fx/src/main/java/beastfx/app/beast/BeastMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ public static void printUsage(final Arguments arguments) {
private static void printVersion() {
Log.info("BEAST " + (new BEASTVersion()).getVersionString());
Log.info("---");
// The same package can be discovered through more than one directory on
// the package path (e.g. BEAST.base is found both in the user package dir
// and via the bundle-root version.xml). List each package once, noting
// where it was found; later duplicates are reported as skipped, but only
// in verbose mode.
java.util.Set<String> printed = new java.util.HashSet<>();
for (String jarDirName : PackageManager.getBeastDirectories()) {
File versionFile = new File(jarDirName + "/version.xml");
if (versionFile.exists()) {
Expand All @@ -111,9 +117,14 @@ private static void printVersion() {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
Document doc = factory.newDocumentBuilder().parse(versionFile);
Element packageElement = doc.getDocumentElement();
Log.info.print(packageElement.getAttribute("name") + " v" + packageElement.getAttribute("version"));
Log.debug.print(" " + jarDirName);
Log.info.print("\n");
String nameAndVersion = packageElement.getAttribute("name") + " v" + packageElement.getAttribute("version");
if (printed.add(nameAndVersion)) {
Log.info.print(nameAndVersion);
Log.debug.print(" (found in " + jarDirName + ")");
Log.info.print("\n");
} else {
Log.debug.println("Skipping duplicate " + nameAndVersion + " found in " + jarDirName);
}
} catch (IOException| SAXException| ParserConfigurationException e) {
Log.err(e.getMessage());
}
Expand Down
31 changes: 30 additions & 1 deletion beast-pkgmgmt/src/main/java/beast/pkgmgmt/BEASTClassLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ public static Class<?> forName(String className) throws ClassNotFoundException {
ClassLoader loader = class2loaderMap.get(className);
try {
return Class.forName(className, false, loader);
} catch (ClassNotFoundException e) {
// The mapped loader cannot see this class. This happens when the
// provider was registered from a version.xml scan (via
// resolveLoaderFor → fallback system/context loader) before the
// owning package's plugin ModuleLayer was registered: the later
// putIfAbsent in registerPluginLayer() then cannot replace the
// stale fallback loader. Fall through to search the plugin layers
// and correct the mapping below.
} catch (NoClassDefFoundError e) {
String missing = e.getMessage();
if (missing != null && (missing.startsWith("beastfx/") || missing.startsWith("beastfx.")
Expand All @@ -91,8 +99,15 @@ public static Class<?> forName(String className) throws ClassNotFoundException {
// 2. Plugin layers (external BEAST packages)
for (ModuleLayer layer : pluginLayers) {
for (Module module : layer.modules()) {
ClassLoader mcl = module.getClassLoader();
if (mcl == null) continue;
try {
return Class.forName(className, false, module.getClassLoader());
Class<?> c = Class.forName(className, false, mcl);
// Self-heal: cache the loader that actually resolved the
// class so subsequent lookups skip the failing mapped loader
// (and the exception-driven fall-through above).
class2loaderMap.put(className, mcl);
return c;
} catch (ClassNotFoundException | NoClassDefFoundError e) {
// try next module
}
Expand Down Expand Up @@ -559,6 +574,20 @@ private static ClassLoader resolveLoaderFor(String provider) {
if (loader != null) return loader;
}
}
// Also consult already-registered plugin layers: a package's classes
// live in its own plugin ModuleLayer (e.g. beast.fx for the BEAUti
// input editors), not in the boot layer. Without this, providers listed
// in a version.xml would be mapped to the fallback system loader, which
// cannot load them, leaving forName() to fall through on every lookup.
for (ModuleLayer layer : pluginLayers) {
for (Module m : layer.modules()) {
java.lang.module.ModuleDescriptor desc = m.getDescriptor();
if (desc != null && desc.packages().contains(pkg)) {
ClassLoader loader = m.getClassLoader();
if (loader != null) return loader;
}
}
}
return fallbackClassLoader();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.spi.ToolProvider;
import java.util.stream.Stream;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
Expand Down Expand Up @@ -64,6 +67,47 @@ public void dependentPackageResolvesAgainstAnotherPluginLayer() throws Exception
assertEquals("base+plugin", result, "cross-layer call should execute");
}

/**
* Regression test for the BEAUti "ClassNotFoundException: beastfx.app.inputeditor.*"
* failure. A package's service providers are listed in its version.xml. If a
* version.xml scan registers a provider (via addServices → resolveLoaderFor)
* before the owning package's plugin ModuleLayer is registered, the provider
* is mapped to the fallback system loader, which cannot load it. registerPluginLayer()
* uses putIfAbsent and so cannot replace that stale mapping. forName() must still
* resolve the class by falling through to the plugin layers.
*/
@Test
public void forNameHealsStaleFallbackLoaderMapping() throws Exception {
ToolProvider javac = ToolProvider.findFirst("javac").orElse(null);
Assumptions.assumeTrue(javac != null, "no javac tool available - skipping");

Path work = Files.createTempDirectory("stale-loader-test");

// A modular jar exporting a provider class, mimicking beast.fx's input editors.
Path editorJar = buildModuleJar(javac, work, "fxstaletest", null,
"module fxstaletest { exports fxstaletest; }",
"fxstaletest", "Editor",
"package fxstaletest; public class Editor { }");

// 1. Simulate the version.xml scan that runs before the plugin layer exists:
// the provider is mapped to the fallback system loader (cannot load it).
Map<String, Set<String>> services = Map.of(
"fxstaletest.InputEditor", Set.of("fxstaletest.Editor"));
BEASTClassLoader.classLoader.addServices("FxStaleTest", services);

// 2. Register the package's real plugin layer. putIfAbsent leaves the
// stale fallback mapping from step 1 in place.
boolean loaded = PackageManager.createAndRegisterModuleLayer(
List.of(editorJar), null, "FxStaleTest", "1.0", "FxStaleTest");
assertTrue(loaded, "provider module should load into a plugin layer");

// 3. Despite the stale mapping, forName must resolve the class via the
// plugin layer (previously threw ClassNotFoundException here).
Class<?> editor = BEASTClassLoader.forName("fxstaletest.Editor");
assertNotNull(editor);
assertEquals("fxstaletest.Editor", editor.getName());
}

/** Compile a single-package module and pack it into a jar; return the jar path. */
private static Path buildModuleJar(ToolProvider javac, Path work, String moduleName,
Path modulePathJar, String moduleInfo,
Expand Down
21 changes: 10 additions & 11 deletions release/Linux/assemble-bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -152,20 +152,19 @@ else
echo " WARNING: DensiTree.jar not found — densitree script will not work"
fi

# ── Step 6: Copy examples ─────────────────────────────────────────────────────
# ── Step 6: Extract examples from the BEAST.base package zip ──────────────────
# The bundle-root examples/ are extracted from the same package zip seeded into
# the user dir (lib/packages/), so the example set has a single source of truth
# (beast-base/src/assembly/package.xml) and the bundle-root and packaged copies
# cannot drift. The bundle-root copy is kept so `beast -validate examples/...`
# works from the install dir before first-run seeding.
echo ""
echo "==> Step 6: Copying examples..."
EXAMPLES_DIR="$REPO_ROOT/beast-base/src/test/resources/beast.base/examples"
if [ -d "$EXAMPLES_DIR" ]; then
find "$EXAMPLES_DIR" -maxdepth 1 -name "*.xml" -exec cp {} "$BUNDLE/examples/" \;
mkdir -p "$BUNDLE/examples/spec"
[ -d "$EXAMPLES_DIR/spec" ] && \
find "$EXAMPLES_DIR/spec" -maxdepth 1 -name "*.xml" -exec cp {} "$BUNDLE/examples/spec/" \;
[ -d "$EXAMPLES_DIR/nexus" ] && cp -r "$EXAMPLES_DIR/nexus" "$BUNDLE/examples/"
echo "==> Step 6: Extracting examples from $(basename "$BASE_PKG_ZIP")..."
if unzip -o -q "$BASE_PKG_ZIP" 'examples/*' -d "$BUNDLE"; then
EXAMPLE_COUNT=$(find "$BUNDLE/examples" -name "*.xml" | wc -l | tr -d ' ')
echo " Copied ${EXAMPLE_COUNT} example XML files (incl. spec/)"
echo " Extracted ${EXAMPLE_COUNT} example XML files (incl. spec/)"
else
echo " WARNING: examples not found at $EXAMPLES_DIR"
echo " WARNING: no examples found in $BASE_PKG_ZIP"
fi

# ── Step 7: Copy version.xml and docs ────────────────────────────────────────
Expand Down
22 changes: 8 additions & 14 deletions release/Mac/build-sign-dmg.sh
Original file line number Diff line number Diff line change
Expand Up @@ -558,21 +558,15 @@ else
fi

# ── examples/ — example BEAST XML files ──────────────────────────────────────
EXAMPLES_DIR="$REPO_ROOT/beast-base/src/test/resources/beast.base/examples"
if [ -d "$EXAMPLES_DIR" ]; then
echo " Copying examples/..."
mkdir -p "$OUTPUT/examples"
# Copy top-level XML files
find "$EXAMPLES_DIR" -maxdepth 1 -name "*.xml" -exec cp {} "$OUTPUT/examples/" \;
# Copy nexus subdirectory
if [ -d "$EXAMPLES_DIR/nexus" ]; then
cp -r "$EXAMPLES_DIR/nexus" "$OUTPUT/examples/nexus"
fi
if [ -d "$EXAMPLES_DIR/spec" ]; then
cp -r "$EXAMPLES_DIR/spec" "$OUTPUT/examples/spec"
fi
# Extracted from the BEAST.base package zip seeded into the user dir
# (Contents/app/packages/), so the example set has a single source of truth
# (beast-base/src/assembly/package.xml) and the bundle-root and packaged copies
# cannot drift. The bundle-root copy is kept so `beast -validate examples/...`
# works from the install dir before first-run seeding.
if [ -n "$BASE_PKG_ZIP" ] && unzip -o -q "$BASE_PKG_ZIP" 'examples/*' -d "$OUTPUT"; then
echo " Extracted examples/ from $(basename "$BASE_PKG_ZIP")"
else
echo " WARNING: $EXAMPLES_DIR not found — skipping examples/"
echo " WARNING: no examples found in BEAST.base package zip — skipping examples/"
fi

# ── README and LICENSE ─────────────────────────────────────────────────────
Expand Down
21 changes: 10 additions & 11 deletions release/Windows/assemble-bundle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -244,20 +244,19 @@ WINBIN_DIR="$SCRIPT_DIR/bat"
mkdir -p "$BUNDLE/bat"
cp "$WINBIN_DIR/"*.bat "$BUNDLE/bat/"

# ── Step 7: Copy examples ─────────────────────────────────────────────────────
# ── Step 7: Extract examples from the BEAST.base package zip ──────────────────
# The bundle-root examples/ are extracted from the same package zip seeded into
# the user dir (app/packages/), so the example set has a single source of truth
# (beast-base/src/assembly/package.xml) and the bundle-root and packaged copies
# cannot drift. The bundle-root copy is kept so `beast -validate examples/...`
# works from the install dir before first-run seeding.
echo ""
echo "==> Step 7: Copying examples..."
EXAMPLES_DIR="$REPO_ROOT/beast-base/src/test/resources/beast.base/examples"
if [ -d "$EXAMPLES_DIR" ]; then
mkdir -p "$BUNDLE/examples/spec"
find "$EXAMPLES_DIR" -maxdepth 1 -name "*.xml" -exec cp {} "$BUNDLE/examples/" \;
[ -d "$EXAMPLES_DIR/spec" ] && \
find "$EXAMPLES_DIR/spec" -maxdepth 1 -name "*.xml" -exec cp {} "$BUNDLE/examples/spec/" \;
[ -d "$EXAMPLES_DIR/nexus" ] && cp -r "$EXAMPLES_DIR/nexus" "$BUNDLE/examples/"
echo "==> Step 7: Extracting examples from $(basename "$BASE_PKG_ZIP")..."
if unzip -o -q "$BASE_PKG_ZIP" 'examples/*' -d "$BUNDLE"; then
EXAMPLE_COUNT=$(find "$BUNDLE/examples" -name "*.xml" | wc -l | tr -d ' ')
echo " Copied ${EXAMPLE_COUNT} example XML files (incl. spec/)"
echo " Extracted ${EXAMPLE_COUNT} example XML files (incl. spec/)"
else
echo " WARNING: examples not found at $EXAMPLES_DIR"
echo " WARNING: no examples found in $BASE_PKG_ZIP"
fi

# ── Step 8: Copy version.xml and docs ────────────────────────────────────────
Expand Down