Skip to content
Open
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 @@ -31,6 +31,7 @@
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
Expand Down Expand Up @@ -195,13 +196,52 @@ private boolean hasChangedSinceLastCheck() {
return true;
}
try {
return getSourcesModificationTime(true) > complianceOutputFile.lastModified();
long lastCheck = complianceOutputFile.lastModified();
if (getSourcesModificationTime(true) > lastCheck) {
return true;
}
if (lastCheckFailed()) {
// A failure report is not a completed check. Without this a
// rerun with unchanged sources would skip straight past the
// violations that just failed the build.
return true;
}
// The invocation rewrites and the class-version cap mutate the
// compiled classes in place, so gating on source mtimes alone is
// not enough: a later compile pass can regenerate target/classes
// with unchanged sources (e.g. another `mvn package` re-running
// javac), silently shedding the rewrites. Shipping such classes
// breaks device builds -- the iOS translator emits calls to
// virtual_java_lang_String_replaceAll etc. that ParparVM's
// JavaAPI never declares. Re-run whenever any compiled class is
// newer than the last check; the marker is written after the
// rewrites, so an up-to-date output tree stays skippable.
return getCompiledClassesModificationTime() > lastCheck;
} catch (IOException ex) {
getLog().error("Failed to check sources modification time for compliance check", ex);
getLog().error("Failed to check sources/classes modification time for compliance check", ex);
return true;
}
}

private boolean lastCheckFailed() throws IOException {
String content = FileUtils.readFileToString(complianceOutputFile, "UTF-8");
return content.startsWith(FAILURE_REPORT_HEADER);
}

private static final FilenameFilter CLASS_FILES_FILTER = (dir, name) -> name.endsWith(".class");

private long getCompiledClassesModificationTime() {
long mTime = lastModifiedRecursive(new File(project.getBuild().getOutputDirectory()), CLASS_FILES_FILTER);
// With kotlin.compiler.incremental the Kotlin compiler writes to its
// own output tree which executeImpl copies into the output directory,
// so fresh classes can sit there before any copy has happened.
File kotlinIncrementalOutputDir = new File(path(project.getBuild().getDirectory(), "kotlin-ic", "compile", "classes"));
if (kotlinIncrementalOutputDir.exists()) {
mTime = Math.max(mTime, lastModifiedRecursive(kotlinIncrementalOutputDir, CLASS_FILES_FILTER));
}
return mTime;
}

private void writeComplianceSuccess(String message, int rewrittenClassCount) throws MojoExecutionException {
complianceOutputFile.getParentFile().mkdirs();
try {
Expand All @@ -216,9 +256,11 @@ private void writeComplianceSuccess(String message, int rewrittenClassCount) thr
}
}

private static final String FAILURE_REPORT_HEADER = "Codename One compliance check failed.";

