diff --git a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala index df02ee0ef..af54da662 100644 --- a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala +++ b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala @@ -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 diff --git a/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala b/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala index 87dcc8830..ce6a10c11 100644 --- a/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala +++ b/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala @@ -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) } } diff --git a/sbt b/sbt index a3a29ae20..bdea93f31 100755 --- a/sbt +++ b/sbt @@ -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)"