Skip to content

Commit 05d2c06

Browse files
Merge pull request #98 from lucidsoftware/bfreestone-fixes-source-filepath-determinism-issue
Fix tasty file non-determinism with `sourceroot` argument in multiplex worker and `analysis_store.gz` changes
2 parents b7d7639 + 0fb2ec3 commit 05d2c06

File tree

5 files changed

+188
-8
lines changed

5 files changed

+188
-8
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package sbt.internal.inc
2+
3+
import xsbti.compile.{CompileOptions, MiniSetup}
4+
import java.nio.file.{Path, Paths}
5+
6+
/**
7+
* Zinc's MiniSetup contains scalacOptions which include the -sourceroot flag that references absolute sandbox paths.
8+
* These paths are non-deterministic across builds because the sandbox directory changes (e.g., __sandbox/4/_main vs
9+
* __sandbox/8/_main), making the analysis files non-deterministic.
10+
*
11+
* This class filters out the -sourceroot option from the scalacOptions to ensure deterministic analysis files.
12+
*
13+
* TODO: Consider if there's a better way to handle this upstream in Zinc
14+
*/
15+
16+
object FilteredSetup {
17+
private val sourcerootFlag = "-sourceroot"
18+
19+
def getFilteredSetup(setup: MiniSetup): MiniSetup = {
20+
val options = setup.options()
21+
// Filter out the -sourceroot option and its value
22+
val filteredScalacOptions = {
23+
val originalOptions = options.scalacOptions()
24+
val filtered = scala.collection.mutable.ArrayBuffer[String]()
25+
var i = 0
26+
while (i < originalOptions.length) {
27+
val option = originalOptions(i)
28+
if (option == sourcerootFlag) {
29+
// Skip both the flag and its value (next argument)
30+
i += 2
31+
} else {
32+
filtered += option
33+
i += 1
34+
}
35+
}
36+
filtered.toArray
37+
}
38+
39+
// Create new CompileOptions with filtered scalac options
40+
val newOptions = options.withScalacOptions(filteredScalacOptions)
41+
// Create new MiniSetup with filtered options
42+
setup.withOptions(newOptions)
43+
}
44+
}

src/main/scala/higherkindness/rules_scala/workers/zinc/compile/ZincRunner.scala

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import net.sourceforge.argparse4j.impl.Arguments as Arg
1818
import net.sourceforge.argparse4j.inf.{ArgumentParserException, Namespace}
1919
import sbt.internal.inc.classpath.ClassLoaderCache
2020
import sbt.internal.inc.caching.ClasspathCache
21-
import sbt.internal.inc.{Analysis, AnalyzingCompiler, CompileFailed, FilteredInfos, FilteredRelations, IncrementalCompilerImpl, Locate, PlainVirtualFile, PlainVirtualFileConverter, ZincUtil}
21+
import sbt.internal.inc.{Analysis, AnalyzingCompiler, CompileFailed, FilteredInfos, FilteredRelations, FilteredSetup, IncrementalCompilerImpl, Locate, PlainVirtualFile, PlainVirtualFileConverter, ZincUtil}
2222
import scala.jdk.CollectionConverters.*
2323
import scala.util.Try
2424
import scala.util.control.NonFatal
@@ -212,17 +212,31 @@ object ZincRunner extends WorkerMain[ZincRunnerWorkerConfig] {
212212
isWorker,
213213
)
214214

215+
// Check if we should include the -sourceroot flag in the compiler options
216+
// We only include it for Scala 3 and later versions, as it is not supported
217+
// in Scala 2.x versions.
218+
// We include this so that the TASTy file generated by the Scala compiler
219+
// will be deterministic across machines and directories Bazel uses for
220+
// multiplexed sandbox execution.
221+
val shouldIncludeSourceRoot = !scalaInstance.actualVersion.startsWith("0.") &&
222+
scalaInstance.actualVersion.startsWith("3")
223+
224+
val scalacOptions =
225+
workRequest.plugins.view.map(p => s"-Xplugin:$p").toArray ++
226+
workRequest.compilerOptions ++
227+
workRequest.compilerOptionsReferencingPaths.toArray ++
228+
(if (shouldIncludeSourceRoot)
229+
Array("-sourceroot", task.workDir.toAbsolutePath().toString)
230+
else
231+
Array.empty[String])
232+
215233
val compileOptions =
216234
CompileOptions.create
217235
.withSources(sources.view.map(source => PlainVirtualFile(source.toAbsolutePath().normalize())).toArray)
218236
.withClasspath((classesOutputDir +: deps.view.map(_.classpath)).map(path => PlainVirtualFile(path)).toArray)
219237
.withClassesDirectory(classesOutputDir)
220238
.withJavacOptions(workRequest.javaCompilerOptions)
221-
.withScalacOptions(
222-
workRequest.plugins.view.map(p => s"-Xplugin:$p").toArray ++
223-
workRequest.compilerOptions ++
224-
workRequest.compilerOptionsReferencingPaths.toArray,
225-
)
239+
.withScalacOptions(scalacOptions)
226240

227241
val compilers = {
228242
val scalaCompiler = ZincUtil
@@ -335,14 +349,17 @@ object ZincRunner extends WorkerMain[ZincRunnerWorkerConfig] {
335349
)
336350
}
337351

