Skip to content

Commit

Permalink
fix: Handle race conditions when getting the compiler bridge
Browse files Browse the repository at this point in the history
It was identified in #738 that a race condition could occur when building a project using multiple threads.

To mitigate the issue, I wrapped the installation of the compiler with a synchronized block on the Static instance.

As a result, if multiple threads try to install the compiler at the same time, only one thread will be able to do so. When they will get unlocked, it will check if the file was created meanwhile and if so, to will early return.
  • Loading branch information
acote-coveo authored and slandelle committed Apr 23, 2024
1 parent c6cef98 commit 75e2044
Showing 1 changed file with 93 additions and 80 deletions.
173 changes: 93 additions & 80 deletions src/main/java/sbt_inc/CompilerBridgeFactory.java
Expand Up @@ -130,88 +130,101 @@ private static File getScala2CompilerBridgeJar(
}

if (!cachedCompiledBridgeJar.exists()) {
mavenLogger.info("Compiler bridge file is not installed yet");
// compile and install
RawCompiler rawCompiler =
new RawCompiler(
scalaInstance, ClasspathOptionsUtil.auto(), new MavenLoggerSbtAdapter(mavenLogger));

File bridgeSources =
resolver.getJar(SBT_GROUP_ID, bridgeArtifactId, zincVersion, "sources").getFile();

Set<Path> bridgeSourcesDependencies =
resolver
.getJarAndDependencies(SBT_GROUP_ID, bridgeArtifactId, zincVersion, "sources")
.stream()
.filter(
artifact ->
artifact.getScope() != null && !artifact.getScope().equals("provided"))
.map(Artifact::getFile)
.map(File::toPath)
.collect(Collectors.toSet());

bridgeSourcesDependencies.addAll(
Arrays.stream(scalaInstance.allJars())
.sequential()
.map(File::toPath)
.collect(Collectors.toList()));

Path sourcesDir = Files.createTempDirectory("scala-maven-plugin-compiler-bridge-sources");
Path classesDir = Files.createTempDirectory("scala-maven-plugin-compiler-bridge-classes");

IO.unzip(bridgeSources, sourcesDir.toFile(), AllPassFilter$.MODULE$, true);

List<Path> bridgeSourcesScalaFiles =
FileUtils.listDirectoryContent(
sourcesDir,
file ->
Files.isRegularFile(file) && file.getFileName().toString().endsWith(".scala"));
List<Path> bridgeSourcesNonScalaFiles =
FileUtils.listDirectoryContent(
sourcesDir,
file ->
Files.isRegularFile(file)
&& !file.getFileName().toString().endsWith(".scala")
&& !file.getFileName().toString().equals("MANIFEST.MF"));

try {
rawCompiler.apply(
IterableHasAsScala(bridgeSourcesScalaFiles).asScala().toSeq(), // sources:Seq[File]
IterableHasAsScala(bridgeSourcesDependencies).asScala().toSeq(), // classpath:Seq[File],
classesDir, // outputDirectory:Path,
IterableHasAsScala(Collections.<String>emptyList())
.asScala()
.toSeq() // options:Seq[String]
);

Manifest manifest = new Manifest();
Path sourcesManifestFile = sourcesDir.resolve("META-INF").resolve("MANIFEST.MF");
try (InputStream is = Files.newInputStream(sourcesManifestFile)) {
manifest.read(is);
// Install the compiler in a synchronized block to prevent multithreaded compiles to corrupt
// the installer.
synchronized (CompilerBridgeFactory.class) {
boolean compiledWasInstalledWhileThisThreadWasWaiting = cachedCompiledBridgeJar.exists();

if(compiledWasInstalledWhileThisThreadWasWaiting){
return cachedCompiledBridgeJar;
}

List<Tuple2<File, String>> scalaCompiledClasses =
computeZipEntries(FileUtils.listDirectoryContent(classesDir, file -> true), classesDir);
List<Tuple2<File, String>> resources =
computeZipEntries(bridgeSourcesNonScalaFiles, sourcesDir);
List<Tuple2<File, String>> allZipEntries = new ArrayList<>();
allZipEntries.addAll(scalaCompiledClasses);
allZipEntries.addAll(resources);

IO.jar(
IterableHasAsScala(
allZipEntries.stream()
.map(x -> scala.Tuple2.apply(x._1, x._2))
.collect(Collectors.toList()))
.asScala(),
cachedCompiledBridgeJar,
manifest);

mavenLogger.info("Compiler bridge installed");

} finally {
FileUtils.deleteDirectory(sourcesDir);
FileUtils.deleteDirectory(classesDir);
mavenLogger.info("Compiler bridge file is not installed yet");
// compile and install
RawCompiler rawCompiler =
new RawCompiler(
scalaInstance, ClasspathOptionsUtil.auto(), new MavenLoggerSbtAdapter(mavenLogger));

File bridgeSources =
resolver.getJar(SBT_GROUP_ID, bridgeArtifactId, zincVersion, "sources").getFile();

Set<Path> bridgeSourcesDependencies =
resolver
.getJarAndDependencies(SBT_GROUP_ID, bridgeArtifactId, zincVersion, "sources")
.stream()
.filter(
artifact ->
artifact.getScope() != null && !artifact.getScope().equals("provided"))
.map(Artifact::getFile)
.map(File::toPath)
.collect(Collectors.toSet());

bridgeSourcesDependencies.addAll(
Arrays.stream(scalaInstance.allJars())
.sequential()
.map(File::toPath)
.collect(Collectors.toList()));

Path sourcesDir = Files.createTempDirectory("scala-maven-plugin-compiler-bridge-sources");
Path classesDir = Files.createTempDirectory("scala-maven-plugin-compiler-bridge-classes");

IO.unzip(bridgeSources, sourcesDir.toFile(), AllPassFilter$.MODULE$, true);

List<Path> bridgeSourcesScalaFiles =
FileUtils.listDirectoryContent(
sourcesDir,
file ->
Files.isRegularFile(file) && file.getFileName().toString().endsWith(".scala"));
List<Path> bridgeSourcesNonScalaFiles =
FileUtils.listDirectoryContent(
sourcesDir,
file ->
Files.isRegularFile(file)
&& !file.getFileName().toString().endsWith(".scala")
&& !file.getFileName().toString().equals("MANIFEST.MF"));

try {
rawCompiler.apply(
IterableHasAsScala(bridgeSourcesScalaFiles).asScala().toSeq(), // sources:Seq[File]
IterableHasAsScala(bridgeSourcesDependencies)
.asScala()
.toSeq(), // classpath:Seq[File],
classesDir, // outputDirectory:Path,
IterableHasAsScala(Collections.<String>emptyList())
.asScala()
.toSeq() // options:Seq[String]
);

Manifest manifest = new Manifest();
Path sourcesManifestFile = sourcesDir.resolve("META-INF").resolve("MANIFEST.MF");
try (InputStream is = Files.newInputStream(sourcesManifestFile)) {
manifest.read(is);
}

List<Tuple2<File, String>> scalaCompiledClasses =
computeZipEntries(
FileUtils.listDirectoryContent(classesDir, file -> true), classesDir);
List<Tuple2<File, String>> resources =
computeZipEntries(bridgeSourcesNonScalaFiles, sourcesDir);
List<Tuple2<File, String>> allZipEntries = new ArrayList<>();
allZipEntries.addAll(scalaCompiledClasses);
allZipEntries.addAll(resources);

IO.jar(
IterableHasAsScala(
allZipEntries.stream()
.map(x -> scala.Tuple2.apply(x._1, x._2))
.collect(Collectors.toList()))
.asScala(),
cachedCompiledBridgeJar,
manifest);

mavenLogger.info("Compiler bridge installed");

} finally {
FileUtils.deleteDirectory(sourcesDir);
FileUtils.deleteDirectory(classesDir);
}
}
}

Expand Down

0 comments on commit 75e2044

Please sign in to comment.