diff --git a/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/CompiledModuleClassProvider.java b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/CompiledModuleClassProvider.java
new file mode 100644
index 00000000000..cb65de48401
--- /dev/null
+++ b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/CompiledModuleClassProvider.java
@@ -0,0 +1,145 @@
+/*
+ * 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.engine.java.runtime;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.eclipse.dirigible.engine.java.spi.LoadedClass;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.event.EventListener;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.stereotype.Component;
+
+/**
+ * Discovers AOT-packaged {@code compiled} modules on the application classpath and registers their
+ * already-compiled classes through {@link JavaLoader#installCompiledModules(List)} - without
+ * any runtime compilation.
+ *
+ *
+ * A compiled module jar carries a marker at {@code META-INF/dirigible//.compiled} - a
+ * UTF-8 text file listing the module's top-level class binary names (its controllers / repositories
+ * / delegates / listeners / …), one per line (blank lines and {@code #} comments ignored). The
+ * build emits it; it names exactly the classes the engine should surface to the
+ * {@code JavaClassConsumer}s.
+ *
+ *
+ * Discovery reads the markers directly from the classpath (so it does not depend on the
+ * {@code ClasspathExpander} having laid the module's registry payload down first) and loads each
+ * listed class through the application classloader - the same one that already holds the
+ * compiled module jar. It runs once, on {@link ApplicationReadyEvent}; a later registry rebuild
+ * unions its classes back in (see {@link JavaLoader}).
+ */
+@Component
+public class CompiledModuleClassProvider {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(CompiledModuleClassProvider.class);
+
+ /**
+ * Marker location pattern: one path segment ({@code }) under {@code META-INF/dirigible}.
+ */
+ private static final String MARKER_PATTERN = "classpath*:META-INF/dirigible/*/.compiled";
+
+ private static final String DIRIGIBLE_ROOT = "META-INF/dirigible/";
+
+ private final JavaLoader javaLoader;
+
+ @Autowired
+ public CompiledModuleClassProvider(JavaLoader javaLoader) {
+ this.javaLoader = javaLoader;
+ }
+
+ /** Discover and register the compiled modules once the application context is ready. */
+ @EventListener(ApplicationReadyEvent.class)
+ public void registerCompiledModules() {
+ List classes = discover();
+ if (classes.isEmpty()) {
+ return;
+ }
+ javaLoader.installCompiledModules(classes);
+ LOGGER.info("Registered [{}] class(es) from AOT compiled module(s) on the classpath", classes.size());
+ }
+
+ /**
+ * Scan the classpath for {@code .compiled} markers and load every listed class through the
+ * application classloader. Package-visible for testing. Never throws: an unreadable marker or an
+ * unloadable class is logged and skipped so one bad module cannot block the rest.
+ */
+ List discover() {
+ List result = new ArrayList<>();
+ ClassLoader classLoader = getClass().getClassLoader();
+ Resource[] markers;
+ try {
+ markers = new PathMatchingResourcePatternResolver(classLoader).getResources(MARKER_PATTERN);
+ } catch (IOException e) {
+ LOGGER.error("Failed to scan the classpath for AOT compiled-module markers", e);
+ return result;
+ }
+ for (Resource marker : markers) {
+ String project = projectOf(marker);
+ for (String fqn : readClassNames(marker)) {
+ try {
+ Class> type = Class.forName(fqn, true, classLoader);
+ result.add(new LoadedClass(project, fqn, type, classLoader));
+ } catch (ClassNotFoundException | LinkageError e) {
+ LOGGER.error("Compiled-module class [{}] (project [{}]) could not be loaded: {}", fqn, project, e.getMessage(), e);
+ }
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The {@code } path segment immediately under {@code META-INF/dirigible/} in the marker
+ * URL.
+ */
+ private static String projectOf(Resource marker) {
+ try {
+ String url = marker.getURL()
+ .toString();
+ int at = url.indexOf(DIRIGIBLE_ROOT);
+ if (at < 0) {
+ return "";
+ }
+ String rest = url.substring(at + DIRIGIBLE_ROOT.length());
+ int slash = rest.indexOf('/');
+ return slash < 0 ? rest : rest.substring(0, slash);
+ } catch (IOException e) {
+ LOGGER.error("Failed to resolve the project of compiled-module marker [{}]: {}", marker, e.getMessage(), e);
+ return "";
+ }
+ }
+
+ /** Non-blank, non-comment lines of a marker, trimmed. */
+ private static List readClassNames(Resource marker) {
+ List names = new ArrayList<>();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(marker.getInputStream(), StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String trimmed = line.trim();
+ if (!trimmed.isEmpty() && !trimmed.startsWith("#")) {
+ names.add(trimmed);
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.error("Failed to read compiled-module marker [{}]: {}", marker, e.getMessage(), e);
+ }
+ return names;
+ }
+
+}
diff --git a/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/JavaLoader.java b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/JavaLoader.java
index ad9da36bd8f..6b6ef01d9d5 100644
--- a/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/JavaLoader.java
+++ b/components/engine/engine-java/src/main/java/org/eclipse/dirigible/engine/java/runtime/JavaLoader.java
@@ -65,9 +65,19 @@ public class JavaLoader {
private final JavaCompiledOutputDirectory outputDirectory;
private final ApplicationEventPublisher eventPublisher;
- /** FQN → {@link LoadedClass} record for the currently-installed generation. */
+ /** FQN → {@link LoadedClass} record for the currently-installed generation (the union below). */
private final Map currentGeneration = new HashMap<>();
+ /** Registry-compiled sub-generation (produced by {@link #rebuild}). */
+ private final Map registryGeneration = new HashMap<>();
+
+ /**
+ * Classpath-discovered sub-generation for AOT-packaged {@code compiled} modules (produced by
+ * {@link #installCompiledModules}). These classes are already loaded by the application classloader
+ * - never compiled at runtime. The installed generation is the union of the two.
+ */
+ private final Map compiledGeneration = new HashMap<>();
+
/**
* Binary class name (top-level + nested) → bytecode for the currently-installed generation.
* Retained so a class that fails to recompile this cycle can fall back to its last-good bytecode
@@ -160,6 +170,42 @@ public synchronized RebuildResult rebuild(List sources) {
}
}
+ // Install the new generation as the union of the registry-compiled classes (this rebuild) and
+ // any classpath-discovered compiled modules. Extracted (applyGeneration) so both producers
+ // drive the identical consumer/container dispatch.
+ registryGeneration.clear();
+ registryGeneration.putAll(nextGeneration);
+ Set removed = applyGeneration(mergedGeneration(), nextLoader);
+
+ currentBytecode.clear();
+ currentBytecode.putAll(effectiveBytecode);
+
+ RebuildResult result = new RebuildResult(Collections.unmodifiableSet(succeeded), Collections.unmodifiableMap(failures),
+ Collections.unmodifiableSet(removed), Collections.unmodifiableMap(batch.diagnostics()), componentContainer.wiringErrors());
+
+ // Writes this cycle's fresh bytecode and deletes only source-removed FQNs. Carried-over
+ // (failed-to-recompile) classes keep their existing .class files untouched.
+ writeClassFiles(batch, result.unloadedFqns());
+ eventPublisher.publishEvent(new JavaCompiledEvent(this, result.succeededFqns(), result.unloadedFqns()));
+
+ return result;
+ }
+
+ /**
+ * Install a freshly-built generation into the running state: diff it against the current
+ * generation, notify every {@link JavaClassConsumer} ({@code onClassUnloaded} for removed/replaced
+ * FQNs, then {@code onClassLoaded} for the new set), swap the client {@link ClassLoader}, and
+ * rebuild the client bean container so behaviour consumers wire over ready instances.
+ *
+ *
+ * Source-agnostic by design. The generation is just a {@code FQN -> LoadedClass} map plus
+ * the loader to install; it may originate from compiled registry sources (the {@link #rebuild}
+ * path) or, for AOT-packaged {@code compiled} modules, from classes discovered on the application
+ * classpath. Both drive the identical consumer/container dispatch here.
+ *
+ * @return the FQNs removed from the generation (present before, absent now)
+ */
+ private Set applyGeneration(Map nextGeneration, ClientClassLoader nextLoader) {
// Diff against the previous generation: notify consumers of removals first, then additions.
Set previousFqns = new HashSet<>(currentGeneration.keySet());
Set nextFqns = new HashSet<>(nextGeneration.keySet());
@@ -195,18 +241,51 @@ public synchronized RebuildResult rebuild(List sources) {
currentGeneration.clear();
currentGeneration.putAll(nextGeneration);
- currentBytecode.clear();
- currentBytecode.putAll(effectiveBytecode);
-
- RebuildResult result = new RebuildResult(Collections.unmodifiableSet(succeeded), Collections.unmodifiableMap(failures),
- Collections.unmodifiableSet(removed), Collections.unmodifiableMap(batch.diagnostics()), componentContainer.wiringErrors());
+ return removed;
+ }
- // Writes this cycle's fresh bytecode and deletes only source-removed FQNs. Carried-over
- // (failed-to-recompile) classes keep their existing .class files untouched.
- writeClassFiles(batch, result.unloadedFqns());
- eventPublisher.publishEvent(new JavaCompiledEvent(this, result.succeededFqns(), result.unloadedFqns()));
+ /**
+ * Install the classes of AOT-packaged {@code compiled} modules discovered on the application
+ * classpath. Unlike {@link #rebuild}, these are not compiled at runtime - they are already
+ * loaded by the application classloader; this only records them as the compiled sub-generation and
+ * installs the union with the current registry-compiled generation through {@link #applyGeneration}
+ * (the same consumer/container dispatch). Idempotent: re-invoking replaces the compiled set.
+ *
+ * @param classes the compiled modules' top-level classes to surface to consumers
+ * @return the FQNs removed from the installed generation as a result (compiled classes dropped
+ * since the previous compiled install; never registry-source classes)
+ */
+ public synchronized Set installCompiledModules(List classes) {
+ compiledGeneration.clear();
+ for (LoadedClass info : classes) {
+ if (info != null) {
+ compiledGeneration.put(info.fqn(), info);
+ }
+ }
+ // No runtime-compiled client code yet → give the holder an empty ClientClassLoader whose parent
+ // (the platform / application classloader) already resolves the compiled-module classes.
+ ClientClassLoader loader = loaderHolder.current();
+ if (loader == null) {
+ loader = new ClientClassLoader(JavaHandler.class.getClassLoader(), Map.of());
+ }
+ return applyGeneration(mergedGeneration(), loader);
+ }
- return result;
+ /**
+ * The installed generation: the union of the registry-compiled and classpath-compiled
+ * sub-generations. On an FQN clash (a capability provided by both a registry source and a compiled
+ * module - a mis-assembled image) the registry source wins and the clash is logged; a clean
+ * "exactly one provider" assembly check is expected to catch this earlier.
+ */
+ private Map mergedGeneration() {
+ Map union = new LinkedHashMap<>(compiledGeneration);
+ for (Map.Entry entry : registryGeneration.entrySet()) {
+ if (union.put(entry.getKey(), entry.getValue()) != null) {
+ LOGGER.warn("FQN [{}] is provided by BOTH a compiled module and a registry source; using the registry source",
+ entry.getKey());
+ }
+ }
+ return union;
}
/**
diff --git a/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/controller/CompiledModuleControllerRegistrationTest.java b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/controller/CompiledModuleControllerRegistrationTest.java
new file mode 100644
index 00000000000..4e743738222
--- /dev/null
+++ b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/controller/CompiledModuleControllerRegistrationTest.java
@@ -0,0 +1,86 @@
+/*
+ * 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.engine.java.controller;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Optional;
+
+import org.eclipse.dirigible.engine.java.component.ComponentContainer;
+import org.eclipse.dirigible.engine.java.runtime.ClientBeansHolder;
+import org.eclipse.dirigible.engine.java.runtime.ClientClassLoaderHolder;
+import org.eclipse.dirigible.engine.java.runtime.JavaCompiledOutputDirectory;
+import org.eclipse.dirigible.engine.java.runtime.JavaLoader;
+import org.eclipse.dirigible.engine.java.runtime.JavaSourceCompiler;
+import org.eclipse.dirigible.engine.java.spi.LoadedClass;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.http.HttpMethod;
+
+/**
+ * End-to-end (within engine-java): a controller shipped by an AOT-packaged {@code compiled} module
+ * - installed through {@code JavaLoader.installCompiledModules} rather than compiled from registry
+ * sources - registers a real route in the {@link ControllerRouter} via the standard
+ * {@link ControllerClassConsumer}. No runtime {@code javac} is involved.
+ */
+class CompiledModuleControllerRegistrationTest {
+
+ private static final ApplicationEventPublisher NOOP_PUBLISHER = new ApplicationEventPublisher() {
+ @Override
+ public void publishEvent(ApplicationEvent event) {
+ // intentionally empty
+ }
+
+ @Override
+ public void publishEvent(Object event) {
+ // intentionally empty
+ }
+ };
+
+ @TempDir
+ Path tempDir;
+
+ @Test
+ void compiled_module_controller_registers_a_route_end_to_end() {
+ ControllerRouter router = new ControllerRouter();
+ ComponentContainer container = new ComponentContainer(new ClientBeansHolder());
+ ControllerClassConsumer controllerConsumer = new ControllerClassConsumer(router, Optional.empty(), container);
+
+ JavaCompiledOutputDirectory outputDirectory = mock(JavaCompiledOutputDirectory.class);
+ when(outputDirectory.get()).thenReturn(tempDir);
+ JavaLoader loader = new JavaLoader(new JavaSourceCompiler(), new ClientClassLoaderHolder(), container, List.of(controllerConsumer),
+ outputDirectory, NOOP_PUBLISHER);
+
+ // A compiled-module controller already on the classpath (no registry .java, no javac).
+ Class> type = CompiledSampleController.class;
+ String project = "acme-mod";
+ loader.installCompiledModules(List.of(new LoadedClass(project, type.getName(), type, type.getClassLoader())));
+
+ // The route is resolvable through the router - base path is /, suffix /ping.
+ String base = "/" + type.getName()
+ .replace('.', '/');
+ RouteMatch match = router.match(HttpMethod.GET, project, base + "/ping")
+ .orElseThrow(() -> new AssertionError("compiled-module controller route not registered"));
+ assertEquals(type.getName(), match.entry()
+ .fqn());
+ assertEquals(HttpMethod.GET, match.route()
+ .httpMethod());
+ assertTrue(router.match(HttpMethod.POST, project, base + "/ping")
+ .isEmpty(),
+ "only the declared GET route matches");
+ }
+}
diff --git a/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/controller/CompiledSampleController.java b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/controller/CompiledSampleController.java
new file mode 100644
index 00000000000..0ede5d7659a
--- /dev/null
+++ b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/controller/CompiledSampleController.java
@@ -0,0 +1,27 @@
+/*
+ * 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.engine.java.controller;
+
+import org.eclipse.dirigible.sdk.http.Controller;
+import org.eclipse.dirigible.sdk.http.Get;
+
+/**
+ * Stand-in for a controller shipped by an AOT-packaged {@code compiled} module (already on the
+ * classpath). Registered end-to-end via {@code JavaLoader.installCompiledModules} in
+ * {@link CompiledModuleControllerRegistrationTest}.
+ */
+@Controller
+public class CompiledSampleController {
+
+ @Get("/ping")
+ public String ping() {
+ return "pong";
+ }
+}
diff --git a/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/AotFixtureClass.java b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/AotFixtureClass.java
new file mode 100644
index 00000000000..fbaf5688636
--- /dev/null
+++ b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/AotFixtureClass.java
@@ -0,0 +1,18 @@
+/*
+ * 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.engine.java.runtime;
+
+/**
+ * A top-level class on the test classpath standing in for an AOT compiled-module class. Listed by
+ * the {@code META-INF/dirigible/aot-test-mod/.compiled} marker so
+ * {@link CompiledModuleClassProvider} discovers and loads it.
+ */
+public class AotFixtureClass {
+}
diff --git a/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/CompiledModuleClassProviderTest.java b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/CompiledModuleClassProviderTest.java
new file mode 100644
index 00000000000..73ea86a70e8
--- /dev/null
+++ b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/CompiledModuleClassProviderTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.engine.java.runtime;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.util.List;
+
+import org.eclipse.dirigible.engine.java.spi.LoadedClass;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies {@link CompiledModuleClassProvider} discovers a
+ * {@code META-INF/dirigible//.compiled} marker on the classpath, derives the project from
+ * the path, and loads the listed class through the application classloader - no runtime
+ * compilation.
+ */
+class CompiledModuleClassProviderTest {
+
+ @Test
+ void discovers_marker_and_loads_the_listed_class() {
+ // javaLoader is unused by discover(); the classpath carries the test fixture marker at
+ // src/test/resources/META-INF/dirigible/aot-test-mod/.compiled.
+ CompiledModuleClassProvider provider = new CompiledModuleClassProvider(null);
+
+ List discovered = provider.discover();
+
+ LoadedClass fixture = discovered.stream()
+ .filter(c -> c.fqn()
+ .equals(AotFixtureClass.class.getName()))
+ .findFirst()
+ .orElse(null);
+ assertNotNull(fixture, "the class listed in the .compiled marker must be discovered");
+ assertEquals("aot-test-mod", fixture.project(), "project is the path segment under META-INF/dirigible");
+ assertSame(AotFixtureClass.class, fixture.type(), "the class is loaded via the application classloader (no compile)");
+ }
+}
diff --git a/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/JavaLoaderTest.java b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/JavaLoaderTest.java
index 9aaafe799d1..2cf2f578b8b 100644
--- a/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/JavaLoaderTest.java
+++ b/components/engine/engine-java/src/test/java/org/eclipse/dirigible/engine/java/runtime/JavaLoaderTest.java
@@ -341,6 +341,34 @@ private Path classFile(String fqn) {
return tempDir.resolve(fqn.replace('.', '/') + ".class");
}
+ @Test
+ void install_compiled_modules_registers_via_consumers_and_survives_a_registry_rebuild() {
+ // An AOT-packaged compiled-module class already loaded by the application classloader — no
+ // runtime compile. installCompiledModules must surface it through the same consumer path.
+ Class> type = CompiledSample.class;
+ LoadedClass compiled = new LoadedClass("acme-employee", type.getName(), type, type.getClassLoader());
+
+ loader.installCompiledModules(List.of(compiled));
+
+ assertTrue(recording.loaded.stream()
+ .anyMatch(c -> c.fqn()
+ .equals(type.getName())),
+ "compiled-module class is registered through the consumer path");
+
+ // A registry rebuild that drops every authored source must NOT report the compiled module as
+ // removed — it survives via the union generation (registry + compiled).
+ JavaLoader.RebuildResult result = loader.rebuild(List.of());
+ assertFalse(result.unloadedFqns()
+ .contains(type.getName()),
+ "compiled module survives a registry rebuild that removes all authored sources");
+ }
+
+ /**
+ * Stand-in for an AOT-packaged compiled-module class already present on the application classpath.
+ */
+ public static class CompiledSample {
+ }
+
private static JavaLoader.ClientSource handlerSource(String fqn, String body) {
int dot = fqn.lastIndexOf('.');
String pkg = dot >= 0 ? fqn.substring(0, dot) : "";
diff --git a/components/engine/engine-java/src/test/resources/META-INF/dirigible/aot-test-mod/.compiled b/components/engine/engine-java/src/test/resources/META-INF/dirigible/aot-test-mod/.compiled
new file mode 100644
index 00000000000..4e0d137c35f
--- /dev/null
+++ b/components/engine/engine-java/src/test/resources/META-INF/dirigible/aot-test-mod/.compiled
@@ -0,0 +1,2 @@
+# AOT compiled-module marker (test fixture): top-level class binary names, one per line.
+org.eclipse.dirigible.engine.java.runtime.AotFixtureClass