tests rules_jvm: ensure '.kotlin_module' content is always in-sync with the output

- ensure output state always corresponds to last known good state of dependency graph: if compilation attempt finishes with errors, all output generated during this compile attempt is deleted. This ensures there will be no stale class files in the output later on.

Merge-request: IJ-MR-191763
Merged-by: Eugene Zhuravlev <eugene.zhuravlev@jetbrains.com>

GitOrigin-RevId: e2adffd268c8e71b8795aec7d070b470551f57e3
This commit is contained in:
Eugene Zhuravlev
2026-02-16 14:54:20 +00:00
committed by intellij-monorepo-bot
parent 019bd2a161
commit 36eea1f4c9
25 changed files with 450 additions and 102 deletions
@@ -75,6 +75,7 @@ kt_jvm_library(
"@bazel_tools//tools/java/runfiles",
"//:annotations_import",
"//:asm-all_import",
"//:kotlin-metadata-jvm_import",
"//:junit4",
"//dependency-graph",
"//jps-builders-6:build-javac-rt",
@@ -373,13 +373,10 @@ public class KotlinTests extends BazelIncBuildTest {
performTest("kotlin/pureKotlin/localClassChanged").assertSuccessful();
}
/*
//todo: handle scenario: remove outdated class references from kotlin_module explicitly, if kotlinc cannot be started due to empty source set
@Test
public void testMoveClass() throws Exception {
performTest(2, "kotlin/pureKotlin/moveClass").assertSuccessful();
}
*/
@Test
public void testMoveFileWithChangingPackage() throws Exception {
@@ -426,18 +423,10 @@ public class KotlinTests extends BazelIncBuildTest {
performTest("kotlin/pureKotlin/packageFileChangedPackage").assertSuccessful();
}
// todo: investigate scenario:
// 1. the only change on disk is the removal of some "a.kt" file with top-level declarations
// 2. => corresponding AKt.class is removed from the output
// 3. Since there are no sources to compile, compiler will not be run
// 4. => the kotlin_module file in the output is not updated and might contain references to AKt
// 5. All successive compilations know nothing about deleted AKt => the corresponding entry in the kotlin_module may stuck forever.
/*
@Test
public void testPackageFileChangedThenOtherRemoved() throws Exception {
performTest(2, "kotlin/pureKotlin/packageFileChangedThenOtherRemoved").assertSuccessful();
}
*/
@Test
public void testPackageFileRemoved() throws Exception {
@@ -464,14 +453,10 @@ public class KotlinTests extends BazelIncBuildTest {
performTest("kotlin/pureKotlin/packagePrivateOnlyChanged").assertSuccessful();
}
/*
// todo: handle scenario: remove outdated class references from kotlin_module explicitly, if kotlinc cannot be started due to empty source set
@Test
public void testPackageRecreated() throws Exception {
performTest(2, "kotlin/pureKotlin/packageRecreated").assertSuccessful();
}
*/
@Test
public void testPackageRecreatedAfterRenaming() throws Exception {
@@ -618,13 +603,10 @@ public class KotlinTests extends BazelIncBuildTest {
performTest("kotlin/pureKotlin/removeTopLevelTypeAlias").assertSuccessful();
}
/*
// todo: handle scenario: remove outdated class references from kotlin_module explicitly, if kotlinc cannot be started due to empty source set
@Test
public void testRemoveUnusedFile() throws Exception {
performTest("kotlin/pureKotlin/removeUnusedFile").assertSuccessful();
}
*/
@Test
public void testRenameClass() throws Exception {
@@ -1,9 +1,22 @@
package com.intellij.tools.build.bazel.impl;
import com.intellij.tools.build.bazel.jvmIncBuilder.DataPaths;
import com.intellij.tools.build.bazel.jvmIncBuilder.NodeSourceSnapshot;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.ConfigurationState;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.Utils;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.graph.PersistentMVStoreMapletFactory;
import kotlin.metadata.jvm.KmModule;
import kotlin.metadata.jvm.KmPackageParts;
import kotlin.metadata.jvm.KotlinModuleMetadata;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.dependency.DependencyGraph;
import org.jetbrains.jps.dependency.Node;
import org.jetbrains.jps.dependency.NodeSource;
import org.jetbrains.jps.dependency.impl.DependencyGraphImpl;
import org.jetbrains.jps.dependency.impl.PathSourceMapper;
import org.jetbrains.jps.dependency.java.JVMClassNode;
import org.jetbrains.jps.dependency.java.KotlinMeta;
import org.jetbrains.jps.util.Iterators;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -28,13 +41,22 @@ import java.nio.file.StandardCopyOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static org.jetbrains.jps.util.Iterators.collect;
import static org.jetbrains.jps.util.Iterators.filter;
import static org.jetbrains.jps.util.Iterators.map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@@ -177,6 +199,7 @@ public abstract class BazelIncBuildTest {
runBazelBuild(bazelTarget).assertSuccessful(); // the initial build
assertTrue("Tests output root directory " + testOutputDir + " should exist. Probably test expectations differ from Bazel's current output dir naming policy", Files.exists(testOutputDir));
validateBuildOutput(testDataRelativePath, testOutputDir);
ExecutionResult result = null;
StringBuilder buildLog = new StringBuilder();
@@ -192,6 +215,8 @@ public abstract class BazelIncBuildTest {
buildLog.append("\n").append("------------------------------------------");
buildLog.append("\n").append("Exit code: ").append(result.isSuccessful()? "OK" : "ERROR");
}
validateBuildOutput(testDataRelativePath, testOutputDir);
}
String expectedBuildLog = Files.readString(expectedBuildLogFile, StandardCharsets.UTF_8).replaceAll("\r\n?", "\n").trim();
@@ -280,6 +305,138 @@ public abstract class BazelIncBuildTest {
});
}
protected void validateBuildOutput(String testDataRelativePath, Path testOutputDir) throws IOException {
// Base validation:
// 1. For a successful build: for every class node in graph the corresponding .class file must exist in the output and vice versa
// 2. For a failed build: for every class file in the output, there must exist a class node in the graph.
// If a class in the graph corresponds to a non-up-to-date source (deleted or modified), there must be no corresponding class file in the output
// see StorageManager.cleanBuildState()
// 3. ConfigurationState with sources with empty digest (=> classes in the graph corresponding to these sources) form dirty scope
// 4. if sources contain 1 or more kotlin files (= for Kotlin tests), kotlin_module must exist in the output
// 5. kotlin_module must contain all facade classes from the output without facade classes from dirty scope
for (BuildOutput output : BuildOutput.scanOutputs(testOutputDir)) {
try (DependencyGraph graph = output.graph) {
NodeSourceSnapshot srcSnapshot = output.configState.getSources();
Set<NodeSource> dirtySources = collect(filter(srcSnapshot.getElements(), src -> srcSnapshot.getDigest(src).isBlank()), new HashSet<>());
// output .class paths for bytecode
Set<String> dirtyClasses = new HashSet<>();
Set<String> allGraphClasses = new HashSet<>();
Set<String> allOutputClasses = new HashSet<>();
boolean hasKotlinBytecode = false;
String kotlinModuleEntryPath = null;
// class names
Set<String> kotlinModuleFacadeClassNames = new HashSet<>();
Set<String> graphFacadeClassNames = new HashSet<>();
Set<String> graphDirtyFacadeClassNames = new HashSet<>();
for (NodeSource src : graph.getSources()) {
boolean isSourceDirty = dirtySources.contains(src);
Consumer<String> acc = isSourceDirty? p -> {dirtyClasses.add(p); allGraphClasses.add(p);} : allGraphClasses::add;
for (Node<?, ?> node : graph.getNodes(src)) {
if (node instanceof JVMClassNode<?,?> clsNode) {
acc.accept(clsNode.getOutFilePath());
Iterator<KotlinMeta> metadata = clsNode.getMetadata(KotlinMeta.class).iterator();
if (metadata.hasNext()) {
hasKotlinBytecode = true;
if (metadata.next().isTopLevelDeclarationContainer()) {
graphFacadeClassNames.add(clsNode.getName());
if (isSourceDirty) {
graphDirtyFacadeClassNames.add(clsNode.getName());
}
}
}
}
}
}
try (var zis = new ZipInputStream(new BufferedInputStream(Files.newInputStream(output.outputJar)))) {
for (ZipEntry entry = zis.getNextEntry(); entry != null; entry = zis.getNextEntry()) {
String path = entry.getName();
if (path.endsWith(".class")) {
allOutputClasses.add(path);
}
if (path.startsWith("META-INF/") && path.endsWith(DataPaths.KOTLIN_MODULE_EXTENSION)) {
kotlinModuleEntryPath = path;
KmModule module = KotlinModuleMetadata.read(zis.readAllBytes()).getKmModule();
for (KmPackageParts parts : module.getPackageParts().values()) {
kotlinModuleFacadeClassNames.addAll(parts.getFileFacades());
kotlinModuleFacadeClassNames.addAll(parts.getMultiFileClassParts().values());
}
}
}
}
if (hasKotlinBytecode) {
assertNotNull("Test " + testDataRelativePath + " must have '.kotlin_module' file in its output", kotlinModuleEntryPath);
}
if (dirtySources.isEmpty()) {
// => successful build
assertTrue(dirtyClasses.isEmpty());
assertTrue(graphDirtyFacadeClassNames.isEmpty());
assertEquals(allGraphClasses, allOutputClasses);
if (hasKotlinBytecode) {
assertEquals("Set of registered Kotlin facade classes in the dependency graph must be the same as in '.kotlin_module' output file", graphFacadeClassNames, kotlinModuleFacadeClassNames);
}
}
else {
// => build completed with errors
assertTrue("All classes in the output should be registered in the dependency graph", allGraphClasses.containsAll(allOutputClasses));
for (String graphClass : allGraphClasses) {
if (dirtyClasses.contains(graphClass)) {
assertFalse("Classes from modified or deleted sources must not be in the output: " + graphClass, allOutputClasses.contains(graphClass));
}
else {
assertTrue("Classes from up-to-date sources must be in the output: " + graphClass, allOutputClasses.contains(graphClass));
}
}
if (hasKotlinBytecode) {
assertTrue("All Kotlin facade classes from the '.kotlin_module' file must be registered in the dependency graph", graphFacadeClassNames.containsAll(kotlinModuleFacadeClassNames));
for (String graphClassName : graphFacadeClassNames) {
if (graphDirtyFacadeClassNames.contains(graphClassName)) {
assertFalse("Facade classes from modified or deleted sources must not be in '.kotlin_module' file: " + graphClassName, kotlinModuleFacadeClassNames.contains(graphClassName));
}
else {
assertTrue("Facade classes from up-to-date sources must be in '.kotlin_module' file: " + graphClassName, kotlinModuleFacadeClassNames.contains(graphClassName));
}
}
}
}
}
}
}
private record BuildOutput(DependencyGraph graph, ConfigurationState configState, Path outputJar) {
static Iterable<BuildOutput> scanOutputs(Path testOutputDir) throws IOException {
List<Path> targetOutputs = Files.list(testOutputDir).filter(path -> matches(path, ".jar") && !matches(path, DataPaths.ABI_JAR_SUFFIX)).toList();
return map(targetOutputs, output -> {
try {
String dataDirName = DataPaths.truncateExtension(getFileName(output)) + DataPaths.DATA_DIR_NAME_SUFFIX;
Path graphPath = output.resolveSibling(dataDirName).resolve(DataPaths.DEP_GRAPH_FILE_NAME);
Path configStatePath = graphPath.resolveSibling(DataPaths.CONFIG_STATE_FILE_NAME);
assertTrue("Dependency graph storage is missing for the output " + output, Files.exists(graphPath));
assertTrue("Configuration state storage is missing for the output " + output, Files.exists(configStatePath));
return new BuildOutput(
new DependencyGraphImpl(new PersistentMVStoreMapletFactory(graphPath.toString(), 1)),
new ConfigurationState(new PathSourceMapper(), configStatePath),
output
);
}
catch (IOException e) {
throw new RuntimeException(e);
}
});
}
}
@NotNull
protected ExecutionResult runBazelBuild(String... options) throws Exception {
return runBazelCommand(
@@ -15,4 +15,4 @@ Compiling files:
java/classModifiers/addStatic/a/C.java
End of files
------------------------------------------
Exit code: ERROR
Exit code: ERROR
@@ -14,4 +14,4 @@ Compiling files:
java/classProperties/convertToCheckedException/a/ppp/Task.java
End of files
------------------------------------------
Exit code: ERROR
Exit code: ERROR
@@ -14,4 +14,4 @@ Compiling files:
java/classProperties/removeExtendsAffectsFieldAccess/a/SwitchProvider.java
End of files
------------------------------------------
Exit code: ERROR
Exit code: ERROR
@@ -14,4 +14,4 @@ Compiling files:
java/classProperties/removeExtendsAffectsMethodAccess/a/SwitchProvider.java
End of files
------------------------------------------
Exit code: ERROR
Exit code: ERROR
@@ -14,4 +14,4 @@ Compiling files:
java/classProperties/removeImplements/a/Client2.java
End of files
------------------------------------------
Exit code: ERROR
Exit code: ERROR
@@ -16,4 +16,4 @@ Compiling files:
java/fieldModifiers/hidePublicWithPackagePrivate/a/qqq/DerivedOutside.java
End of files
------------------------------------------
Exit code: ERROR
Exit code: ERROR
@@ -14,4 +14,4 @@ Compiling files:
java/generics/argumentContainment3/a/GenericBound.java
End of files
------------------------------------------
Exit code: ERROR
Exit code: ERROR
@@ -16,4 +16,4 @@ Compiling files:
java/membersChange/addSAMInterfaceAbstractMethod/a/ServiceClient.java
End of files
------------------------------------------
Exit code: ERROR
Exit code: ERROR
@@ -14,4 +14,4 @@ Compiling files:
java/membersChange/addVarargMethod/a/D.java
End of files
------------------------------------------
Exit code: ERROR
Exit code: ERROR
@@ -2,6 +2,9 @@
Cleaning output files:
Foo.class
End of files
Cleaning output files:
META-INF/kotlin_pureKotlin_moveClass-a.kotlin_module
End of files
Compiling files:
kotlin/pureKotlin/moveClass/a/b.kt
End of files
@@ -10,9 +13,6 @@ Exit code: OK
================ Step #2 =================
Cleaning output files:
Foo.class
META-INF/kotlin_pureKotlin_moveClass-a.kotlin_module
End of files
Compiling files:
End of files
------------------------------------------
Exit code: OK
Exit code: OK
@@ -10,10 +10,7 @@ End of files
Exit code: OK
================ Step #2 =================
Cleaning output files:
META-INF/kotlin_pureKotlin_packageFileChangedThenOtherRemoved-a.kotlin_module
test/BKt.class
End of files
Compiling files:
End of files
------------------------------------------
Exit code: OK
Exit code: OK
@@ -0,0 +1,2 @@
// Dummy file so that for this target is always less than 100% of sources are modified.
// => the builder will work in incremental mode and will not rebuild whole target from scratch
@@ -2,8 +2,6 @@
Cleaning output files:
test/AKt.class
End of files
Compiling files:
End of files
------------------------------------------
Exit code: OK
================ Step #2 =================
@@ -14,4 +12,4 @@ Compiling files:
kotlin/pureKotlin/packageRecreated/a/b.kt
End of files
------------------------------------------
Exit code: OK
Exit code: OK
@@ -0,0 +1,2 @@
// Dummy file so that for this target is always less than 100% of sources are modified.
// => the builder will work in incremental mode and will not rebuild whole target from scratch
@@ -3,7 +3,5 @@ Cleaning output files:
test/Bar.class
test/UnusedKt.class
End of files
Compiling files:
End of files
------------------------------------------
Exit code: OK
Exit code: OK
@@ -1,14 +1,33 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.tools.build.bazel.jvmIncBuilder;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.*;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.BatchBuildProcessLogger;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.BuildDiagnosticCollector;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.ConfigurationState;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.ElementSnapshotDeltaImpl;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.FormsCompiler;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.OutputSinkImpl;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.PostponedDiagnosticSink;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.ResourcesSnapshotDelta;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.RunnerRegistry;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.SnapshotDeltaImpl;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.Utils;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.forms.FormBinding;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.graph.AsyncLibraryGraphLoader;
import com.intellij.tools.build.bazel.jvmIncBuilder.impl.graph.DeltaView;
import com.intellij.tools.build.bazel.jvmIncBuilder.runner.CompilerRunner;
import com.intellij.tools.build.bazel.jvmIncBuilder.runner.OutputFile;
import com.intellij.tools.build.bazel.jvmIncBuilder.runner.OutputOrigin;
import com.intellij.tools.build.bazel.jvmIncBuilder.runner.OutputSink;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.dependency.*;
import org.jetbrains.jps.dependency.CompositeGraph;
import org.jetbrains.jps.dependency.Delta;
import org.jetbrains.jps.dependency.DependencyGraph;
import org.jetbrains.jps.dependency.Graph;
import org.jetbrains.jps.dependency.Node;
import org.jetbrains.jps.dependency.NodeSource;
import org.jetbrains.jps.dependency.NodeSourcePathMapper;
import org.jetbrains.jps.dependency.java.JVMClassNode;
import org.jetbrains.jps.util.Pair;
import org.jetbrains.jps.util.SystemInfo;
@@ -18,12 +37,24 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import static org.jetbrains.jps.util.Iterators.*;
import static org.jetbrains.jps.util.Iterators.collect;
import static org.jetbrains.jps.util.Iterators.contains;
import static org.jetbrains.jps.util.Iterators.count;
import static org.jetbrains.jps.util.Iterators.filter;
import static org.jetbrains.jps.util.Iterators.flat;
import static org.jetbrains.jps.util.Iterators.isEmpty;
import static org.jetbrains.jps.util.Iterators.map;
/** @noinspection SSBasedInspection*/
public class BazelIncBuilder {
@@ -238,6 +269,9 @@ public class BazelIncBuilder {
for (CompilerRunner runner : roundCompilers) {
Iterable<NodeSource> toCompile = collect(filter(srcSnapshotDelta.getModified(), runner::canCompile), new ArrayList<>());
if (context.getBuildLogger() instanceof BatchBuildProcessLogger batchLogger) {
batchLogger.startBatch();
}
if (!srcSnapshotDelta.isRecompileAll() && !isEmpty(toCompile)) {
// delete outputs corresponding to recompiled sources before running the compiler
@@ -245,16 +279,20 @@ public class BazelIncBuilder {
Collection<String> cleaned = deleteCompilerOutputs(
storageManager.getGraph(), toCompile, outBuilder, new ArrayList<>()
);
for (String toDelete : runner.getOutputPathsToDelete()) {
if (outBuilder.deleteEntry(toDelete)) {
cleaned.add(toDelete);
}
}
logDeletedPaths(context, cleaned);
}
ExitCode code = runner.compile(toCompile, filter(srcSnapshotDelta.getDeleted(), runner::canCompile), diagnostic, outSink);
if (context.getBuildLogger() instanceof BatchBuildProcessLogger batchLogger) {
batchLogger.stopBatch();
}
if (code == ExitCode.CANCEL) {
if (!srcSnapshotDelta.isRecompileAll()) {
// in case of errors, clean partially compiled output to maintain consistent state
deleteGeneratedOutputs(outSink, storageManager.getCompositeOutputBuilder());
}
return code;
}
if (code == ExitCode.ERROR && !diagnostic.hasErrors()) {
@@ -274,19 +312,26 @@ public class BazelIncBuilder {
srcSnapshotDelta = nextSnapshotDelta;
}
else {
if (srcSnapshotDelta.isRecompileAll() || !nextSnapshotDelta.hasChanges()) {
if (srcSnapshotDelta.isRecompileAll()) {
// no need to clean partial outputs, the next build will start from the clean state anyway
return ExitCode.ERROR;
}
// keep previous snapshot delta, just augment it with the newly found sources for recompilation
if (nextSnapshotDelta.isRecompileAll()) {
srcSnapshotDelta.markRecompileAll();
}
else {
for (NodeSource source : nextSnapshotDelta.getModified()) {
srcSnapshotDelta.markRecompile(source);
if (nextSnapshotDelta.hasChanges()) {
// keep previous snapshot delta, just augment it with the newly found sources for recompilation
if (nextSnapshotDelta.isRecompileAll()) {
srcSnapshotDelta.markRecompileAll();
}
else {
for (NodeSource source : nextSnapshotDelta.getModified()) {
srcSnapshotDelta.markRecompile(source);
}
}
}
if (!isInitialRound) {
if (!isInitialRound || !nextSnapshotDelta.hasChanges()) {
// in case of errors, clean partially compiled output to maintain consistent state
deleteGeneratedOutputs(outSink, storageManager.getCompositeOutputBuilder());
return ExitCode.ERROR;
}
// for initial round, partial compilation and when analysis has expanded the scope, attempt automatic error recovery by repeating the compilation with the expanded scope
@@ -494,6 +539,12 @@ public class BazelIncBuilder {
return deletedPathsAcc;
}
private static void deleteGeneratedOutputs(OutputSink sink, ZipOutputBuilder outBuilder) {
for (String outputPath : flat(map(EnumSet.allOf(OutputOrigin.Kind.class), origin -> sink.getGeneratedOutputPaths(origin, OutputFile.Kind.bytecode)))) {
outBuilder.deleteEntry(outputPath);
}
}
private static void logDeletedPaths(BuildContext context, Iterable<String> deletedPaths) {
if (!context.isRebuild()) {
BuildProcessLogger logger = context.getBuildLogger();
@@ -0,0 +1,126 @@
package com.intellij.tools.build.bazel.jvmIncBuilder.impl;
import com.intellij.tools.build.bazel.jvmIncBuilder.BuildProcessLogger;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import static org.jetbrains.jps.util.Iterators.flat;
public final class BatchBuildProcessLogger implements BuildProcessLogger {
private final BuildProcessLogger myDelegate;
private boolean myBatchActive;
private final List<PostponedEvent> myEvents = new ArrayList<>();
public BatchBuildProcessLogger(BuildProcessLogger delegate) {
myDelegate = delegate;
}
public void startBatch() {
myBatchActive = true;
}
public void stopBatch() {
myBatchActive = false;
try {
for (PostponedEvent event : myEvents) {
event.process(myDelegate);
}
}
finally {
myEvents.clear();
}
}
@Override
public boolean isEnabled() {
return myDelegate.isEnabled();
}
@Override
public void logDeletedPaths(Iterable<String> paths) {
if (myBatchActive && isEnabled()) {
if (myEvents.isEmpty() || !myEvents.getLast().mergeDeleted(paths)) {
myEvents.add(PostponedEvent.pathsDeleted(paths));
}
}
else {
myDelegate.logDeletedPaths(paths);
}
}
@Override
public void logCompiledPaths(Iterable<Path> files, String builderId, String description) {
if (myBatchActive && isEnabled()) {
if (myEvents.isEmpty() || !myEvents.getLast().mergeCompiled(files, builderId, description)) {
myEvents.add(PostponedEvent.pathsCompiled(files, builderId, description));
}
}
else {
myDelegate.logCompiledPaths(files, builderId, description);
}
}
@Override
public String getCollectedData() {
stopBatch(); // ensure all postponed data is logged
return myDelegate.getCollectedData();
}
private interface PostponedEvent {
void process(BuildProcessLogger target);
boolean mergeDeleted(Iterable<String> paths);
boolean mergeCompiled(Iterable<Path> files, String builderId, String description);
static PostponedEvent pathsDeleted(Iterable<String> paths) {
return new PostponedEvent() {
Iterable<String> myData = paths;
@Override
public void process(BuildProcessLogger target) {
target.logDeletedPaths(myData);
}
@Override
public boolean mergeDeleted(Iterable<String> paths) {
myData = flat(myData, paths);
return true;
}
@Override
public boolean mergeCompiled(Iterable<Path> files, String builderId, String description) {
return false;
}
};
}
static PostponedEvent pathsCompiled(Iterable<Path> files, String builderId, String description) {
return new PostponedEvent() {
private Iterable<Path> myData = files;
private String myDescription = description;
@Override
public void process(BuildProcessLogger target) {
target.logCompiledPaths(myData, builderId, myDescription);
}
@Override
public boolean mergeDeleted(Iterable<String> paths) {
return false;
}
@Override
public boolean mergeCompiled(Iterable<Path> otherFiles, String otherBuilderId, String otherDescription) {
if (!builderId.equals(otherBuilderId)) {
return false;
}
myData = flat(myData, otherFiles);
if (!myDescription.endsWith(otherDescription)) {
myDescription = myDescription + "\n" + otherDescription;
}
return true;
}
};
}
}
}
@@ -172,7 +172,7 @@ public class BuildContextImpl implements BuildContext {
}
myBuilderOptions = BuilderOptions.create(buildJavaOptions(flags), buildKotlinOptions(flags, map(myLibraries.getElements(), myPathMapper::toPath)));
myBuildProcessLogger = VMFlags.isBuildProcessLoggerEnabled()? new BuildProcessLoggerImpl(baseDir) : BuildProcessLogger.EMPTY;
myBuildProcessLogger = VMFlags.isBuildProcessLoggerEnabled()? new BatchBuildProcessLogger(new BuildProcessLoggerImpl(baseDir)) : BuildProcessLogger.EMPTY;
}
private static @NotNull List<String> buildKotlinOptions(Map<CLFlags, List<String>> flags, @NotNull Iterable<@NotNull Path> classpath) {
@@ -9,7 +9,9 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.jetbrains.jps.util.Iterators.*;
import static org.jetbrains.jps.util.Iterators.collect;
import static org.jetbrains.jps.util.Iterators.isEmpty;
import static org.jetbrains.jps.util.Iterators.map;
public class BuildProcessLoggerImpl implements BuildProcessLogger {
private final StringBuilder myBuf = new StringBuilder();
@@ -2,6 +2,7 @@
package com.intellij.tools.build.bazel.jvmIncBuilder.impl;
import com.intellij.tools.build.bazel.jvmIncBuilder.BuildContext;
import com.intellij.tools.build.bazel.jvmIncBuilder.BuildProcessLogger;
import com.intellij.tools.build.bazel.jvmIncBuilder.CLFlags;
import com.intellij.tools.build.bazel.jvmIncBuilder.DataPaths;
import com.intellij.tools.build.bazel.jvmIncBuilder.DiagnosticSink;
@@ -16,6 +17,8 @@ import com.intellij.tools.build.bazel.jvmIncBuilder.runner.OutputOrigin;
import com.intellij.tools.build.bazel.jvmIncBuilder.runner.OutputSink;
import kotlin.Unit;
import kotlin.jvm.functions.Function1;
import kotlin.metadata.jvm.KmPackageParts;
import kotlin.metadata.jvm.KotlinModuleMetadata;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.jps.dependency.NodeSource;
@@ -59,10 +62,12 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Consumer;
import static com.intellij.tools.build.bazel.jvmIncBuilder.impl.KotlinPluginsKt.configurePlugins;
@@ -91,9 +96,6 @@ public class KotlinCompilerRunner implements CompilerRunner {
private final @NotNull Map<@NotNull String, List<CliOptionValue>> myInternalPluginIdToOptions = new HashMap<>();
private final List<String> myJavaSources;
private final @Nullable String myModuleEntryPath;
private byte @Nullable [] myLastGoodModuleEntryContent;
public KotlinCompilerRunner(BuildContext context, StorageManager storageManager) {
myContext = context;
myStorageManager = storageManager;
@@ -123,19 +125,6 @@ public class KotlinCompilerRunner implements CompilerRunner {
myJavaSources = collect(
map(filter(context.getSources().getElements(), KotlinCompilerRunner::isJavaSource), ns -> myPathMapper.toPath(ns).toString()), new ArrayList<>()
);
String moduleEntryPath = null;
try {
ZipOutputBuilderImpl outBuilder = storageManager.getOutputBuilder();
moduleEntryPath = find(outBuilder.listEntries("META-INF/"), n -> n.endsWith(DataPaths.KOTLIN_MODULE_EXTENSION));
if (moduleEntryPath != null) {
myLastGoodModuleEntryContent = outBuilder.getContent(moduleEntryPath);
}
}
catch (IOException e) {
context.report(Message.create(this, e));
}
myModuleEntryPath = moduleEntryPath;
}
@Override
@@ -156,19 +145,38 @@ public class KotlinCompilerRunner implements CompilerRunner {
return src.toString().endsWith(".java");
}
@Override
public Iterable<String> getOutputPathsToDelete() {
return myModuleEntryPath != null? List.of(myModuleEntryPath) : List.of();
}
@Override
public ExitCode compile(Iterable<NodeSource> sources, Iterable<NodeSource> deletedSources, DiagnosticSink diagnostic, OutputSink out) throws Exception {
try {
ZipOutputBuilder outputBuilder = myStorageManager.getOutputBuilder();
ZipOutputBuilder abiOutputBuilder = myStorageManager.getAbiOutputBuilder();
String moduleEntryPath = find(outputBuilder.listEntries("META-INF/"), n -> n.endsWith(DataPaths.KOTLIN_MODULE_EXTENSION));
byte[] moduleEntryContent = moduleEntryPath != null? outputBuilder.getContent(moduleEntryPath) : null;
byte[] abiModuleEntryContent = moduleEntryPath != null && abiOutputBuilder != null? abiOutputBuilder.getContent(moduleEntryPath) : null;
IncrementalCache incCache = new KotlinIncrementalCacheImpl(myStorageManager, flat(deletedSources, sources), moduleEntryPath, moduleEntryContent);
if (isEmpty(sources)) {
if (moduleEntryPath != null && !isEmpty(deletedSources)) {
// ObsoletePackageParts here are only classes corresponding to deleted sources
outputBuilder.putEntry(moduleEntryPath, cleanObsoletePackageParts(incCache, moduleEntryContent));
if (abiOutputBuilder != null){
abiOutputBuilder.putEntry(moduleEntryPath, cleanObsoletePackageParts(incCache, abiModuleEntryContent));
}
}
return ExitCode.OK;
}
if (moduleEntryPath != null && myStorageManager.getCompositeOutputBuilder().deleteEntry(moduleEntryPath)) { // ensure previous state is removed
BuildProcessLogger logger = myContext.getBuildLogger();
if (logger.isEnabled()) {
logger.logDeletedPaths(List.of(moduleEntryPath));
}
}
K2JVMCompilerArguments kotlinArgs = buildKotlinCompilerArguments(myContext, sources);
KotlinIncrementalCacheImpl incCache = new KotlinIncrementalCacheImpl(myStorageManager, flat(deletedSources, sources), myModuleEntryPath, myLastGoodModuleEntryContent);
OutputVirtualFile outputFileSystemRoot = new OutputFileSystem(new KotlinVirtualFileProvider(out)).root;
Services services = buildServices(kotlinArgs.getModuleName(), incCache, outputFileSystemRoot);
MessageCollector messageCollector = new KotlinMessageCollector(diagnostic, this);
@@ -205,15 +213,14 @@ public class KotlinCompilerRunner implements CompilerRunner {
}
finally {
processTrackers(out, generatedClasses);
if (myModuleEntryPath != null) {
byte[] updated = myStorageManager.getOutputBuilder().getContent(myModuleEntryPath);
if (updated != null) {
// save the updated state for the next round
myLastGoodModuleEntryContent = updated;
if (moduleEntryPath != null) {
if (outputBuilder.getContent(moduleEntryPath) == null) {
// restore adjusted module entry
outputBuilder.putEntry(moduleEntryPath, cleanObsoletePackageParts(incCache, moduleEntryContent));
}
else {
// make sure the output contains the module entry corresponding to last known good state
myStorageManager.getOutputBuilder().putEntry(myModuleEntryPath, myLastGoodModuleEntryContent);
if (abiOutputBuilder != null && abiOutputBuilder.getContent(moduleEntryPath) == null) {
// restore adjusted module entry
abiOutputBuilder.putEntry(moduleEntryPath, cleanObsoletePackageParts(incCache, abiModuleEntryContent));
}
}
}
@@ -227,6 +234,37 @@ public class KotlinCompilerRunner implements CompilerRunner {
}
}
private static byte[] cleanObsoletePackageParts(IncrementalCache incCache, byte[] moduleEntryContent) {
Collection<String> _parts = incCache.getObsoletePackageParts();
if (_parts.isEmpty()) {
return moduleEntryContent;
}
Set<String> obsoleteFacades = _parts instanceof Set? (Set<String>) _parts : collect(_parts, new HashSet<>());
KotlinModuleMetadata moduleMeta = KotlinModuleMetadata.read(moduleEntryContent);
Map<String, KmPackageParts> packageParts = moduleMeta.getKmModule().getPackageParts(); // mutable
boolean changed = false;
for (Iterator<Map.Entry<String, KmPackageParts>> packagePartsIterator = packageParts.entrySet().iterator(); packagePartsIterator.hasNext(); ) {
KmPackageParts parts = packagePartsIterator.next().getValue();
List<String> facades = parts.getFileFacades(); // mutable
Map<String, String> multiFileParts = parts.getMultiFileClassParts(); // mutable
changed |= facades.removeAll(obsoleteFacades);
for (Iterator<Map.Entry<String, String>> it = multiFileParts.entrySet().iterator(); it.hasNext(); ) {
Map.Entry<String, String> entry = it.next();
if (obsoleteFacades.contains(entry.getValue())) {
it.remove();
changed = true;
}
}
if (facades.isEmpty() && multiFileParts.isEmpty()) {
packagePartsIterator.remove();
changed = true;
}
}
return changed? moduleMeta.write() : moduleEntryContent;
}
private record GeneratedClass(String jvmClassName, File source) {}
private void processTrackers(OutputSink out, List<GeneratedClass> generated) {
@@ -241,10 +279,7 @@ public class KotlinCompilerRunner implements CompilerRunner {
processInferredTypeTracker(inferredTypeTracker, out);
}
private static void processInlineConstTracker(InlineConstTrackerImpl inlineConstTracker,
GeneratedClass output,
OutputSink callback) {
private static void processInlineConstTracker(InlineConstTrackerImpl inlineConstTracker, GeneratedClass output, OutputSink callback) {
Map<String, Collection<ConstantRef>> constMap = inlineConstTracker.getInlineConstMap();
Collection<ConstantRef> constantRefs = constMap.get(output.source.getPath());
if (constantRefs == null) return;
@@ -17,7 +17,10 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import static org.jetbrains.jps.util.Iterators.*;
import static org.jetbrains.jps.util.Iterators.filter;
import static org.jetbrains.jps.util.Iterators.find;
import static org.jetbrains.jps.util.Iterators.flat;
import static org.jetbrains.jps.util.Iterators.map;
public final class KotlinIncrementalCacheImpl implements IncrementalCache {
@@ -9,8 +9,6 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.jps.dependency.NodeSource;
import org.jetbrains.jps.dependency.NodeSourcePathMapper;
import java.util.List;
import static org.jetbrains.jps.util.Iterators.isEmpty;
import static org.jetbrains.jps.util.Iterators.map;
@@ -20,10 +18,6 @@ public interface CompilerRunner extends Runner{
ExitCode compile(Iterable<NodeSource> sources, Iterable<NodeSource> deletedSources, DiagnosticSink diagnostic, OutputSink out)
throws Exception;
default Iterable<String> getOutputPathsToDelete() {
return List.of();
}
default void logCompiledFiles(BuildContext context, Iterable<@NotNull NodeSource> toCompile) {
if (!context.isRebuild()) {
BuildProcessLogger logger = context.getBuildLogger();