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
63 changes: 63 additions & 0 deletions build/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,44 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<!-- the input of the provided-BOM: this module's own resolved runtime
dependencies, which are exactly what spring-boot:repackage packs into
BOOT-INF/lib (never derived by unzipping the assembled artifact - a
reactor-built module's jar does not live at a local-repository path) -->
<id>list-provided-dependencies</id>
<phase>process-classes</phase>
<goals>
<goal>list</goal>
</goals>
<configuration>
<includeScope>runtime</includeScope>
<sort>true</sort>
<outputFile>${project.build.directory}/provided-dependencies.txt</outputFile>
</configuration>
</execution>
<execution>
<!-- stages the BOM generator on its own: the generator is deliberately
dependency-free, so the forked java below needs only this one jar on
its command line - the full runtime classpath would exceed the Windows
CreateProcess length limit (error=206) -->
<id>copy-bom-generator</id>
<phase>process-classes</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.eclipse.dirigible</groupId>
<artifactId>dirigible-components-core-dependencies</artifactId>
<version>${project.version}</version>
<outputDirectory>${project.build.directory}/bom-generator</outputDirectory>
<destFileName>bom-generator.jar</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
<execution>
<id>unpack-launcher-agent</id>
<phase>prepare-package</phase>
Expand Down Expand Up @@ -326,6 +364,31 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<!-- Turns the resolved dependency list into the provided-BOM (a standard
dependencyManagement POM) inside the classes, so it ships embedded in
the artifact as META-INF/dirigible-provided-bom.xml - the resolver reads
it to treat platform-shipped coordinates as provided and to report
shadowing. Runs in prepare-package, before the jar is assembled.
Forked with ONLY the staged generator jar on the classpath: the full
runtime classpath would exceed the Windows CreateProcess command-line
length limit (error=206), and a non-forked java is no alternative -
Ant installs a SecurityManager for it, which JDK 18+ refuses. -->
<id>generate-provided-bom</id>
<phase>prepare-package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<java classname="org.eclipse.dirigible.components.dependencies.ProvidedBomGenerator" classpath="${project.build.directory}/bom-generator/bom-generator.jar" fork="true" failonerror="true">
<arg value="${project.build.directory}/provided-dependencies.txt"/>
<arg value="${project.build.outputDirectory}/META-INF/dirigible-provided-bom.xml"/>
<arg value="${project.version}"/>
</java>
</target>
</configuration>
</execution>
<execution>
<!-- The launcher agent's classes must sit at the executable jar's ROOT: the
JVM loads the Launcher-Agent-Class through the system classloader before
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ void the_executable_jar_carries_the_agent_delivery() throws IOException {
}
}

@Test
void the_executable_jar_carries_the_provided_bom() throws IOException {
try (JarFile jar = new JarFile(executableJar().toFile())) {
// the ZIP-layout repackage keeps the original jar's META-INF at the ROOT, where the
// system classloader sees it on -jar launches
ZipEntry bom = jar.getEntry("META-INF/dirigible-provided-bom.xml");
assertNotNull(bom, "the provided-BOM must ship inside the artifact - the resolver's shadowing detection reads it");
String content = new String(jar.getInputStream(bom)
.readAllBytes(),
StandardCharsets.UTF_8);
assertTrue(content.contains("<artifactId>dirigible-provided-bom</artifactId>"),
"the embedded BOM must be the standard dependencyManagement POM");
assertTrue(content.contains("<groupId>com.google.code.gson</groupId>"),
"the BOM must enumerate the platform's BOOT-INF/lib inventory");
}
}

