diff --git a/.github/workflows/archetype-smoke.yml b/.github/workflows/archetype-smoke.yml
index 424545f63ce..c0bb92eda58 100644
--- a/.github/workflows/archetype-smoke.yml
+++ b/.github/workflows/archetype-smoke.yml
@@ -27,6 +27,7 @@ on:
- 'maven/integration-tests/cn1app-archetype-test.sh'
- 'maven/integration-tests/cn1app-staged-jar-test.sh'
- 'maven/integration-tests/cn1app-minimal-layout-test.sh'
+ - 'maven/integration-tests/backend-cn1lib-test.sh'
- 'scripts/initializr/common/src/main/resources/common-hosted-platform-profiles.xml'
- 'scripts/initializr/common/src/main/resources/backend-only-pom.xml'
# What a generated project stages for upload also depends on the
@@ -54,6 +55,7 @@ on:
- 'maven/integration-tests/cn1app-archetype-test.sh'
- 'maven/integration-tests/cn1app-staged-jar-test.sh'
- 'maven/integration-tests/cn1app-minimal-layout-test.sh'
+ - 'maven/integration-tests/backend-cn1lib-test.sh'
- 'scripts/initializr/common/src/main/resources/common-hosted-platform-profiles.xml'
- 'scripts/initializr/common/src/main/resources/backend-only-pom.xml'
# What a generated project stages for upload also depends on the
@@ -138,6 +140,15 @@ jobs:
run: |
cd maven/integration-tests
xvfb-run -a bash cn1app-archetype-test.sh
+ # A cn1lib with a server half, used by a generated server: the library's
+ # controller, bean, entity, repository and migration are wired by the
+ # server's build, on the JVM and natively.
+ - name: Check a cn1lib's backend part in a generated server
+ env:
+ CN1_BACKEND_PACKAGE_REQUIRED: '1'
+ run: |
+ cd maven/integration-tests
+ env -u DISPLAY -u WAYLAND_DISPLAY bash backend-cn1lib-test.sh
# The jar cn1:build would upload, for every target the generated build.sh
# offers. #5380 put ~300 MB of ffmpeg natives into every desktop upload and
# the build client refused them; nothing looked at this jar for two months.
diff --git a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m
index 7e22e11a7b8..15e2f3175b6 100644
--- a/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m
+++ b/Ports/iOSPort/nativeSources/CodenameOne_GLViewController.m
@@ -3402,12 +3402,40 @@ - (void)viewDidLayoutSubviews {
// never leaves landscape, so when the user stays in landscape its bounds never
// change and no layout pass runs to correct the published size. viewDidAppear
// and the transition's completion therefore re-read the real bounds as well.
+// Reads the window's safe-area insets into the globals the Java side asks for.
+// Answers whether any of them changed.
+- (BOOL)cn1ReadSafeAreaInsets {
+ UIEdgeInsets in = self.view.window.safeAreaInsets;
+ JAVA_INT l = (JAVA_INT)in.left * scaleValue;
+ JAVA_INT r = (JAVA_INT)in.right * scaleValue;
+ JAVA_INT t = (JAVA_INT)in.top * scaleValue;
+ JAVA_INT b = (JAVA_INT)in.bottom * scaleValue;
+ if (l == safeLeft && r == safeRight && t == safeTop && b == safeBottom) {
+ return NO;
+ }
+ safeLeft = l;
+ safeRight = r;
+ safeTop = t;
+ safeBottom = b;
+ return YES;
+}
+
- (void)cn1PublishViewSizeIfChanged {
// The same reason viewWillTransitionToSize: gives: iOS lays the view out
// at the opposite orientation for its background snapshot.
if (isAppSuspended || self.view == nil) {
return;
}
+ // The insets go stale the same way the size does. viewSafeAreaInsetsDidChange
+ // is not sent to a view that a presented controller covers, so a rotation
+ // under one -- an orientation-locked screen, a camera -- left the landscape
+ // insets in place after the app was back in portrait, and every form with a
+ // safe area was squeezed between the old notch margins.
+ if (self.view.window != nil && [self cn1ReadSafeAreaInsets]) {
+ [self cn1UpdateStatusBarTapProxyFrame];
+ lockDrawing = NO;
+ repaintUI();
+ }
CGSize sz = self.view.bounds.size;
int newW = (int)(sz.width * scaleValue);
int newH = (int)(sz.height * scaleValue);
@@ -5025,10 +5053,7 @@ - (void)viewSafeAreaInsetsDidChange {
#if TARGET_OS_OSX
#else
[super viewSafeAreaInsetsDidChange];
- safeLeft = (JAVA_INT)self.view.window.safeAreaInsets.left * scaleValue;
- safeRight = (JAVA_INT)self.view.window.safeAreaInsets.right * scaleValue;
- safeTop = (JAVA_INT)self.view.window.safeAreaInsets.top * scaleValue;
- safeBottom = (JAVA_INT)self.view.window.safeAreaInsets.bottom * scaleValue;
+ [self cn1ReadSafeAreaInsets];
// Status-bar tap proxy height tracks safeAreaInsets.top, so refresh it
// here so rotations and other safe-area changes keep the proxy aligned
diff --git a/docs/demos/common/src/main/snippets/developer-guide/backend-beans.xml b/docs/demos/common/src/main/snippets/developer-guide/backend-beans.xml
new file mode 100644
index 00000000000..f3346a4ec69
--- /dev/null
+++ b/docs/demos/common/src/main/snippets/developer-guide/backend-beans.xml
@@ -0,0 +1,9 @@
+// Generated from docs/developer-guide source blocks. Edit the guide snippets here, not inline.
+
+// tag::backend-beans-library-dependency[]
+
+ com.example
+ notelib-backend
+ 1.0
+
+// end::backend-beans-library-dependency[]
diff --git a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
index 19b80a59e65..67f84f275b3 100644
--- a/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
+++ b/docs/developer-guide/Advanced-Topics-Under-The-Hood.asciidoc
@@ -1081,6 +1081,8 @@ include::../demos/common/src/main/snippets/developer-guide/advanced-topics-under
Once the project is generated, add your cross-platform Java sources under `common/src/main/java`, CSS under `common/src/main/css`, and run `mvn install` from the project root to build. The build produces both a Maven artifact for pom-style consumption and a legacy `.cn1lib` file in `common/target` for users still on Ant.
+A library can also have a server half. The archetype's `backend` module holds classes for an application's server: its controllers, beans, entities, repositories and migrations are wired into any server that depends on the library, as if that server had compiled them. When the module has sources or resources, the `.cn1lib` carries them as `backend.zip`. See <>.
+
==== Build hints in cn1libs
Some cn1libs are pretty simple to install - just add them as a Maven dependency in `common/pom.xml`. For example, many of the more elaborate cn1libs need some pretty complex configurations. This is the case when native code is involved where you need to add permissions or plist entries for the various native platforms to get everything to work. This makes the cn1lib's helpful but less than seamless which is where you want to go.
@@ -1159,6 +1161,7 @@ The table below covers the files that can/should be a part of a cn1lib file:
| nativewin.zip | × | Native Windows sources if applicable
// vale-skip: Microsoft.FirstPerson: "ME" in "Java ME" is the Java Micro Edition platform abbreviation, not a first-person pronoun.
| nativeme.zip | × | Native Java ME sources if applicable
+| backend.zip | × | The library's server half, compiled, if it has one. See <>
|====
****
diff --git a/docs/developer-guide/Backend-Beans.asciidoc b/docs/developer-guide/Backend-Beans.asciidoc
index 10363fcbb48..bc9e526719c 100644
--- a/docs/developer-guide/Backend-Beans.asciidoc
+++ b/docs/developer-guide/Backend-Beans.asciidoc
@@ -393,6 +393,43 @@ A start-up that fails partway runs the destroy methods of the beans already
built, so a server a supervisor restarts doesn't leak what their constructors
opened.
+[[backend-libraries]]
+=== Controllers and beans from a library
+
+A server's wiring is generated while it builds, from the classes it compiles.
+A library can take part in that wiring: its `@RestController` classes get
+routes, its `@Component` and `@Service` classes become beans, its entities and
+repositories get their data access, and its `db/migration` scripts run, all as
+if the server had compiled them itself. The server's own code can inject the
+library's beans like any other.
+
+A jar opts in by carrying the empty resource
+`META-INF/codenameone/backend-library`. A jar without the marker is an ordinary
+dependency: its classes are on the classpath, but none of them are wired.
+
+A cn1lib carries its server half inside the `.cn1lib` file, as `backend.zip`
+beside the client parts:
+
+* A library made from the Maven cn1lib archetype has a `backend` module. Put
+ the server classes in `backend/src/main/java`; the module already holds the
+ marker. When the module has sources or resources of its own, the `.cn1lib`
+ includes it, and a server
+ that uses the library depends on the `-backend` artifact (or the
+ `backend` classifier of an installed `.cn1lib`):
++
+[source,xml]
+----
+include::../demos/common/src/main/snippets/developer-guide/backend-beans.xml[tag=backend-beans-library-dependency,indent=0]
+----
+* A Gradle cn1lib puts the server classes in `src/backend/java` and publishes
+ them as `-backend`, marked.
+* A library with no client side is a plain jar that depends on
+ `codenameone-backend` with `provided` scope and carries the marker.
+
+The library itself is never wired at its own build; only the server that uses
+it generates the routes and beans. When the server defines a class with the
+same name as one in a library, the server's class is used.
+
=== What the build refuses
Everything a container would discover at start-up is checked while the module
diff --git a/docs/developer-guide/Backend-Data.asciidoc b/docs/developer-guide/Backend-Data.asciidoc
index d15eab7a32c..a1f458d4418 100644
--- a/docs/developer-guide/Backend-Data.asciidoc
+++ b/docs/developer-guide/Backend-Data.asciidoc
@@ -408,6 +408,9 @@ name. The set gets its own history table, so its versions never collide with
the application's, and it runs before the application's scripts so they can
refer to its tables.
+A library jar marked as a backend library ships its own `db/migration`
+scripts the same way; see <>.
+
[source,java,role=cn1-no-try]
----
include::../demos/backend/src/main/java/com/codenameone/developerguide/backend/MigrationSnippets.java[tag=backend-migrations-library,indent=0]
diff --git a/docs/developer-guide/Gradle-Project-Workflow.asciidoc b/docs/developer-guide/Gradle-Project-Workflow.asciidoc
index c113e6f9a6e..1a3003fbe4f 100644
--- a/docs/developer-guide/Gradle-Project-Workflow.asciidoc
+++ b/docs/developer-guide/Gradle-Project-Workflow.asciidoc
@@ -334,6 +334,7 @@ The library is published in the same shape the Maven `cn1lib-archetype` produces
`N-common`, classifier `cn1css`:: The library's CSS, as a zip.
`N-`:: One jar per platform: compiled classes for `javase`, sources for the others, which the native builders compile.
`N-lib`:: A `pom` that applications depend on, with one `codename1.platform` profile per platform.
+`N-backend`:: The library's server half, compiled from `src/backend/java` and `src/backend/resources`, when there is one. A server that depends on it wires its controllers, beans and entities as its own; see <>.
Gradle cn1libs don't produce `.cn1lib` files.
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/AnnotationProcessing.java b/maven/build-engine/src/main/java/com/codename1/maven/AnnotationProcessing.java
index c5e06b4c0ea..bfd71fa6d95 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/AnnotationProcessing.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/AnnotationProcessing.java
@@ -109,6 +109,19 @@ public AnnotationProcessing withOwnClassDirectories(List directories) {
return this;
}
+ /// Whether backend libraries on the classpath are merged into the output
+ /// first; see [BackendLibraries].
+ private boolean mergeBackendLibraries;
+
+ /// Merges the backend libraries on the compile classpath into the output
+ /// directory before anything scans it, so their controllers, beans and
+ /// entities are wired as the module's own. For a module's main classes
+ /// only: its tests see the merged classes through the main output.
+ public AnnotationProcessing withBackendLibraries(boolean merge) {
+ this.mergeBackendLibraries = merge;
+ return this;
+ }
+
/// The compile classpath without [#ownClassDirectories].
private List dependencyClasspath() {
if (ownClassDirectories.isEmpty()) {
@@ -142,10 +155,28 @@ public void run() throws BuildExecutionException {
classpath.add(new File(element));
}
com.codename1.maven.annotations.JavaSourceCompiler.setProjectClasspath(classpath);
+ boolean processed = false;
try {
runProcessors();
+ processed = true;
} finally {
com.codename1.maven.annotations.JavaSourceCompiler.clearProjectClasspath();
+ // The processors rewrite merged classes in place; the record must
+ // describe what this build left -- a failed build too, since a
+ // processor may have woven some classes before another one threw --
+ // or the next build reads every rewritten library class as the
+ // module's own and never refreshes it.
+ if (mergeBackendLibraries) {
+ try {
+ BackendLibraries.restamp(outputDirectory);
+ } catch (IOException e) {
+ if (processed) {
+ throw new BuildExecutionException("Could not record the merged backend "
+ + "libraries in " + outputDirectory + ": " + e.getMessage(), e);
+ }
+ // Otherwise the processors' own failure is the one to report.
+ }
+ }
}
}
@@ -154,6 +185,27 @@ private void runProcessors() throws BuildExecutionException {
log.debug("cn1: nothing compiled at " + outputDirectory + " -- skipping annotation processing");
return;
}
+ if (new File(outputDirectory, BackendLibraries.MARKER).isFile()) {
+ // A backend library is wired by the server that uses it, as that
+ // server's own classes. Processed here as well, it would ship
+ // generated classes with fixed names -- cn1app.DaoBootstrap, the
+ // wiring record -- that collide with the server's.
+ log.info("cn1: " + outputDirectory + " is a backend library; it is processed "
+ + "by the server that depends on it");
+ return;
+ }
+ if (mergeBackendLibraries) {
+ try {
+ int merged = BackendLibraries.merge(compileClasspath, outputDirectory);
+ if (merged > 0) {
+ log.info("cn1: merged " + merged + " backend librar"
+ + (merged == 1 ? "y" : "ies") + " into " + outputDirectory);
+ }
+ } catch (IOException e) {
+ throw new BuildExecutionException("Could not merge the backend libraries into "
+ + outputDirectory + ": " + e.getMessage(), e);
+ }
+ }
List processors = loadProcessors();
if (processors.isEmpty()) {
log.debug("cn1: no AnnotationProcessor services registered -- nothing to do");
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/BackendLibraries.java b/maven/build-engine/src/main/java/com/codename1/maven/BackendLibraries.java
new file mode 100644
index 00000000000..fda684c03c8
--- /dev/null
+++ b/maven/build-engine/src/main/java/com/codename1/maven/BackendLibraries.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.maven;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/// Backend libraries: jars whose classes a server module builds as its own.
+///
+/// A server is wired at build time. Its routers, its bean wiring and the dao of
+/// every entity are generated from the annotated classes the build scans, and
+/// the build scans the module's own output. A library's `@RestController`,
+/// `@Service` or `@Entity` was therefore compiled, shipped and never wired: no
+/// route, no bean, no table.
+///
+/// A jar that carries [#MARKER] says it is meant to be wired by the server that
+/// uses it -- the backend part of a cn1lib (`backend.zip` inside the `.cn1lib`)
+/// or a backend-only library. Before anything scans the module, its classes and
+/// resources are copied into the module's output directory, so every processor,
+/// every bytecode rewrite and the native translation see one application and
+/// nothing has to know where a class came from. The library is not processed
+/// when it is built: generated wiring has fixed names (`cn1app.DaoBootstrap`),
+/// and a library's copy would collide with the application's.
+///
+/// The module's own classes always win. What was copied is listed in
+/// [#RECORD] with its size and checksum, so the next build replaces or removes
+/// exactly those files -- a library upgraded or dropped leaves nothing stale
+/// behind. A listed file whose content changed since is no longer the
+/// library's: the module compiled a class of that name over it, and it stays.
+public final class BackendLibraries {
+
+ /// The entry that marks a jar as a backend library. Its content is ignored.
+ public static final String MARKER = "META-INF/codenameone/backend-library";
+
+ /// What the last merge copied: one line per file, the output-relative path,
+ /// its size and its CRC-32, separated by tabs.
+ static final String RECORD = "META-INF/codenameone/backend-libraries.merged";
+
+ private BackendLibraries() {
+ }
+
+ /// Whether `element`, a classpath entry, is a backend library.
+ public static boolean isLibrary(File element) {
+ if (element.isDirectory()) {
+ return new File(element, MARKER).isFile();
+ }
+ if (!element.isFile()) {
+ return false;
+ }
+ try (ZipFile zip = new ZipFile(element)) {
+ return zip.getEntry(MARKER) != null;
+ } catch (IOException notAnArchive) {
+ return false;
+ }
+ }
+
+ /// Copies every backend library on `classpath` into `output`.
+ ///
+ /// @param classpath classpath elements, in classpath order; the first
+ /// library to define a path wins, as on a classpath
+ /// @param output the module's compiled classes
+ /// @return the number of libraries merged
+ public static int merge(List classpath, File output) throws IOException {
+ File record = new File(output, RECORD);
+ Map previous = read(record);
+ // Undo the last merge first: a file still listed there, with the
+ // content the merge wrote, is a library's, and whatever the libraries
+ // hold now is copied back below. One whose content changed was written
+ // since by the module's own compile, and is the module's.
+ for (Map.Entry e : previous.entrySet()) {
+ File stale = new File(output, e.getKey());
+ if (stale.isFile() && (e.getValue() == null || e.getValue().equals(stamp(stale)))
+ && !stale.delete()) {
+ throw new IOException("Could not remove " + stale);
+ }
+ }
+ Set merged = new LinkedHashSet();
+ int libraries = 0;
+ if (classpath != null) {
+ File own = canonical(output);
+ for (String path : classpath) {
+ File element = new File(path);
+ if (canonical(element).equals(own) || !isLibrary(element)) {
+ continue;
+ }
+ libraries++;
+ if (element.isDirectory()) {
+ copyDirectory(element, element, output, merged);
+ } else {
+ copyArchive(element, output, merged);
+ }
+ }
+ }
+ if (merged.isEmpty()) {
+ if (record.isFile() && !record.delete()) {
+ throw new IOException("Could not remove " + record);
+ }
+ return libraries;
+ }
+ mkdirs(record.getParentFile());
+ try (Writer out = new OutputStreamWriter(new FileOutputStream(record), StandardCharsets.UTF_8)) {
+ for (String path : merged) {
+ out.write(path);
+ out.write('\t');
+ out.write(stamp(new File(output, path)));
+ out.write('\n');
+ }
+ }
+ return libraries;
+ }
+
+ /// Whether a library entry is copied: class files and resources, but not the
+ /// jar's own metadata, which describes the library and not the application.
+ private static boolean wanted(String name) {
+ if (name.startsWith("META-INF/")) {
+ // A service registration is copied like any resource; when the
+ // module registers the same service, the module's file wins.
+ return !name.equals("META-INF/MANIFEST.MF") && !name.equals(MARKER)
+ && !name.startsWith("META-INF/maven/") && !name.endsWith(".SF")
+ && !name.endsWith(".RSA") && !name.endsWith(".DSA")
+ && !name.equals(RECORD);
+ }
+ return !name.equals("module-info.class");
+ }
+
+ private static void copyArchive(File jar, File output, Set merged) throws IOException {
+ try (ZipFile zip = new ZipFile(jar)) {
+ Enumeration extends ZipEntry> entries = zip.entries();
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (entry.isDirectory() || !wanted(name) || name.contains("..")) {
+ continue;
+ }
+ File target = new File(output, name);
+ if (target.exists()) {
+ // The module's own file, or an earlier library's.
+ continue;
+ }
+ mkdirs(target.getParentFile());
+ try (InputStream in = zip.getInputStream(entry)) {
+ copy(in, target);
+ }
+ merged.add(name);
+ }
+ }
+ }
+
+ private static void copyDirectory(File root, File dir, File output, Set merged)
+ throws IOException {
+ File[] children = dir.listFiles();
+ if (children == null) {
+ return;
+ }
+ for (File child : children) {
+ if (child.isDirectory()) {
+ copyDirectory(root, child, output, merged);
+ continue;
+ }
+ String name = root.toURI().relativize(child.toURI()).getPath();
+ if (!wanted(name)) {
+ continue;
+ }
+ File target = new File(output, name);
+ if (target.exists()) {
+ continue;
+ }
+ mkdirs(target.getParentFile());
+ try (InputStream in = new FileInputStream(child)) {
+ copy(in, target);
+ }
+ merged.add(name);
+ }
+ }
+
+ /// A file's size and CRC-32, as the record stores them.
+ private static String stamp(File file) throws IOException {
+ CRC32 crc = new CRC32();
+ try (InputStream in = new FileInputStream(file)) {
+ byte[] buffer = new byte[8192];
+ int n;
+ while ((n = in.read(buffer)) > 0) {
+ crc.update(buffer, 0, n);
+ }
+ }
+ return file.length() + "\t" + Long.toHexString(crc.getValue());
+ }
+
+ /// The record's paths, each with its stamp; null for a line written
+ /// without one.
+ private static Map read(File record) throws IOException {
+ Map out = new LinkedHashMap();
+ if (!record.isFile()) {
+ return out;
+ }
+ try (BufferedReader in = new BufferedReader(new InputStreamReader(
+ new FileInputStream(record), StandardCharsets.UTF_8))) {
+ String line;
+ while ((line = in.readLine()) != null) {
+ int tab = line.indexOf('\t');
+ String path = tab < 0 ? line : line.substring(0, tab);
+ if (!path.isEmpty() && !path.contains("..")) {
+ out.put(path, tab < 0 ? null : line.substring(tab + 1));
+ }
+ }
+ }
+ return out;
+ }
+
+ /// Stamps every file the last merge into `output` copied with its current
+ /// content. The build calls this once it has finished rewriting classes --
+ /// bean weaving and the ORM enhancer change merged classes in place -- so
+ /// the next merge recognizes them as the library's. A file the stamp at
+ /// merge time no longer matched before this runs would be the module's, but
+ /// nothing in a build writes a class over a merged one between the merge
+ /// and here except those rewrites.
+ public static void restamp(File output) throws IOException {
+ File record = new File(output, RECORD);
+ Map listed = read(record);
+ if (listed.isEmpty()) {
+ return;
+ }
+ try (Writer out = new OutputStreamWriter(new FileOutputStream(record), StandardCharsets.UTF_8)) {
+ for (String path : listed.keySet()) {
+ File file = new File(output, path);
+ if (!file.isFile()) {
+ continue;
+ }
+ out.write(path);
+ out.write('\t');
+ out.write(stamp(file));
+ out.write('\n');
+ }
+ }
+ }
+
+ /// What the last merge into `output` copied, as output-relative paths.
+ public static Set mergedPaths(File output) throws IOException {
+ return new LinkedHashSet(read(new File(output, RECORD)).keySet());
+ }
+
+ /// The classpath entries that are backend libraries.
+ public static List libraries(List classpath) {
+ List out = new ArrayList();
+ if (classpath != null) {
+ for (String path : classpath) {
+ if (isLibrary(new File(path))) {
+ out.add(path);
+ }
+ }
+ }
+ return out;
+ }
+
+ private static void copy(InputStream in, File target) throws IOException {
+ try (OutputStream out = new FileOutputStream(target)) {
+ byte[] buffer = new byte[8192];
+ int n;
+ while ((n = in.read(buffer)) > 0) {
+ out.write(buffer, 0, n);
+ }
+ }
+ }
+
+ private static void mkdirs(File dir) throws IOException {
+ if (dir != null && !dir.isDirectory() && !dir.mkdirs()) {
+ throw new IOException("Could not create " + dir);
+ }
+ }
+
+ private static File canonical(File file) {
+ try {
+ return file.getCanonicalFile();
+ } catch (IOException e) {
+ return file.getAbsoluteFile();
+ }
+ }
+}
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/BackendPackager.java b/maven/build-engine/src/main/java/com/codename1/maven/BackendPackager.java
index 5c2b86194f6..d4d53c5133f 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/BackendPackager.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/BackendPackager.java
@@ -268,11 +268,31 @@ public File execute() throws BuildExecutionException {
prepareSources(work, runtimeVersion);
compile(jdk, javaApi, runtimeSources, classes);
+ // Backend libraries are wired as the module's own classes, exactly as
+ // process-annotations does on the JVM; see BackendLibraries.
+ try {
+ BackendLibraries.merge(compileClasspathWithoutRuntime(), classes);
+ } catch (IOException ex) {
+ throw new BuildExecutionException("Could not merge the backend libraries: "
+ + ex.getMessage(), ex);
+ }
// Generated code stays at Java 8, but its compiler must be able to read
// the application's newer class files (including when Maven runs on 8).
+ boolean generated = false;
try (JavaSourceCompiler.CompilerScope ignored = JavaSourceCompiler.useJdk(jdk)) {
generateControllers(classes, work);
afterGenerate(jdk, javaApi, classes, work);
+ generated = true;
+ } finally {
+ // After the rewrites above, failed or not; see AnnotationProcessing.run.
+ try {
+ BackendLibraries.restamp(classes);
+ } catch (IOException ex) {
+ if (generated) {
+ throw new BuildExecutionException("Could not record the merged backend "
+ + "libraries: " + ex.getMessage(), ex);
+ }
+ }
}
requireMainClass(classes);
translate(jdk, compilerJar, javaApi, classes, nativeSources, translated,
@@ -843,7 +863,22 @@ List stageDependencyClasses(List classpath, File staged,
boolean any = false;
for (int i = 0; i < classpath.size(); i++) {
File element = new File(classpath.get(i));
- if (element.isDirectory()) {
+ if (BackendLibraries.isLibrary(element)) {
+ // Its classes were merged into the module's own; only its
+ // natives are taken from here, so no class is translated twice.
+ // Tested before the directory branch: a sibling reactor module's
+ // target/classes is a library too.
+ if (element.isDirectory()) {
+ File natives = new File(element, "cn1-native");
+ if (natives.isDirectory()) {
+ mkdirs(nativeSources);
+ copyDirectoryFirstWins(natives, nativeSources);
+ }
+ } else {
+ File scratch = new File(staged.getParentFile(), "backend-library-" + i);
+ unzip(element, scratch, nativeSources, true);
+ }
+ } else if (element.isDirectory()) {
copyDirectoryFirstWins(element, staged, nativeSources);
any = true;
} else if (element.isFile()) {
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/annotations/AnnotatedClass.java b/maven/build-engine/src/main/java/com/codename1/maven/annotations/AnnotatedClass.java
index ced17c086b3..035874b5fd0 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/annotations/AnnotatedClass.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/annotations/AnnotatedClass.java
@@ -117,6 +117,15 @@ private static Set collectAllDescriptors(
/// guess and this is not.
public String getSourceFile() { return sourceFile; }
+ private boolean fromBackendLibrary;
+
+ /// Whether a backend library contributed this class to the module's output.
+ /// Such a class is the module's to wire, though no source of the module
+ /// declares it.
+ public boolean isFromBackendLibrary() { return fromBackendLibrary; }
+
+ void setFromBackendLibrary(boolean fromBackendLibrary) { this.fromBackendLibrary = fromBackendLibrary; }
+
/**
* The name to write in generated JAVA SOURCE.
*
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/annotations/ClassScanner.java b/maven/build-engine/src/main/java/com/codename1/maven/annotations/ClassScanner.java
index a38844a06ca..a794d8b573e 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/annotations/ClassScanner.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/annotations/ClassScanner.java
@@ -73,9 +73,32 @@ public static Map scan(File root) throws ProcessingExcep
Map out = new LinkedHashMap();
if (root == null || !root.isDirectory()) return out;
scan(root, out);
+ markLibraryClasses(root, out);
return out;
}
+ /// Marks what a backend library contributed to `root` (see
+ /// `com.codename1.maven.BackendLibraries`): those classes have no source
+ /// in the module, and the checks that drop a class whose source was
+ /// deleted must not read them as stale.
+ private static void markLibraryClasses(File root, Map out)
+ throws ProcessingException {
+ java.util.Set merged;
+ try {
+ merged = com.codename1.maven.BackendLibraries.mergedPaths(root);
+ } catch (IOException e) {
+ throw new ProcessingException("Could not read the backend library record under " + root, e);
+ }
+ for (String path : merged) {
+ if (path.endsWith(".class")) {
+ AnnotatedClass cls = out.get(path.substring(0, path.length() - ".class".length()));
+ if (cls != null) {
+ cls.setFromBackendLibrary(true);
+ }
+ }
+ }
+ }
+
private static void scan(File dir, Map out) throws ProcessingException {
File[] children = dir.listFiles();
if (children == null) return;
diff --git a/maven/build-engine/src/main/java/com/codename1/maven/processors/BuildHintAnnotationProcessor.java b/maven/build-engine/src/main/java/com/codename1/maven/processors/BuildHintAnnotationProcessor.java
index ee68b0cfdda..424b40efc02 100644
--- a/maven/build-engine/src/main/java/com/codename1/maven/processors/BuildHintAnnotationProcessor.java
+++ b/maven/build-engine/src/main/java/com/codename1/maven/processors/BuildHintAnnotationProcessor.java
@@ -322,6 +322,11 @@ public static boolean hasBackingSource(AnnotatedClass cls, List compileS
/// forever and failing its placement check on every incremental build.
public static boolean hasBackingSource(AnnotatedClass cls, List compileSourceRoots,
String sourceEncoding) {
+ if (cls.isFromBackendLibrary()) {
+ // A backend library's class, merged into the module: its source is
+ // the library's, and it is as live as the dependency.
+ return true;
+ }
List roots = compileSourceRoots;
if (roots == null || roots.isEmpty()) {
return true;
diff --git a/maven/build-engine/src/test/java/com/codename1/maven/BackendLibrariesTest.java b/maven/build-engine/src/test/java/com/codename1/maven/BackendLibrariesTest.java
new file mode 100644
index 00000000000..b5289a87b0d
--- /dev/null
+++ b/maven/build-engine/src/test/java/com/codename1/maven/BackendLibrariesTest.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (c) 2026, Codename One and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Codename One designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Codename One through http://www.codenameone.com/ if you
+ * need additional information or have any questions.
+ */
+package com.codename1.maven;
+
+import com.codename1.backend.HttpServer;
+import com.codename1.build.SystemStreamLog;
+import com.codename1.maven.annotations.JavaSourceCompiler;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/// A server wires the controllers, beans and entities of a backend library as
+/// its own; see [BackendLibraries].
+public class BackendLibrariesTest {
+
+ @Rule
+ public TemporaryFolder tmp = new TemporaryFolder();
+
+ private static final String LIBRARY_CONTROLLER = "package com.lib;\n"
+ + "import com.codename1.backend.annotations.*;\n"
+ + "@RestController\n"
+ + "@RequestMapping(\"/lib\")\n"
+ + "public class LibNotes {\n"
+ + " @GetMapping(\"/ping\")\n"
+ + " public String ping() { return \"pong\"; }\n"
+ + "}\n";
+
+ private static final String LIBRARY_ENTITY = "package com.lib;\n"
+ + "import com.codename1.annotations.*;\n"
+ + "@Entity(table=\"lib_note\")\n"
+ + "public class LibNote { @Id public long id; public String text; }\n";
+
+ private static final String LIBRARY_SERVICE = "package com.lib;\n"
+ + "@com.codename1.backend.annotations.Component\n"
+ + "public class LibService { public String name() { return \"lib\"; } }\n";
+
+ private static final String APP_CONTROLLER = "package com.app;\n"
+ + "import com.codename1.backend.annotations.*;\n"
+ + "@RestController\n"
+ + "@RequestMapping(\"/app\")\n"
+ + "public class AppNotes {\n"
+ + " private final com.lib.LibService service;\n"
+ + " public AppNotes(com.lib.LibService service) { this.service = service; }\n"
+ + " @GetMapping(\"/hello\")\n"
+ + " public String hello() { return \"hi\"; }\n"
+ + "}\n";
+
+ @Test
+ public void aLibrarysControllerAndEntityAreWiredIntoTheServer() throws Exception {
+ File library = library(true);
+ File classes = app(library);
+ process(classes, library);
+
+ assertTrue("library controller merged", new File(classes, "com/lib/LibNotes.class").isFile());
+ assertTrue("library router generated into the server",
+ new File(classes, "com/lib/LibNotesRouter.class").isFile());
+ assertTrue("the server's own router", new File(classes, "com/app/AppNotesRouter.class").isFile());
+ assertTrue("library entity registered with the server's dao bootstrap",
+ contains(new File(classes, "cn1app/BackendDaoBootstrap.class"), "com/lib/LibNote"));
+ assertFalse("the marker describes the library, not the server",
+ new File(classes, BackendLibraries.MARKER).exists());
+ }
+
+ @Test
+ public void anUnmarkedJarIsLeftOnTheClasspath() throws Exception {
+ File library = library(false);
+ File classes = app(library);
+ try {
+ process(classes, library);
+ fail("an unmarked jar's @Component was wired");
+ } catch (com.codename1.build.BuildFailureException expected) {
+ assertTrue(expected.getMessage(), expected.getMessage().contains("com.lib.LibService"));
+ }
+ assertFalse(new File(classes, "com/lib/LibNotes.class").exists());
+ assertFalse(new File(classes, "com/lib/LibNotesRouter.class").exists());
+ }
+
+ @Test
+ public void theModulesOwnClassWins() throws Exception {
+ File library = library(true);
+ File classes = app(library);
+ File own = new File(classes, "com/lib/LibNote.class");
+ mkdirs(own.getParentFile());
+ byte[] mine = "mine".getBytes(StandardCharsets.UTF_8);
+ Files.write(own.toPath(), mine);
+ BackendLibraries.merge(Collections.singletonList(library.getAbsolutePath()), classes);
+ assertTrue(Arrays.equals(mine, Files.readAllBytes(own.toPath())));
+ // Merged again, the module's file is still the module's.
+ BackendLibraries.merge(Collections.singletonList(library.getAbsolutePath()), classes);
+ assertTrue(Arrays.equals(mine, Files.readAllBytes(own.toPath())));
+ }
+
+ @Test
+ public void aClassTheModuleCompiledOverAMergedOneIsKept() throws Exception {
+ File library = library(true);
+ File classes = app(library);
+ List cp = Collections.singletonList(library.getAbsolutePath());
+ BackendLibraries.merge(cp, classes);
+ File merged = new File(classes, "com/lib/LibNote.class");
+ assertTrue(merged.isFile());
+ // The module adds its own class of that name; javac writes over the copy.
+ byte[] mine = "mine".getBytes(StandardCharsets.UTF_8);
+ Files.write(merged.toPath(), mine);
+ BackendLibraries.merge(cp, classes);
+ assertTrue("the module's class survives the undo of the last merge",
+ Arrays.equals(mine, Files.readAllBytes(merged.toPath())));
+ BackendLibraries.merge(cp, classes);
+ assertTrue(Arrays.equals(mine, Files.readAllBytes(merged.toPath())));
+ }
+
+ @Test
+ public void aMergedClassTheBuildRewroteIsStillTheLibrarys() throws Exception {
+ File library = library(true);
+ File classes = app(library);
+ List cp = Collections.singletonList(library.getAbsolutePath());
+ BackendLibraries.merge(cp, classes);
+ File merged = new File(classes, "com/lib/LibNote.class");
+ byte[] original = Files.readAllBytes(merged.toPath());
+ // Weaving rewrites the copy in place; the build then restamps.
+ Files.write(merged.toPath(), "woven".getBytes(StandardCharsets.UTF_8));
+ BackendLibraries.restamp(classes);
+ BackendLibraries.merge(cp, classes);
+ assertTrue("the next merge replaces it with the library's class",
+ Arrays.equals(original, Files.readAllBytes(merged.toPath())));
+ }
+
+ @Test
+ public void aDroppedLibraryLeavesNothingBehind() throws Exception {
+ File library = library(true);
+ File classes = app(library);
+ assertEquals(1, BackendLibraries.merge(Collections.singletonList(library.getAbsolutePath()), classes));
+ assertTrue(new File(classes, "com/lib/LibNotes.class").isFile());
+ assertEquals(0, BackendLibraries.merge(Collections.emptyList(), classes));
+ assertFalse(new File(classes, "com/lib/LibNotes.class").exists());
+ assertFalse(new File(classes, BackendLibraries.RECORD).exists());
+ assertTrue("the server's own classes stay", new File(classes, "com/app/AppNotes.class").isFile());
+ }
+
+ @Test
+ public void aLibraryModuleIsNotProcessedItself() throws Exception {
+ File classes = tmp.newFolder();
+ compile(classes, "com.lib.LibNotes", LIBRARY_CONTROLLER);
+ File marker = new File(classes, BackendLibraries.MARKER);
+ mkdirs(marker.getParentFile());
+ Files.write(marker.toPath(), new byte[0]);
+ process(classes, null);
+ assertFalse(new File(classes, "com/lib/LibNotesRouter.class").exists());
+ }
+
+ private File appSources;
+
+ private File app(File library) throws Exception {
+ File classes = tmp.newFolder();
+ List classpath = new ArrayList(backendClasspath());
+ classpath.add(library);
+ JavaSourceCompiler.compile(Collections.singletonMap("com.app.AppNotes", APP_CONTROLLER),
+ classes, classpath);
+ appSources = tmp.newFolder();
+ File source = new File(appSources, "com/app/AppNotes.java");
+ mkdirs(source.getParentFile());
+ Files.write(source.toPath(), APP_CONTROLLER.getBytes(StandardCharsets.UTF_8));
+ return classes;
+ }
+
+ /// A library jar, marked as a backend library or not.
+ private File library(boolean marked) throws Exception {
+ File classes = tmp.newFolder();
+ Map sources = new LinkedHashMap();
+ sources.put("com.lib.LibNotes", LIBRARY_CONTROLLER);
+ sources.put("com.lib.LibNote", LIBRARY_ENTITY);
+ sources.put("com.lib.LibService", LIBRARY_SERVICE);
+ JavaSourceCompiler.compile(sources, classes, backendClasspath());
+ File jar = new File(tmp.newFolder(), "library.jar");
+ try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(jar))) {
+ if (marked) {
+ out.putNextEntry(new ZipEntry(BackendLibraries.MARKER));
+ out.closeEntry();
+ }
+ add(out, classes, classes);
+ }
+ return jar;
+ }
+
+ private void compile(File classes, String name, String source) throws Exception {
+ JavaSourceCompiler.compile(Collections.singletonMap(name, source), classes, backendClasspath());
+ }
+
+ private void process(File classes, File library) throws Exception {
+ List classpath = new ArrayList();
+ for (File f : backendClasspath()) {
+ classpath.add(f.getAbsolutePath());
+ }
+ if (library != null) {
+ classpath.add(library.getAbsolutePath());
+ }
+ // A real source root, as Maven passes one: with none, every class reads
+ // as having its source, and a library class -- whose source is not the
+ // module's -- would pass for the wrong reason.
+ File sources = appSources != null ? appSources : tmp.newFolder();
+ new AnnotationProcessing(new SystemStreamLog(), classes, tmp.newFolder(), tmp.newFolder(),
+ new Properties(), null, Collections.singletonList(sources.getAbsolutePath()),
+ "UTF-8", classpath)
+ .withBackendLibraries(true)
+ .run();
+ }
+
+ private static void add(ZipOutputStream out, File root, File dir) throws Exception {
+ File[] children = dir.listFiles();
+ if (children == null) {
+ return;
+ }
+ for (File child : children) {
+ if (child.isDirectory()) {
+ add(out, root, child);
+ continue;
+ }
+ out.putNextEntry(new ZipEntry(root.toURI().relativize(child.toURI()).getPath()));
+ try (FileInputStream in = new FileInputStream(child)) {
+ byte[] buffer = new byte[8192];
+ int n;
+ while ((n = in.read(buffer)) > 0) {
+ out.write(buffer, 0, n);
+ }
+ }
+ out.closeEntry();
+ }
+ }
+
+ private static boolean contains(File file, String text) throws Exception {
+ return file.isFile() && new String(Files.readAllBytes(file.toPath()),
+ StandardCharsets.ISO_8859_1).contains(text);
+ }
+
+ private static void mkdirs(File dir) {
+ assertTrue(dir.isDirectory() || dir.mkdirs());
+ }
+
+ private static List backendClasspath() throws Exception {
+ URL url = HttpServer.class.getProtectionDomain().getCodeSource().getLocation();
+ return Arrays.asList(new File(url.toURI()));
+ }
+}
diff --git a/maven/cn1app-archetype/src/main/resources/archetype-resources/backend/pom.xml b/maven/cn1app-archetype/src/main/resources/archetype-resources/backend/pom.xml
index 9c610c4059a..3a9400427b5 100644
--- a/maven/cn1app-archetype/src/main/resources/archetype-resources/backend/pom.xml
+++ b/maven/cn1app-archetype/src/main/resources/archetype-resources/backend/pom.xml
@@ -84,6 +84,8 @@
5.9.3
test
+
+
diff --git a/maven/cn1lib-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/maven/cn1lib-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
index ac99b187955..7d4612cdb89 100644
--- a/maven/cn1lib-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
+++ b/maven/cn1lib-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -78,6 +78,22 @@
+
+
+
+ src/main/java
+
+ **/*.java
+
+
+
+ src/main/resources
+
+ **/*
+
+
+
+
diff --git a/maven/cn1lib-archetype/src/main/resources/archetype-resources/backend/pom.xml b/maven/cn1lib-archetype/src/main/resources/archetype-resources/backend/pom.xml
new file mode 100644
index 00000000000..24156c39701
--- /dev/null
+++ b/maven/cn1lib-archetype/src/main/resources/archetype-resources/backend/pom.xml
@@ -0,0 +1,40 @@
+
+
+ 4.0.0
+
+ ${groupId}
+ ${rootArtifactId}
+ ${version}
+
+ ${groupId}
+ ${artifactId}
+ ${version}
+
+ ${artifactId}
+
+
+
+ UTF-8
+
+
+
+
+
+ com.codenameone
+ codenameone-backend
+ provided
+
+
+
diff --git a/maven/cn1lib-archetype/src/main/resources/archetype-resources/backend/src/main/java/.gitignore b/maven/cn1lib-archetype/src/main/resources/archetype-resources/backend/src/main/java/.gitignore
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/maven/cn1lib-archetype/src/main/resources/archetype-resources/backend/src/main/resources/META-INF/codenameone/backend-library b/maven/cn1lib-archetype/src/main/resources/archetype-resources/backend/src/main/resources/META-INF/codenameone/backend-library
new file mode 100644
index 00000000000..0f7b6bda85b
--- /dev/null
+++ b/maven/cn1lib-archetype/src/main/resources/archetype-resources/backend/src/main/resources/META-INF/codenameone/backend-library
@@ -0,0 +1,2 @@
+# Marks this jar as a Codename One backend library: a server that depends on it
+# wires its @RestController, @Service and @Entity classes as the server's own.
diff --git a/maven/cn1lib-archetype/src/main/resources/archetype-resources/pom.xml b/maven/cn1lib-archetype/src/main/resources/archetype-resources/pom.xml
index 48c8c146d32..e0e0e5c1675 100644
--- a/maven/cn1lib-archetype/src/main/resources/archetype-resources/pom.xml
+++ b/maven/cn1lib-archetype/src/main/resources/archetype-resources/pom.xml
@@ -63,6 +63,8 @@
1.8
+
+ backend
common
android
ios
@@ -89,6 +91,11 @@
codenameone-javase
${cn1.version}
+
+ com.codenameone
+ codenameone-backend
+ ${cn1.version}
+
org.jetbrains.kotlin
kotlin-stdlib
diff --git a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/BackendSupport.java b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/BackendSupport.java
index d8097f2a79a..7764898fed0 100644
--- a/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/BackendSupport.java
+++ b/maven/codenameone-gradle-plugin/src/main/java/com/codename1/gradle/BackendSupport.java
@@ -86,6 +86,18 @@ static Provider webAppDirectory(Project project)
private BackendSupport() {
}
+ /// A named class rather than a method reference, so the configuration
+ /// cache can serialize the filtered input it belongs to.
+ private static final class IsBackendLibrary implements org.gradle.api.specs.Spec,
+ java.io.Serializable {
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ public boolean isSatisfiedBy(File element) {
+ return com.codename1.maven.BackendLibraries.isLibrary(element);
+ }
+ }
+
static void apply(final Project project, final ProjectLayout layout, final CodenameOneExtension ext,
final Provider