352+
// Filter out non-deterministic --sourceroot paths from the setup
353+
val filteredSetup = FilteredSetup.getFilteredSetup(compileResult.setup)
354+
338355
val analysisStoreText = AnalysisUtil.getAnalysisStore(
339356
new File(pathString.substring(0, pathString.length() - 3) + ".text.gz"),
340357
true,
341358
readWriteMappers,
342359
)
343360

344-
analysisStoreText.set(AnalysisContents.create(resultAnalysis, compileResult.setup))
345-
analysisStore.set(AnalysisContents.create(resultAnalysis, compileResult.setup))
361+
analysisStoreText.set(AnalysisContents.create(resultAnalysis, filteredSetup))
362+
analysisStore.set(AnalysisContents.create(resultAnalysis, filteredSetup))
346363

347364
// create used deps
348365
val usedDeps =

tests/determinism/BUILD

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
load("@rules_scala_annex//rules:scala.bzl", "scala_library")
2+
3+
# Sample library to test TASTy determinism
4+
scala_library(
5+
name = "tasty_test_lib",
6+
srcs = ["SampleClass.scala"],
7+
)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package determinism.test
2+
3+
class SampleClass {
4+
def method1(): String = "hello"
5+
6+
def method2(x: Int, y: String): Boolean = {
7+
x > 0 && y.nonEmpty
8+
}
9+
10+
private val field = 42
11+
12+
case class InnerCase(name: String, value: Int)
13+
14+
object InnerObject {
15+
def compute(): Int = field * 2
16+
}
17+
}

tests/determinism/test

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/bin/bash -e
2+
. "$(dirname "$0")"/../common.sh
3+
4+
# Test that verifies TASTy files can be generated deterministically
5+
echo "Testing TASTy file generation with Scala 3..."
6+
7+
# Build the Scala 3 target, explicitly using Scala 3 toolchain
8+
echo "Building target with Scala 3 toolchain..."
9+
bazel build --@rules_scala_annex//rules/scala:scala-toolchain=test_zinc_3 --keep_going --remote_executor= --remote_cache= --disk_cache= :tasty_test_lib
10+
11+
# Get the generated jar file
12+
bazel_bin=$(bazel info bazel-bin)
13+
jar_file="$bazel_bin/determinism/tasty_test_lib.jar"
14+
15+
echo "Extracting TASTy files from build..."
16+
temp_dir=$(mktemp -d)
17+
18+
cleanup() {
19+
exit_code=$?
20+
for dir in "${temp_dirs[@]}"; do
21+
rm -r "$dir" 2>/dev/null || true
22+
done
23+
finish $exit_code
24+
}
25+
trap cleanup EXIT
26+
27+
# Extract all .tasty files from the jar
28+
unzip -j "$jar_file" "*.tasty" -d "$temp_dir" 2>/dev/null || {
29+
echo "No .tasty files found in jar, checking if they exist at all..."
30+
unzip -l "$jar_file" | grep -i tasty || {
31+
echo "ERROR: No TASTy files found in the generated jar"
32+
echo "Jar contents:"
33+
unzip -l "$jar_file"
34+
exit 1
35+
}
36+
}
37+
38+
# Verify TASTy files were generated
39+
tasty_files=$(find "$temp_dir" -name "*.tasty" | wc -l)
40+
if [ "$tasty_files" -eq 0 ]; then
41+
echo "ERROR: No TASTy files were extracted"
42+
exit 1
43+
fi
44+
45+
echo "SUCCESS: Found $tasty_files TASTy file(s) generated by Scala 3 compiler"
46+
47+
# Show what files were found
48+
echo "Generated TASTy files:"
49+
find "$temp_dir" -name "*.tasty" -exec basename {} \;
50+
51+
# Test determinism by rebuilding multiple times
52+
echo "Testing determinism by rebuilding 5 times..."
53+
54+
# Array to store temp directories for each build
55+
temp_dirs=("$temp_dir")
56+
57+
# Perform 4 additional builds (we already have one)
58+
for i in $(seq 2 5); do
59+
echo "Build $i/5..."
60+
bazel clean
61+
bazel build --@rules_scala_annex//rules/scala:scala-toolchain=test_zinc_3 --remote_executor= --remote_cache= --disk_cache= :tasty_test_lib
62+
63+
# Create temp directory for this build
64+
temp_dir_n=$(mktemp -d)
65+
temp_dirs+=("$temp_dir_n")
66+
67+
# Extract TASTy files from this build
68+
unzip -j "$jar_file" "*.tasty" -d "$temp_dir_n" 2>/dev/null || {
69+
echo "ERROR: Failed to extract TASTy files from build $i"
70+
exit 1
71+
}
72+
done
73+
74+
# Compare all builds against the first one
75+
echo "Comparing TASTy files across all 5 builds for determinism..."
76+
all_identical=true
77+
78+
for i in $(seq 2 5); do
79+
build_num=$((i-1))
80+
echo "Comparing build 1 with build $i..."
81+
diff_output=$(diff -r "${temp_dirs[0]}" "${temp_dirs[$build_num]}" 2>&1)
82+
if [ $? -ne 0 ]; then
83+
echo "ERROR: TASTy files differ between build 1 and build $i - not deterministic"
84+
echo "Differences found:"
85+
echo "$diff_output"
86+
all_identical=false
87+
fi
88+
done
89+
90+
if [ "$all_identical" = true ]; then
91+
echo "SUCCESS: TASTy files are identical across all 5 builds - build is deterministic!"
92+
else
93+
echo "ERROR: TASTy files differ between builds - not deterministic"
94+
exit 1
95+
fi

0 commit comments

Comments
 (0)