@Test
void a_jar_launch_installs_the_agent_before_main() throws IOException, InterruptedException {
Path workingDirectory = Files.createDirectories(tempDir.resolve("launch"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.components.dependencies;

/**
* The reported status of one artifact in the dependency report - every artifact the endpoint knows
* carries exactly one of the {@code STATUS_*} values, so nothing about the dependency layer is ever
* silent: shadowing, mediation, integrity failures and frozen-mode rejections all surface here.
*
* @param coordinate the groupId:artifactId:version coordinate (or the declared id when the
* declaration itself failed)
* @param scope module or platform
* @param status one of the {@code STATUS_*} values
* @param message what happened, operator-readable
*/
record ArtifactStatus(String coordinate, String scope, String status, String message) {

/** Serving in this process. */
static final String STATUS_ACTIVE = "active";

/** Takes effect at the next launch. */
static final String STATUS_PENDING_RESTART = "pending-restart";

/** The platform provides a different version; the declared one is inert. */
static final String STATUS_SHADOWED = "shadowed";

/** More than one version was requested; mediation chose this artifact's. */
static final String STATUS_MEDIATED = "mediated";

/** Not activated - declaration, resolution, integrity or activation failure. */
static final String STATUS_FAILED = "failed";

/** Rejected in frozen mode - the coordinate is not part of the lockfile. */
static final String STATUS_FROZEN_MISMATCH = "frozen-mismatch";

/** The module scope name as reported. */
static final String SCOPE_MODULE = "module";

/** The platform scope name as reported. */
static final String SCOPE_PLATFORM = "platform";

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@
* @param dependencies the declared dependencies in registry order
* @param errors the declaration errors that never reached resolution (bad coordinate, version
* range, unknown scope, ...), keyed by the declared id or the declaring project
* @param declaredBy the declaring projects per declared coordinate - the lockfile's
* {@code requestedBy} attribution
*/
record DeclaredDependencies(Set<MavenDependency> dependencies, Map<String, String> errors) {
record DeclaredDependencies(Set<MavenDependency> dependencies, Map<String, String> errors, Map<String, Set<String>> declaredBy) {

/**
* A change-detection fingerprint of the declarations - stable across collection order, different
* for any semantic change (coordinate, scope, exclusions, or a declaration error).
* for any semantic change (coordinate, scope, exclusions, attribution, or a declaration error).
*
* @return the fingerprint
*/
Expand All @@ -40,6 +42,11 @@ String fingerprint() {
.map(entry -> entry.getKey() + "=" + entry.getValue())
.sorted()
.collect(Collectors.joining(";"));
return declared + "||" + failed;
String attribution = declaredBy.entrySet()
.stream()
.map(entry -> entry.getKey() + "=" + new TreeSet<>(entry.getValue()))
.sorted()
.collect(Collectors.joining(";"));
return declared + "||" + failed + "||" + attribution;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,15 @@ ResponseEntity<DependenciesState> getState() {
}

/**
* Runs the union resolution on demand.
* Runs the union resolution on demand - in frozen mode this re-activates the locked set and
* surfaces any declaration the lock does not carry.
*
* @return the resolved state
*/
@PostMapping("resolve")
@RolesAllowed({"ADMINISTRATOR", "OPERATOR"})
ResponseEntity<DependenciesState> resolve() {
if (!dependenciesService.isDynamicEnabled()) {
if (!dependenciesService.isDynamicEnabled() && !dependenciesService.isFrozen()) {
throw new ResponseStatusException(HttpStatus.CONFLICT,
"Dynamic dependency resolution is disabled - set DIRIGIBLE_DEPENDENCIES_DYNAMIC=true to enable it");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
* Resolves the maven dependencies declared across the registry projects at startup - ordered after
* the synchronization initializer, so the project.json files are present in the registry. The
* resolved jars are activated through the modules classloader immediately; the run also arms the
* declaration watcher by recording the first fingerprint.
* declaration watcher by recording the first fingerprint. A frozen instance
* ({@code DIRIGIBLE_DEPENDENCIES_FROZEN=true}) boots through here too - its activation verifies
* every locked artifact's checksum before anything serves.
*/
@Order(ApplicationReadyEventListeners.DEPENDENCIES_INITIALIZER)
@Component
Expand All @@ -48,7 +50,7 @@ class DependenciesInitializer implements ApplicationListener<ApplicationReadyEve
*/
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
if (!dependenciesService.isDynamicEnabled()) {
if (!dependenciesService.isDynamicEnabled() && !dependenciesService.isFrozen()) {
LOGGER.debug("Dynamic dependency resolution is disabled");
return;
}
Expand Down
Loading
Loading