[2.x] fix: Fix sbtopts files priority in sbt launch script (#8514)

* Fix sbtopts files priority in sbt launch script (fixes #7179)

- Change project .sbtopts from prepend to append so it appears last
This commit is contained in:
SID 2026-01-13 10:30:13 -08:00 committed by GitHub
parent 4ed22747dc
commit bb02c3331c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 148 additions and 16 deletions

View File

@ -122,4 +122,76 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil:
testOutput("--sbt-cache")("--sbt-cache", "./cachePath"): (out: List[String]) =>
assert(out.contains[String]("-Dsbt.global.localcache=./cachePath"))
// Test for issue #7179: sbtopts files priority
testOutput(
"project .sbtopts overrides dist sbtopts",
distSbtoptsContents = "-Dsbt.test.config=dist-default",
sbtOptsFileContents = "-Dsbt.test.config=project-local"
)("-d", "-v"): (out: List[String]) =>
if (isWindows) cancel("Test not supported on windows")
else
// Find the command line section
val cmdLineStart = out.indexWhere(_.contains("Executing command line"))
assert(cmdLineStart >= 0, "Command line section not found")
val cmdLine = out.drop(cmdLineStart + 1).takeWhile(!_.trim.isEmpty)
val distIndex = cmdLine.indexWhere(_.contains("Dsbt.test.config=dist-default"))
val projectIndex = cmdLine.indexWhere(_.contains("Dsbt.test.config=project-local"))
assert(distIndex >= 0, "Dist config not found in command line")
assert(projectIndex >= 0, "Project config not found in command line")
assert(
projectIndex > distIndex,
s"Project config should appear after dist config. distIndex=$distIndex, projectIndex=$projectIndex"
)
testOutput(
"project .sbtopts overrides machine sbtopts",
machineSbtoptsContents = "-Dsbt.test.config=machine-config",
sbtOptsFileContents = "-Dsbt.test.config=project-local"
)("-d", "-v"): (out: List[String]) =>
if (isWindows) cancel("Test not supported on windows")
else
// Find the command line section
val cmdLineStart = out.indexWhere(_.contains("Executing command line"))
assert(cmdLineStart >= 0, "Command line section not found")
val cmdLine = out.drop(cmdLineStart + 1).takeWhile(!_.trim.isEmpty)
val machineIndex = cmdLine.indexWhere(_.contains("Dsbt.test.config=machine-config"))
val projectIndex = cmdLine.indexWhere(_.contains("Dsbt.test.config=project-local"))
assert(machineIndex >= 0, "Machine config not found in command line")
assert(projectIndex >= 0, "Project config not found in command line")
assert(
projectIndex > machineIndex,
s"Project config should appear after machine config. machineIndex=$machineIndex, projectIndex=$projectIndex"
)
testOutput(
"project .sbtopts overrides both dist and machine sbtopts",
distSbtoptsContents = "-Dsbt.test.config=dist-default",
machineSbtoptsContents = "-Dsbt.test.config=machine-config",
sbtOptsFileContents = "-Dsbt.test.config=project-local"
)("-d", "-v"): (out: List[String]) =>
if (isWindows) cancel("Test not supported on windows")
else
// Find the command line section
val cmdLineStart = out.indexWhere(_.contains("Executing command line"))
assert(cmdLineStart >= 0, "Command line section not found")
val cmdLine = out.drop(cmdLineStart + 1).takeWhile(!_.trim.isEmpty)
val distIndex = cmdLine.indexWhere(_.contains("Dsbt.test.config=dist-default"))
val machineIndex = cmdLine.indexWhere(_.contains("Dsbt.test.config=machine-config"))
val projectIndex = cmdLine.indexWhere(_.contains("Dsbt.test.config=project-local"))
// When machine sbtopts exists, the script only loads machine (not dist) due to if-else structure
// So dist should NOT be present, but machine and project should be
assert(distIndex < 0, "Dist config should NOT be present when machine config exists")
assert(machineIndex >= 0, "Machine config not found in command line")
assert(projectIndex >= 0, "Project config not found in command line")
assert(
machineIndex < projectIndex,
s"Machine config should appear before project config. machineIndex=$machineIndex, projectIndex=$projectIndex"
)
end RunnerScriptTest

View File

@ -10,9 +10,9 @@ trait ShellScriptUtil extends BasicTestSuite {
val isWindows: Boolean =
sys.props("os.name").toLowerCase(java.util.Locale.ENGLISH).contains("windows")
private val javaBinDir = new File("launcher-package/integration-test/bin").getAbsolutePath
protected val javaBinDir = new File("launcher-package/integration-test/bin").getAbsolutePath
private def retry[A1](f: () => A1, maxAttempt: Int = 10): A1 =
protected def retry[A1](f: () => A1, maxAttempt: Int = 10): A1 =
try {
f()
} catch {
@ -33,12 +33,18 @@ trait ShellScriptUtil extends BasicTestSuite {
javaOpts: String = "",
sbtOpts: String = "",
sbtOptsFileContents: String = "",
javaToolOptions: String = ""
javaToolOptions: String = "",
distSbtoptsContents: String = "",
machineSbtoptsContents: String = ""
)(args: String*)(f: List[String] => Any) =
test(name) {
val workingDirectory = Files.createTempDirectory("sbt-launcher-package-test").toFile
retry(() => IO.copyDirectory(new File("launcher-package/citest"), workingDirectory))
var sbtHome: Option[File] = None
var configHome: Option[File] = None
var tempSbtHome: Option[File] = None
var testSbtScript: File = sbtScript
try
val sbtOptsFile = new File(workingDirectory, ".sbtopts")
sbtOptsFile.createNewFile()
@ -48,24 +54,77 @@ trait ShellScriptUtil extends BasicTestSuite {
} finally {
writer.close()
}
val envVars = scala.collection.mutable.Map[String, String]()
// Set up dist sbtopts if provided
// Note: sbt script derives sbt_home from script location, not SBT_HOME env var
// Copy the sbt staging directory to a temp location to avoid modifying the staging directory
if (distSbtoptsContents.nonEmpty) {
val originalSbtHome = sbtScript.getParentFile.getParentFile
val tempSbtHomeDir = Files.createTempDirectory("sbt-home-test").toFile
tempSbtHome = Some(tempSbtHomeDir)
// Copy the entire sbt home directory structure
retry(() => IO.copyDirectory(originalSbtHome, tempSbtHomeDir))
// Get the script from the copied directory
val binDir = new File(tempSbtHomeDir, "bin")
testSbtScript = new File(binDir, sbtScript.getName)
// Create dist sbtopts in the copied directory
val distSbtoptsDir = new File(tempSbtHomeDir, "conf")
distSbtoptsDir.mkdirs()
val distSbtoptsFile = new File(distSbtoptsDir, "sbtopts")
IO.write(distSbtoptsFile, distSbtoptsContents)
// Store reference for cleanup
sbtHome = Some(tempSbtHomeDir)
}
// Ensure no machine sbtopts exists when testing dist-only (unless explicitly provided)
// The script only loads dist if machine doesn't exist
if (distSbtoptsContents.nonEmpty && machineSbtoptsContents.isEmpty && configHome.isEmpty) {
// Set XDG_CONFIG_HOME to a temp directory without sbtopts to prevent default machine sbtopts from being found
val emptyConfigHome = Files.createTempDirectory("empty-config-home").toFile
envVars("XDG_CONFIG_HOME") = emptyConfigHome.getAbsolutePath
// Also unset SBT_ETC_FILE if it exists
sys.env.get("SBT_ETC_FILE").foreach(_ => envVars("SBT_ETC_FILE") = "")
// Store for cleanup
configHome = Some(emptyConfigHome)
}
// Set up machine sbtopts if provided
if (machineSbtoptsContents.nonEmpty) {
val configHomeDir = Files.createTempDirectory("config-home").toFile
configHome = Some(configHomeDir)
val machineSbtoptsDir = new File(configHomeDir, "sbt")
machineSbtoptsDir.mkdirs()
val machineSbtoptsFile = new File(machineSbtoptsDir, "sbtopts")
IO.write(machineSbtoptsFile, machineSbtoptsContents)
envVars("XDG_CONFIG_HOME") = configHomeDir.getAbsolutePath
}
val path = sys.env.getOrElse("PATH", sys.env("Path"))
envVars("JAVA_OPTS") = javaOpts
envVars("SBT_OPTS") = sbtOpts
envVars("JAVA_TOOL_OPTIONS") = javaToolOptions
if (isWindows)
envVars("JAVACMD") = new File(javaBinDir, "java").getAbsolutePath()
else
envVars("PATH") = javaBinDir + File.pathSeparator + path
val out = scala.sys.process
.Process(
Seq(sbtScript.getAbsolutePath) ++ args,
Seq(testSbtScript.getAbsolutePath) ++ args,
workingDirectory,
"JAVA_OPTS" -> javaOpts,
"SBT_OPTS" -> sbtOpts,
"JAVA_TOOL_OPTIONS" -> javaToolOptions,
if (isWindows)
"JAVACMD" -> new File(javaBinDir, "java").getAbsolutePath()
else
"PATH" -> (javaBinDir + File.pathSeparator + path)
envVars.toSeq*
)
.!!
.linesIterator
.toList
f(out)
()
finally IO.delete(workingDirectory)
finally
IO.delete(workingDirectory)
// Clean up temporary sbt home directory if we created one
tempSbtHome.foreach(IO.delete)
configHome.foreach(IO.delete)
}
}

9
sbt
View File

@ -858,14 +858,15 @@ original_args=("$@")
# Pull in the machine-wide settings configuration.
if [[ -f "$machine_sbt_opts_file" ]]; then
set -- $(loadConfigFile "$machine_sbt_opts_file") "$@"
set -- "$@" $(loadConfigFile "$machine_sbt_opts_file")
else
# Otherwise pull in the default settings configuration.
[[ -f "$dist_sbt_opts_file" ]] && set -- $(loadConfigFile "$dist_sbt_opts_file") "$@"
[[ -f "$dist_sbt_opts_file" ]] && set -- "$@" $(loadConfigFile "$dist_sbt_opts_file")
fi
# Pull in the project-level config file, if it exists.
[[ -f "$sbt_opts_file" ]] && set -- $(loadConfigFile "$sbt_opts_file") "$@"
# Pull in the project-level config file, if it exists (highest priority, overrides machine/dist).
# Append so it appears last in command line and wins for duplicate properties.
[[ -f "$sbt_opts_file" ]] && set -- "$@" $(loadConfigFile "$sbt_opts_file")
# Pull in the project-level java config, if it exists.
[[ -f ".jvmopts" ]] && export JAVA_OPTS="$JAVA_OPTS $(loadConfigFile .jvmopts)"