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
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>css</goal>
<!-- Scans the project's compiled bytecode for
@Route declarations and generates the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@
</goals>
</execution>
<execution>
<id>cn1-compliance-check</id>
<id>cn1-bytecode-compliance</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
</goals>
</execution>
<execution>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -81,6 +84,7 @@ public class BytecodeComplianceMojo extends AbstractCN1Mojo {

private File complianceOutputFile;
private InvocationRewriteSummary lastInvocationRewriteSummary = new InvocationRewriteSummary();
private URLClassLoader validationClassLoader;

private static Map<MethodRef, MethodRef> createInvocationRewriteRules() {
Map<MethodRef, MethodRef> rules = new LinkedHashMap<MethodRef, MethodRef>();
Expand Down Expand Up @@ -318,20 +322,24 @@ private InvocationRewriteSummary applyInvocationRewrites(File outputDir) throws
InvocationRewriteSummary summary = new InvocationRewriteSummary();
List<File> classFiles = new ArrayList<File>();
collectClassFiles(outputDir, classFiles);
for (File classFile : classFiles) {
try {
byte[] originalBytes = FileUtils.readFileToByteArray(classFile);
InvocationRewriteResult rewriteResult = rewriteClassInvocations(originalBytes);
if (rewriteResult.rewrittenCallsites > 0) {
validateClass(rewriteResult.bytes, classFile);
FileUtils.writeByteArrayToFile(classFile, rewriteResult.bytes);
summary.rewrittenClasses++;
summary.rewrittenCallsites += rewriteResult.rewrittenCallsites;
getLog().info("Applied " + rewriteResult.rewrittenCallsites + " invocation rewrite(s) in " + classFile.getAbsolutePath());
try {
for (File classFile : classFiles) {
try {
byte[] originalBytes = FileUtils.readFileToByteArray(classFile);
InvocationRewriteResult rewriteResult = rewriteClassInvocations(originalBytes);
if (rewriteResult.rewrittenCallsites > 0) {
validateClass(rewriteResult.bytes, classFile, outputDir);
FileUtils.writeByteArrayToFile(classFile, rewriteResult.bytes);
summary.rewrittenClasses++;
summary.rewrittenCallsites += rewriteResult.rewrittenCallsites;
getLog().info("Applied " + rewriteResult.rewrittenCallsites + " invocation rewrite(s) in " + classFile.getAbsolutePath());
}
} catch (IOException ex) {
throw new MojoExecutionException("Failed to rewrite invocations for " + classFile, ex);
}
} catch (IOException ex) {
throw new MojoExecutionException("Failed to rewrite invocations for " + classFile, ex);
}
} finally {
closeValidationClassLoader();
}
return summary;
}
Expand Down Expand Up @@ -364,21 +372,85 @@ public void visitMethodInsn(int opcode, String owner, String methodName, String
return result;
}

private void validateClass(byte[] classBytes, File classFile) throws MojoExecutionException {
private void validateClass(byte[] classBytes, File classFile, File outputDir) throws MojoExecutionException {
ClassLoader loader = getValidationClassLoader(outputDir);
try {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
CheckClassAdapter.verify(new ClassReader(classBytes), false, printWriter);
CheckClassAdapter.verify(new ClassReader(classBytes), loader, false, printWriter);
printWriter.flush();
String validationOutput = stringWriter.toString().trim();
if (!validationOutput.isEmpty()) {
if (isUnresolvableTypeOutput(validationOutput)) {
getLog().debug("Skipping deep verification for " + classFile.getName()
+ ": referenced type(s) not on classpath. Output: " + validationOutput);
return;
}
throw new MojoExecutionException("Bytecode validation failed for " + classFile + ": " + validationOutput);
}
} catch (RuntimeException ex) {
if (isUnresolvableTypeCause(ex)) {
getLog().debug("Skipping deep verification for " + classFile.getName()
+ ": referenced type(s) not on classpath (" + ex.getMessage() + ").");
return;
}
throw new MojoExecutionException("Bytecode validation failed for " + classFile, ex);
}
}

private ClassLoader getValidationClassLoader(File outputDir) {
if (validationClassLoader != null) {
return validationClassLoader;
}
List<URL> urls = new ArrayList<URL>();
try {
if (outputDir != null && outputDir.isDirectory()) {
urls.add(outputDir.toURI().toURL());
}
if (project != null) {
for (File jar : getDependencyJarsForScanning()) {
if (jar != null && jar.exists()) {
urls.add(jar.toURI().toURL());
}
}
}
} catch (MalformedURLException ex) {
getLog().debug("Failed to assemble validation classloader URLs; falling back to plugin classloader.", ex);
return getClass().getClassLoader();
}
validationClassLoader = new URLClassLoader(urls.toArray(new URL[0]), getClass().getClassLoader());
return validationClassLoader;
}

private void closeValidationClassLoader() {
if (validationClassLoader != null) {
try {
validationClassLoader.close();
} catch (IOException ex) {
getLog().debug("Failed to close validation classloader", ex);
}
validationClassLoader = null;
}
}

private static boolean isUnresolvableTypeCause(Throwable ex) {
Throwable t = ex;
while (t != null) {
if (t instanceof ClassNotFoundException || t instanceof TypeNotPresentException || t instanceof NoClassDefFoundError) {
return true;
}
t = t.getCause();
}
return false;
}

private static boolean isUnresolvableTypeOutput(String output) {
return output.contains("ClassNotFoundException")
|| output.contains("TypeNotPresentException")
|| output.contains("NoClassDefFoundError")
|| output.contains(" not present");
}

private List<Violation> scanProjectClasses(File outputDir, final Map<String, ClassMetadata> allowedIndex, final Map<String, ClassMetadata> projectAndDependencyIndex) throws MojoExecutionException {
List<File> classFiles = new ArrayList<File>();
collectClassFiles(outputDir, classFiles);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import org.apache.maven.plugins.annotations.ResolutionScope;

/**
* @deprecated Use {@link BytecodeComplianceMojo}. This goal is kept as a backward-compatible alias.
* @deprecated Renamed to the bytecode-compliance goal. This alias is kept for backward compatibility with older project POMs.
*/
@Deprecated
@Mojo(name = "compliance-check", defaultPhase = LifecyclePhase.PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.TEST)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</process-resources>
<compile>
org.apache.maven.plugins:maven-compiler-plugin:compile,
com.codenameone:codenameone-maven-plugin:compliance-check
com.codenameone:codenameone-maven-plugin:bytecode-compliance
</compile>
<process-test-resources>
org.apache.maven.plugins:maven-resources-plugin:testResources
Expand Down
2 changes: 1 addition & 1 deletion maven/tests/core-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>compile-javase-sources</goal>
</goals>
</execution>
Expand Down
2 changes: 1 addition & 1 deletion maven/tests/signindemo/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>compile-javase-sources</goal>
</goals>
</execution>
Expand Down
2 changes: 1 addition & 1 deletion maven/tests/testnativeinterfaces/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>compile-javase-sources</goal>
</goals>
</execution>
Expand Down
2 changes: 1 addition & 1 deletion scripts/cn1playground/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>css</goal>
<!--<goal>compile-javase-sources</goal>-->
</goals>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>css</goal>
<!--<goal>compile-javase-sources</goal>-->
</goals>
Expand Down
2 changes: 1 addition & 1 deletion scripts/hellocodenameone/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>css</goal>
<goal>process-annotations</goal>
<!--<goal>compile-javase-sources</goal>-->
Expand Down
2 changes: 1 addition & 1 deletion scripts/initializr/common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -366,7 +366,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>css</goal>
<!--<goal>compile-javase-sources</goal>-->
</goals>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>css</goal>
<!--<goal>compile-javase-sources</goal>-->
</goals>
Expand Down
2 changes: 1 addition & 1 deletion scripts/initializr/common/src/main/resources/grub-pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>css</goal>
<!--<goal>compile-javase-sources</goal>-->
</goals>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>css</goal>
<!--<goal>compile-javase-sources</goal>-->
</goals>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ This project targets **Java 17** (`<source>17</source>` / `<target>17</target>`
- `switch` expressions
- Lambdas, method references, `Stream`s

**Caveat — the build server cross-compiles to bytecode that ParparVM/TeaVM can consume.** Codename One ships a curated subset of the JDK, **not** the full `java.*` namespace. The `cn1:compliance-check` Maven goal runs on every compile and fails the build if you call an unsupported API. The most common gotchas:
**Caveat — the build server cross-compiles to bytecode that ParparVM/TeaVM can consume.** Codename One ships a curated subset of the JDK, **not** the full `java.*` namespace. The `cn1:bytecode-compliance` Maven goal runs on every compile and fails the build if you call an unsupported API. The most common gotchas:

- No `java.nio.file.*` — use `com.codename1.io.FileSystemStorage` and `Storage`.
- No `java.net.http.*` / `java.net.URLConnection` — use `com.codename1.io.rest.Rest` (preferred) or `ConnectionRequest`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Run `mvn -pl common compile` and the CN1 plugin scans `cn1libs/`, unpacks each f
To verify a cn1lib is wired in correctly, run the bytecode compliance check — it scans all dependencies and will fail noisily if a class is missing:

```bash
mvn -pl common compile cn1:compliance-check
mvn -pl common compile cn1:bytecode-compliance
```

## Creating a new cn1lib (Maven)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Java API Subset, IO, and Networking

Codename One does **not** ship with the full JDK. The Java code you write in `common/` is cross-compiled by ParparVM (iOS) and TeaVM (web), and runs on Android against a hand-curated JDK subset. If you call a class or method that isn't in the subset, the cloud build fails the **compliance check** (`cn1:compliance-check`, which the `process-classes` phase runs automatically).
Codename One does **not** ship with the full JDK. The Java code you write in `common/` is cross-compiled by ParparVM (iOS) and TeaVM (web), and runs on Android against a hand-curated JDK subset. If you call a class or method that isn't in the subset, the cloud build fails the **bytecode compliance check** (`cn1:bytecode-compliance`, which the `process-classes` phase runs automatically).

This document tells you (1) how to discover what *is* supported, and (2) where the IO and networking APIs differ from standard Java.

## How to discover the supported API

The supported API surface is defined by two artifacts that the Codename One Maven plugin resolves from Maven Central. The `compliance-check` goal compares your compiled bytecode against both jars and fails on anything not present.
The supported API surface is defined by two artifacts that the Codename One Maven plugin resolves from Maven Central. The `bytecode-compliance` goal compares your compiled bytecode against both jars and fails on anything not present.

| Artifact | What's inside |
| --- | --- |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ java tools/IsApiSupported.java java.util.HashMap#put
# YES (class present at java-runtime-7.0.242.jar!java/util/HashMap.class — for method-level confirmation run `javap -p -classpath …` and grep for `put`)
```

Useful when porting code from desktop Java and you want a quick "is this safe to use" check before discovering it at `mvn cn1:compliance-check` time.
Useful when porting code from desktop Java and you want a quick "is this safe to use" check before discovering it at `mvn cn1:bytecode-compliance` time.

For the full picture of what's supported and what isn't, see `references/java-api-subset.md`.

Expand Down
2 changes: 1 addition & 1 deletion scripts/initializr/common/src/main/resources/tweet-pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@
<id>cn1-process-classes</id>
<phase>process-classes</phase>
<goals>
<goal>compliance-check</goal>
<goal>bytecode-compliance</goal>
<goal>css</goal>
<!--<goal>compile-javase-sources</goal>-->
</goals>
Expand Down
Loading