2026-01-12 02:57:59 +01:00
|
|
|
package example.test
|
|
|
|
|
|
|
|
|
|
import java.io.File
|
|
|
|
|
import java.io.PrintWriter
|
|
|
|
|
import java.nio.file.Files
|
|
|
|
|
import sbt.io.IO
|
2026-02-23 07:39:15 +01:00
|
|
|
import scala.collection.mutable
|
|
|
|
|
import scala.sys.process.{ BasicIO, Process, ProcessIO }
|
2026-01-12 02:57:59 +01:00
|
|
|
import verify.BasicTestSuite
|
|
|
|
|
|
|
|
|
|
trait ShellScriptUtil extends BasicTestSuite {
|
|
|
|
|
val isWindows: Boolean =
|
|
|
|
|
sys.props("os.name").toLowerCase(java.util.Locale.ENGLISH).contains("windows")
|
|
|
|
|
|
[2.x] feat: sbtw launcher (#8742)
- Add sbtwProj: Scala 3.x launcher with scopt, drop-in for sbt.bat
- Config: .sbtopts, .jvmopts, sbtconfig.txt, JAVA_OPTS/SBT_OPTS precedence
- Options: --client, --server, --jvm-client, mem, sbt-version, java-home, etc.
- sbt 2.x defaults to native client; --server forces JVM launcher
- JVM run via xsbt.boot.Boot; native via sbtn with --sbt-script
- build.sbt: sbtwProj in root build and allProjects; NativeImagePlugin
- Fixes: JAVA_OPTS then .jvmopts, build.properties trim, shutdownAll PID, Iterator.lastOption
2026-02-16 22:21:30 +01:00
|
|
|
protected val javaBinDir = new File("bin").getAbsolutePath
|
2026-01-12 02:57:59 +01:00
|
|
|
|
2026-01-13 19:30:13 +01:00
|
|
|
protected def retry[A1](f: () => A1, maxAttempt: Int = 10): A1 =
|
2026-01-12 02:57:59 +01:00
|
|
|
try {
|
|
|
|
|
f()
|
|
|
|
|
} catch {
|
2026-01-22 11:35:13 +01:00
|
|
|
case e: Exception if maxAttempt > 1 =>
|
2026-01-12 02:57:59 +01:00
|
|
|
Thread.sleep(100)
|
|
|
|
|
retry(f, maxAttempt - 1)
|
2026-01-22 11:35:13 +01:00
|
|
|
case e: Exception => throw e
|
2026-01-12 02:57:59 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-26 04:51:52 +01:00
|
|
|
def isGitBashTest: Boolean = false
|
|
|
|
|
lazy val sbtScript = IntegrationTestPaths.sbtScript(isWindows && !isGitBashTest)
|
2026-01-12 02:57:59 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* testOutput is a helper function to create a test for shell script.
|
|
|
|
|
*/
|
|
|
|
|
inline def testOutput(
|
|
|
|
|
name: String,
|
|
|
|
|
javaOpts: String = "",
|
|
|
|
|
sbtOpts: String = "",
|
|
|
|
|
sbtOptsFileContents: String = "",
|
2026-01-13 19:30:13 +01:00
|
|
|
javaToolOptions: String = "",
|
|
|
|
|
distSbtoptsContents: String = "",
|
2026-01-19 20:37:08 +01:00
|
|
|
machineSbtoptsContents: String = "",
|
|
|
|
|
jvmoptsFileContents: String = "",
|
|
|
|
|
windowsSupport: Boolean = true,
|
2026-02-08 04:16:21 +01:00
|
|
|
citestVariant: String = "citest",
|
2026-01-12 02:57:59 +01:00
|
|
|
)(args: String*)(f: List[String] => Any) =
|
2026-02-26 04:51:52 +01:00
|
|
|
if isGitBashTest && !isWindows then
|
|
|
|
|
test("gitbash: " + name):
|
|
|
|
|
cancel("skip")
|
|
|
|
|
else if !isGitBashTest && !windowsSupport && isWindows then
|
2026-01-19 20:37:08 +01:00
|
|
|
test(name):
|
|
|
|
|
cancel("test not supported on Windows")
|
|
|
|
|
else
|
2026-02-26 04:51:52 +01:00
|
|
|
test(if isGitBashTest then "gitbash: " + name else name) {
|
2026-01-19 20:37:08 +01:00
|
|
|
val workingDirectory = Files.createTempDirectory("sbt-launcher-package-test").toFile
|
[2.x] feat: sbtw launcher (#8742)
- Add sbtwProj: Scala 3.x launcher with scopt, drop-in for sbt.bat
- Config: .sbtopts, .jvmopts, sbtconfig.txt, JAVA_OPTS/SBT_OPTS precedence
- Options: --client, --server, --jvm-client, mem, sbt-version, java-home, etc.
- sbt 2.x defaults to native client; --server forces JVM launcher
- JVM run via xsbt.boot.Boot; native via sbtn with --sbt-script
- build.sbt: sbtwProj in root build and allProjects; NativeImagePlugin
- Fixes: JAVA_OPTS then .jvmopts, build.properties trim, shutdownAll PID, Iterator.lastOption
2026-02-16 22:21:30 +01:00
|
|
|
val citestDir = IntegrationTestPaths.citestDir(citestVariant)
|
2026-01-22 11:35:13 +01:00
|
|
|
// Clean target directory if it exists to avoid copying temporary files that may be deleted during copy
|
|
|
|
|
val targetDir = new File(citestDir, "target")
|
|
|
|
|
if (targetDir.exists()) {
|
|
|
|
|
try {
|
|
|
|
|
IO.delete(targetDir)
|
|
|
|
|
} catch {
|
|
|
|
|
case _: Exception => // Ignore deletion errors, will retry copy
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Retry copy operation to handle race conditions with temporary files
|
|
|
|
|
retry(() => {
|
|
|
|
|
try {
|
|
|
|
|
IO.copyDirectory(citestDir, workingDirectory)
|
|
|
|
|
} catch {
|
|
|
|
|
case e: java.io.IOException if e.getMessage.contains("does not exist") =>
|
|
|
|
|
// If a file doesn't exist during copy, clean target and retry
|
|
|
|
|
val targetInCitest = new File(citestDir, "target")
|
|
|
|
|
if (targetInCitest.exists()) {
|
|
|
|
|
try {
|
|
|
|
|
IO.delete(targetInCitest)
|
|
|
|
|
} catch {
|
|
|
|
|
case _: Exception => // Ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
throw e // Re-throw to trigger retry
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-01-12 02:57:59 +01:00
|
|
|
|
2026-01-19 20:37:08 +01:00
|
|
|
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()
|
|
|
|
|
val writer = new PrintWriter(sbtOptsFile)
|
|
|
|
|
try {
|
|
|
|
|
writer.write(sbtOptsFileContents)
|
|
|
|
|
} finally {
|
|
|
|
|
writer.close()
|
|
|
|
|
}
|
2026-01-13 19:30:13 +01:00
|
|
|
|
2026-01-19 20:37:08 +01:00
|
|
|
// Create .jvmopts file if contents provided
|
|
|
|
|
if (jvmoptsFileContents.nonEmpty) {
|
|
|
|
|
val jvmoptsFile = new File(workingDirectory, ".jvmopts")
|
|
|
|
|
jvmoptsFile.createNewFile()
|
|
|
|
|
val jvmoptsWriter = new PrintWriter(jvmoptsFile)
|
|
|
|
|
try {
|
|
|
|
|
jvmoptsWriter.write(jvmoptsFileContents)
|
|
|
|
|
} finally {
|
|
|
|
|
jvmoptsWriter.close()
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-01-13 19:30:13 +01:00
|
|
|
|
2026-01-19 20:37:08 +01:00
|
|
|
val envVars = scala.collection.mutable.Map[String, String]()
|
2026-01-13 19:30:13 +01:00
|
|
|
|
2026-01-19 20:37:08 +01:00
|
|
|
// 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)
|
|
|
|
|
}
|
2026-01-13 19:30:13 +01:00
|
|
|
|
2026-01-19 20:37:08 +01:00
|
|
|
// 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)
|
|
|
|
|
}
|
2026-01-13 19:30:13 +01:00
|
|
|
|
2026-01-19 20:37:08 +01:00
|
|
|
// 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
|
|
|
|
|
}
|
2026-01-13 19:30:13 +01:00
|
|
|
|
2026-03-10 04:11:05 +01:00
|
|
|
val path = sys.env.getOrElse("PATH", sys.env.getOrElse("Path", ""))
|
|
|
|
|
val javaHomeEnv = sys.env.getOrElse("JAVA_HOME", System.getProperty("java.home"))
|
2026-01-19 20:37:08 +01:00
|
|
|
envVars("JAVA_OPTS") = javaOpts
|
|
|
|
|
envVars("SBT_OPTS") = sbtOpts
|
|
|
|
|
envVars("JAVA_TOOL_OPTIONS") = javaToolOptions
|
2026-02-26 04:51:52 +01:00
|
|
|
if isWindows then
|
2026-01-19 20:37:08 +01:00
|
|
|
envVars("JAVACMD") = new File(javaBinDir, "java").getAbsolutePath()
|
2026-03-10 04:11:05 +01:00
|
|
|
envVars("JAVA_HOME") = javaHomeEnv
|
2026-01-19 20:37:08 +01:00
|
|
|
else
|
|
|
|
|
envVars("PATH") = javaBinDir + File.pathSeparator + path
|
2026-03-10 04:11:05 +01:00
|
|
|
envVars("JAVA_HOME") = javaHomeEnv
|
2026-02-26 04:51:52 +01:00
|
|
|
val cmd =
|
|
|
|
|
LauncherTestHelper.launcherCommand(testSbtScript.getAbsolutePath, isGitBashTest) ++ args
|
2026-02-23 07:39:15 +01:00
|
|
|
val lines = mutable.ListBuffer.empty[String]
|
|
|
|
|
def processLine(line: String): Unit =
|
|
|
|
|
Console.err.println(line)
|
|
|
|
|
lines.append(line)
|
|
|
|
|
val p = Process(cmd, workingDirectory, envVars.toSeq*)
|
|
|
|
|
.run(
|
|
|
|
|
new ProcessIO(
|
|
|
|
|
_.close(),
|
|
|
|
|
BasicIO.processFully(processLine),
|
|
|
|
|
BasicIO.processFully(processLine)
|
|
|
|
|
)
|
2026-01-19 20:37:08 +01:00
|
|
|
)
|
2026-02-23 07:39:15 +01:00
|
|
|
if p.exitValue != 0 then
|
|
|
|
|
lines.foreach(l => Console.err.println(l))
|
|
|
|
|
sys.error(s"process exit with ${p.exitValue}")
|
|
|
|
|
f(lines.toList)
|
2026-01-19 20:37:08 +01:00
|
|
|
()
|
|
|
|
|
finally
|
|
|
|
|
IO.delete(workingDirectory)
|
|
|
|
|
// Clean up temporary sbt home directory if we created one
|
|
|
|
|
tempSbtHome.foreach(IO.delete)
|
|
|
|
|
configHome.foreach(IO.delete)
|
|
|
|
|
}
|
2026-01-12 02:57:59 +01:00
|
|
|
}
|