private void writeComplianceReport(List<Violation> violations, File outputDir, List<File> dependencyJars, int rewrittenClassCount) throws MojoExecutionException {
StringBuilder report = new StringBuilder();
report.append("Codename One compliance check failed.\n");
report.append(FAILURE_REPORT_HEADER).append("\n");
report.append("Project: ").append(project.getName()).append("\n");
report.append("Output classes: ").append(outputDir.getAbsolutePath()).append("\n");
report.append("Dependency jars scanned: ").append(dependencyJars.size()).append("\n");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,93 @@ void recognizesAllSimdAllocaHelperNames() throws Exception {
assertFalse(((Boolean) method.invoke(null, "app/Other", "allocaByte", "(I)[B")).booleanValue());
}

@Test
void rerunsComplianceCheckWhenClassesRecompiledWithoutSourceChange(@TempDir Path tempDir) throws Exception {
BytecodeComplianceMojo mojo = newMojoForSkipCheck(tempDir);
long markerTime = markerFile(mojo).lastModified();

Path classFile = writeClassWithVersion(outputDir(mojo), "app/Stable", Opcodes.V1_8);
assertTrue(classFile.toFile().setLastModified(markerTime - 60000));
assertFalse(hasChangedSinceLastCheck(mojo),
"Expected skip when classes are older than the last compliance check");

// The rewrites mutate target/classes in place; a recompile with
// unchanged sources regenerates the classes and must invalidate the
// skip or the rewrites are silently lost.
assertTrue(classFile.toFile().setLastModified(markerTime + 60000));
assertTrue(hasChangedSinceLastCheck(mojo),
"Expected re-run when classes are newer than the last compliance check");
}

@Test
void rerunsComplianceCheckWhenKotlinIncrementalClassesAreNewer(@TempDir Path tempDir) throws Exception {
BytecodeComplianceMojo mojo = newMojoForSkipCheck(tempDir);
long markerTime = markerFile(mojo).lastModified();

Path kotlinIcDir = tempDir.resolve("ios").resolve("target")
.resolve("kotlin-ic").resolve("compile").resolve("classes");
Path kotlinClass = writeClassWithVersion(kotlinIcDir, "app/FromKotlinIc", Opcodes.V1_8);
assertTrue(kotlinClass.toFile().setLastModified(markerTime + 60000));

assertTrue(hasChangedSinceLastCheck(mojo),
"Expected re-run when the Kotlin incremental output tree has classes newer than the last check");
}

@Test
void rerunsComplianceCheckAfterPreviousFailureReport(@TempDir Path tempDir) throws Exception {
BytecodeComplianceMojo mojo = newMojoForSkipCheck(tempDir);
java.io.File marker = markerFile(mojo);
long markerTime = marker.lastModified();
Files.write(marker.toPath(),
"Codename One compliance check failed.\nProject: test\n".getBytes("UTF-8"));
assertTrue(marker.setLastModified(markerTime));

assertTrue(hasChangedSinceLastCheck(mojo),
"Expected re-run when the previous check recorded violations, even with nothing newer");
}

private BytecodeComplianceMojo newMojoForSkipCheck(Path tempDir) throws Exception {
Path iosDir = tempDir.resolve("ios");
Path commonDir = tempDir.resolve("common");
Files.createDirectories(iosDir.resolve("target").resolve("codenameone"));
Files.createDirectories(commonDir);

long base = System.currentTimeMillis();
Path settings = commonDir.resolve("codenameone_settings.properties");
Files.write(settings, new byte[0]);
assertTrue(settings.toFile().setLastModified(base - 120000));

Path marker = iosDir.resolve("target").resolve("codenameone").resolve("compliance_check.txt");
Files.write(marker, "Completed compliance check on test\n".getBytes("UTF-8"));
assertTrue(marker.toFile().setLastModified(base));

MavenProject project = new MavenProject();
project.setFile(iosDir.resolve("pom.xml").toFile());
org.apache.maven.model.Build build = new org.apache.maven.model.Build();
build.setDirectory(iosDir.resolve("target").toString());
build.setOutputDirectory(iosDir.resolve("target").resolve("classes").toString());
project.setBuild(build);

BytecodeComplianceMojo mojo = new BytecodeComplianceMojo();
mojo.project = project;
setField(mojo, "complianceOutputFile", marker.toFile());
return mojo;
}

private java.io.File markerFile(BytecodeComplianceMojo mojo) throws Exception {
return (java.io.File) field(mojo, "complianceOutputFile");
}

private Path outputDir(BytecodeComplianceMojo mojo) {
return java.nio.file.Paths.get(mojo.project.getBuild().getOutputDirectory());
}

private boolean hasChangedSinceLastCheck(BytecodeComplianceMojo mojo) throws Exception {
Method method = BytecodeComplianceMojo.class.getDeclaredMethod("hasChangedSinceLastCheck");
method.setAccessible(true);
return ((Boolean) method.invoke(mojo)).booleanValue();
}

@SuppressWarnings("unchecked")
private Map<String, ?> buildClassIndex(BytecodeComplianceMojo mojo, List<java.io.File> roots) throws Exception {
Method method = BytecodeComplianceMojo.class.getDeclaredMethod("buildClassIndex", List.class);
Expand Down
Loading