diff --git a/.github/workflows/client-test.yml b/.github/workflows/client-test.yml index 4b7c51903..83dc3a1f0 100644 --- a/.github/workflows/client-test.yml +++ b/.github/workflows/client-test.yml @@ -58,10 +58,9 @@ jobs: ./client/target/bin/sbtn --sbt-script=$(pwd)/sbt shutdown # test launcher script echo build using JDK 8 test using JDK 8 and JDK 25 - cd launcher-package - sbt -Dsbt.build.version=$TEST_SBT_VER rpm:packageBin debian:packageBin - sbt -Dsbt.build.version=$TEST_SBT_VER integrationTest/test - cd citest && ./test.sh + sbt -Dsbt.build.version=$TEST_SBT_VER launcherPackage/Rpm/packageBin launcherPackage/Debian/packageBin + sbt -Dsbt.build.version=$TEST_SBT_VER launcherPackageIntegrationTest/test + cd launcher-package/citest && ./test.sh JAVA_HOME="$JAVA_HOME_25_X64" PATH="$JAVA_HOME_25_X64/bin:$PATH" java -Xmx32m -version @@ -73,9 +72,8 @@ jobs: # test building sbtn on macOS ./sbt "-Dsbt.io.virtual=false" nativeImage # test launcher script - cd launcher-package - bin/coursier resolve - sbt -Dsbt.build.version=$TEST_SBT_VER integrationTest/test + launcher-package/bin/coursier resolve + sbt -Dsbt.build.version=$TEST_SBT_VER launcherPackageIntegrationTest/test # This fails due to the JLine issue # cd citest && ./test.sh - name: Client test (Windows) @@ -89,9 +87,8 @@ jobs: ./client/target/bin/sbtn --sbt-script=$(pwd)/launcher-package/src/universal/bin/sbt.bat shutdown # test launcher script echo build using JDK 8 test using JDK 8 and JDK 25 - cd launcher-package - bin/coursier.bat resolve - sbt -Dsbt.build.version=$TEST_SBT_VER integrationTest/test - cd citest + launcher-package/bin/coursier.bat resolve + sbt -Dsbt.build.version=$TEST_SBT_VER launcherPackageIntegrationTest/test + cd launcher-package/citest ./test.bat test3/test3.bat diff --git a/.scalafmt.conf b/.scalafmt.conf index 06a114f59..8888e3585 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -24,5 +24,6 @@ trailingCommas = preserve # TODO update scalafmt and enable Scala 3 project.excludeFilters = [ - "internal/util-position/src/main/scala-3/sbt/internal/util/SourcePositionMacro.scala" + "internal/util-position/src/main/scala-3/sbt/internal/util/SourcePositionMacro.scala", + "launcher-package/integration-test/src/test/scala/*", ] diff --git a/build.sbt b/build.sbt index 37f9e5e55..695456f8f 100644 --- a/build.sbt +++ b/build.sbt @@ -54,6 +54,8 @@ Global / excludeLint := (Global / excludeLint).?.value.getOrElse(Set.empty) Global / excludeLint += componentID Global / excludeLint += scriptedBufferLog Global / excludeLint += checkPluginCross +Global / excludeLint += nativeImageJvm +Global / excludeLint += nativeImageVersion def commonBaseSettings: Seq[Setting[_]] = Def.settings( headerLicense := Some( @@ -1508,3 +1510,32 @@ ThisBuild / publishTo := { else localStaging.value } ThisBuild / publishMavenStyle := true + +lazy val launcherPackage = (project in file("launcher-package")) +lazy val launcherPackageIntegrationTest = + (project in (file("launcher-package") / "integration-test")) + .settings( + name := "integration-test", + scalaVersion := scala3, + libraryDependencies ++= Seq( + scalaVerify % Test, + hedgehog % Test, + // This needs to be hardcoded here, and not use addSbtIO + "org.scala-sbt" %% "io" % "1.10.5" % Test, + ), + testFrameworks += TestFramework("hedgehog.sbt.Framework"), + testFrameworks += TestFramework("verify.runner.Framework"), + Test / test := { + (Test / test) + .dependsOn(launcherPackage / Universal / packageBin) + .dependsOn(launcherPackage / Universal / stage) + .value + }, + Test / testOnly := { + (Test / testOnly) + .dependsOn(launcherPackage / Universal / packageBin) + .dependsOn(launcherPackage / Universal / stage) + .evaluated + }, + Test / parallelExecution := false + ) diff --git a/launcher-package/build.sbt b/launcher-package/build.sbt index 53e00cb55..a835e698e 100755 --- a/launcher-package/build.sbt +++ b/launcher-package/build.sbt @@ -1,7 +1,9 @@ import scala.util.control.Exception.catching -import NativePackagerHelper._ -import com.typesafe.sbt.packager.SettingsHelper._ -import DebianConstants._ +import scala.sys.process.* +import NativePackagerHelper.* +import com.typesafe.sbt.packager.SettingsHelper.* +import DebianConstants.* +import Dependencies.* lazy val sbtOfflineInstall = sys.props.getOrElse("sbt.build.offline", sys.env.getOrElse("sbt.build.offline", "false")) match { @@ -21,12 +23,10 @@ lazy val sbtIncludeSbtLaunch = case "false" | "0" => false case _ => false } -lazy val sbtVersionToRelease = sys.props.getOrElse("sbt.build.version", sys.env.getOrElse("sbt.build.version", { - sys.error("-Dsbt.build.version must be set") - })) +lazy val sbtVersionToRelease = sys.props + .getOrElse("sbt.build.version", sys.env.getOrElse("sbt.build.version", "1.12.0")) lazy val scala210 = "2.10.7" -lazy val scala212 = "2.12.21" lazy val scala210Jline = "org.scala-lang" % "jline" % scala210 lazy val jansi = { if (sbtVersionToRelease startsWith "1.") "org.fusesource.jansi" % "jansi" % "1.12" @@ -69,6 +69,8 @@ val debianBuildId = settingKey[Int]("build id for Debian") val exportRepoUsingCoursier = taskKey[File]("export Maven style repository") val exportRepoCsrDirectory = settingKey[File]("") +val exportRepo = taskKey[File]("export Ivy style repository") +val exportRepoDirectory = settingKey[File]("directory for exported repository") val universalMacPlatform = "universal-apple-darwin" val x86LinuxPlatform = "x86_64-pc-linux" @@ -79,11 +81,10 @@ val x86LinuxImageName = s"sbtn-$x86LinuxPlatform" val aarch64LinuxImageName = s"sbtn-$aarch64LinuxPlatform" val x86WindowsImageName = s"sbtn-$x86WindowsPlatform.exe" -organization in ThisBuild := "org.scala-sbt" -version in ThisBuild := "0.1.0" +Global / excludeLintKeys += bintrayGenericPackagesUrl // This build creates a SBT plugin with handy features *and* bundles the SBT script for distribution. -val root = (project in file(".")). +val launcherPackage = (project in file(".")). enablePlugins(UniversalPlugin, LinuxPlugin, DebianPlugin, RpmPlugin, WindowsPlugin, UniversalDeployPlugin, RpmDeployPlugin, WindowsDeployPlugin). settings( @@ -91,7 +92,7 @@ val root = (project in file(".")). packageName := "sbt", crossTarget := target.value, clean := { - val _ = (clean in dist).value + val _ = (dist / clean).value clean.value }, credentials ++= { @@ -109,13 +110,24 @@ val root = (project in file(".")). sbtLaunchJar := { val uri = sbtLaunchJarUrl.value val file = sbtLaunchJarLocation.value - import dispatch.classic._ if(!file.exists) { // oddly, some places require us to create the file before writing... IO.touch(file) + val url = new java.net.URL(uri) + val connection = url.openConnection() + val input = connection.getInputStream val writer = new java.io.BufferedOutputStream(new java.io.FileOutputStream(file)) - try Http(url(uri) >>> writer) - finally writer.close() + try { + val buffer = new Array[Byte](8192) + var bytesRead = input.read(buffer) + while (bytesRead != -1) { + writer.write(buffer, 0, bytesRead) + bytesRead = input.read(buffer) + } + } finally { + input.close() + writer.close() + } } // TODO - GPG Trust validation. file @@ -135,12 +147,23 @@ val root = (project in file(".")). val linuxX86Tar = t / linuxX86ImageTar val linuxAarch64Tar = t / linuxAarch64ImageTar val windowsZip = t / windowsImageZip - import dispatch.classic._ if(!macosUniversalTar.exists && !isWindows && sbtIncludeSbtn) { IO.touch(macosUniversalTar) + val url = new java.net.URL(s"$baseUrl/v$v/$macosUniversalImageTar") + val connection = url.openConnection() + val input = connection.getInputStream val writer = new java.io.BufferedOutputStream(new java.io.FileOutputStream(macosUniversalTar)) - try Http(url(s"$baseUrl/v$v/$macosUniversalImageTar") >>> writer) - finally writer.close() + try { + val buffer = new Array[Byte](8192) + var bytesRead = input.read(buffer) + while (bytesRead != -1) { + writer.write(buffer, 0, bytesRead) + bytesRead = input.read(buffer) + } + } finally { + input.close() + writer.close() + } val platformDir = t / universalMacPlatform IO.createDirectory(platformDir) s"tar zxvf $macosUniversalTar --directory $platformDir".! @@ -148,9 +171,21 @@ val root = (project in file(".")). } if(!linuxX86Tar.exists && !isWindows && sbtIncludeSbtn) { IO.touch(linuxX86Tar) + val url = new java.net.URL(s"$baseUrl/v$v/$linuxX86ImageTar") + val connection = url.openConnection() + val input = connection.getInputStream val writer = new java.io.BufferedOutputStream(new java.io.FileOutputStream(linuxX86Tar)) - try Http(url(s"$baseUrl/v$v/$linuxX86ImageTar") >>> writer) - finally writer.close() + try { + val buffer = new Array[Byte](8192) + var bytesRead = input.read(buffer) + while (bytesRead != -1) { + writer.write(buffer, 0, bytesRead) + bytesRead = input.read(buffer) + } + } finally { + input.close() + writer.close() + } val platformDir = t / x86LinuxPlatform IO.createDirectory(platformDir) s"""tar zxvf $linuxX86Tar --directory $platformDir""".! @@ -158,9 +193,21 @@ val root = (project in file(".")). } if(!linuxAarch64Tar.exists && !isWindows && sbtIncludeSbtn) { IO.touch(linuxAarch64Tar) + val url = new java.net.URL(s"$baseUrl/v$v/$linuxAarch64ImageTar") + val connection = url.openConnection() + val input = connection.getInputStream val writer = new java.io.BufferedOutputStream(new java.io.FileOutputStream(linuxAarch64Tar)) - try Http(url(s"$baseUrl/v$v/$linuxAarch64ImageTar") >>> writer) - finally writer.close() + try { + val buffer = new Array[Byte](8192) + var bytesRead = input.read(buffer) + while (bytesRead != -1) { + writer.write(buffer, 0, bytesRead) + bytesRead = input.read(buffer) + } + } finally { + input.close() + writer.close() + } val platformDir = t / aarch64LinuxPlatform IO.createDirectory(platformDir) s"""tar zxvf $linuxAarch64Tar --directory $platformDir""".! @@ -168,9 +215,21 @@ val root = (project in file(".")). } if(!windowsZip.exists && sbtIncludeSbtn) { IO.touch(windowsZip) + val url = new java.net.URL(s"$baseUrl/v$v/$windowsImageZip") + val connection = url.openConnection() + val input = connection.getInputStream val writer = new java.io.BufferedOutputStream(new java.io.FileOutputStream(windowsZip)) - try Http(url(s"$baseUrl/v$v/$windowsImageZip") >>> writer) - finally writer.close() + try { + val buffer = new Array[Byte](8192) + var bytesRead = input.read(buffer) + while (bytesRead != -1) { + writer.write(buffer, 0, bytesRead) + bytesRead = input.read(buffer) + } + } finally { + input.close() + writer.close() + } val platformDir = t / x86WindowsPlatform IO.unzip(windowsZip, platformDir) IO.move(platformDir / "sbtn.exe", t / x86WindowsImageName) @@ -200,44 +259,44 @@ val root = (project in file(".")). // DEBIAN SPECIFIC debianBuildId := sys.props.getOrElse("sbt.build.patch", sys.env.getOrElse("DIST_PATCHVER", "0")).toInt, - version in Debian := { + Debian / version := { if (debianBuildId.value == 0) sbtVersionToRelease else sbtVersionToRelease + "." + debianBuildId.value }, // Used to have "openjdk-8-jdk" but that doesn't work on Ubuntu 14.04 https://github.com/sbt/sbt/issues/3105 // before that we had java6-runtime-headless" and that was pulling in JDK9 on Ubuntu 16.04 https://github.com/sbt/sbt/issues/2931 - debianPackageDependencies in Debian ++= Seq("bash (>= 3.2)"), - debianPackageRecommends in Debian += "git", - linuxPackageMappings in Debian += { + Debian / debianPackageDependencies ++= Seq("bash (>= 3.2)", "curl | wget"), + Debian / debianPackageRecommends += "git", + Debian / linuxPackageMappings += { val bd = sourceDirectory.value (packageMapping( (bd / "debian" / "changelog") -> "/usr/share/doc/sbt/changelog.gz" ) withUser "root" withGroup "root" withPerms "0644" gzipped) asDocs() }, - debianChangelog in Debian := { Some(sourceDirectory.value / "debian" / "changelog") }, - addPackage(Debian, packageBin in Debian, "deb"), - debianNativeBuildOptions in Debian := Seq("-Zgzip", "-z3"), + Debian / debianChangelog := { Some(sourceDirectory.value / "debian" / "changelog") }, + addPackage(Debian, (Debian / packageBin), "deb"), + Debian / debianNativeBuildOptions := Seq("-Zgzip", "-z3"), // use the following instead of DebianDeployPlugin to skip changelog - makeDeploymentSettings(Debian, packageBin in Debian, "deb"), + makeDeploymentSettings(Debian, (Debian / packageBin), "deb"), // RPM SPECIFIC rpmRelease := debianBuildId.value.toString, - version in Rpm := { + Rpm / version := { val stable0 = (sbtVersionToRelease split "[^\\d]" filterNot (_.isEmpty) mkString ".") val stable = if (rpmRelease.value == "0") stable0 else stable0 + "." + rpmRelease.value if (isExperimental) ((sbtVersionToRelease split "[^\\d]" filterNot (_.isEmpty)).toList match { - case List(a, b, c, d) => List(0, 99, c, d).mkString(".") + case List(_, _, c, d) => List(0, 99, c, d).mkString(".") }) else stable }, // remove sbtn from RPM because it complains about it being noarch - linuxPackageMappings in Rpm := { - val orig = (linuxPackageMappings in Rpm).value - val nativeMappings = sbtnJarsMappings.value + Rpm / linuxPackageMappings := { + val orig = ((Rpm / linuxPackageMappings)).value + val _ = sbtnJarsMappings.value orig.map(o => o.copy(mappings = o.mappings.toList filterNot { - case (x, p) => p.contains("sbtn-x86_64") || p.contains("sbtn-aarch64") + case (_, p) => p.contains("sbtn-x86_64") || p.contains("sbtn-aarch64") })) }, rpmVendor := "scalacenter", @@ -252,7 +311,7 @@ val root = (project in file(".")). // WINDOWS SPECIFIC windowsBuildId := 0, - version in Windows := { + Windows / version := { val bid = windowsBuildId.value val sv = sbtVersionToRelease (sv split "[^\\d]" filterNot (_.isEmpty)) match { @@ -262,26 +321,26 @@ val root = (project in file(".")). case Array(major) => Seq(major, "0", "0", bid.toString) mkString "." } }, - maintainer in Windows := "Scala Center", - packageSummary in Windows := "sbt " + (version in Windows).value, - packageDescription in Windows := "The interactive build tool.", + Windows / maintainer := "Scala Center", + Windows / packageSummary := "sbt " + (Windows / version).value, + Windows / packageDescription := "The interactive build tool.", wixProductId := "ce07be71-510d-414a-92d4-dff47631848a", - wixProductUpgradeId := Hash.toHex(Hash((version in Windows).value)).take(32), + wixProductUpgradeId := Hash.toHex(Hash((Windows / version).value)).take(32), javacOptions := Seq("-source", "1.8", "-target", "1.8"), // Universal ZIP download install. - packageName in Universal := packageName.value, // needs to be set explicitly due to a bug in native-packager - name in Windows := packageName.value, - packageName in Windows := packageName.value, - version in Universal := sbtVersionToRelease, + Universal / packageName := packageName.value, // needs to be set explicitly due to a bug in native-packager + Windows / name := packageName.value, + Windows / packageName := packageName.value, + Universal / version := sbtVersionToRelease, - mappings in Universal += { + Universal / mappings += { (baseDirectory.value.getParentFile / "sbt") -> ("bin" + java.io.File.separator + "sbt") }, - mappings in Universal := { - val t = (target in Universal).value - val prev = (mappings in Universal).value + Universal / mappings := { + val t = (Universal / target).value + val prev = (Universal / mappings).value val BinSbt = "bin" + java.io.File.separator + "sbt" val BinBat = BinSbt + ".bat" prev.toList map { @@ -307,7 +366,7 @@ val root = (project in file(".")). case (k, v) => (k, v) } }, - mappings in Universal ++= (Def.taskDyn { + Universal / mappings ++= (Def.taskDyn { if (sbtIncludeSbtLaunch) Def.task { Seq( @@ -316,65 +375,46 @@ val root = (project in file(".")). } else Def.task { Seq[(File, String)]() } }).value, - mappings in Universal ++= sbtnJarsMappings.value, - mappings in Universal ++= (Def.taskDyn { + Universal / mappings ++= sbtnJarsMappings.value, + Universal / mappings ++= (Def.taskDyn { if (sbtOfflineInstall && sbtVersionToRelease.startsWith("1.")) Def.task { - val _ = (exportRepoUsingCoursier in dist).value - directory((target in dist).value / "lib") + val _ = ((dist / exportRepoUsingCoursier)).value + directory(((dist / target)).value / "lib") } else if (sbtOfflineInstall) Def.task { - val _ = (exportRepo in dist).value - directory((target in dist).value / "lib") + val _ = ((dist / exportRepo)).value + directory(((dist / target)).value / "lib") } else Def.task { Seq[(File, String)]() } }).value, - mappings in Universal ++= { + Universal / mappings ++= { val base = baseDirectory.value if (sbtVersionToRelease startsWith "0.13.") Nil else Seq[(File, String)](base.getParentFile / "LICENSE" -> "LICENSE", base / "NOTICE" -> "NOTICE") }, - // Misccelaneous publishing stuff... - projectID in Debian := { + // Miscellaneous publishing stuff... + Debian / projectID := { val m = moduleID.value - m.copy(revision = (version in Debian).value) + m.withRevision((Debian / version).value) }, - projectID in Windows := { + Windows / projectID := { val m = moduleID.value - m.copy(revision = (version in Windows).value) + m.withRevision((Windows / version).value) }, - projectID in Rpm := { + Rpm / projectID := { val m = moduleID.value - m.copy(revision = (version in Rpm).value) + m.withRevision((Rpm / version).value) }, - projectID in Universal := { + Universal / projectID := { val m = moduleID.value - m.copy(revision = (version in Universal).value) + m.withRevision((Universal / version).value) } ) -lazy val integrationTest = (project in file("integration-test")) - .settings( - name := "integration-test", - scalaVersion := scala212, - libraryDependencies ++= Seq( - "io.monix" %% "minitest" % "2.3.2" % Test, - "com.eed3si9n.expecty" %% "expecty" % "0.11.0" % Test, - "org.scala-sbt" %% "io" % "1.10.5" % Test - ), - testFrameworks += new TestFramework("minitest.runner.Framework"), - test in Test := { - (test in Test).dependsOn(((packageBin in Universal) in LocalRootProject).dependsOn(((stage in (Universal) in LocalRootProject)))).value - }, - testOnly in Test := { - (testOnly in Test).dependsOn(((packageBin in Universal) in LocalRootProject).dependsOn(((stage in (Universal) in LocalRootProject)))).evaluated - }, - parallelExecution in Test := false - ) - -def downloadUrlForVersion(v: String) = (v split "[^\\d]" flatMap (i => catching(classOf[Exception]) opt (i.toInt))) match { +def downloadUrlForVersion(v: String) = (v.split("[^\\d]") flatMap (i => catching(classOf[Exception]) opt (i.toInt))) match { case Array(0, 11, 3, _*) => "https://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.11.3-2/sbt-launch.jar" case Array(0, 11, x, _*) if x >= 3 => "https://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/"+v+"/sbt-launch.jar" case Array(0, y, _*) if y >= 12 => "https://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/"+v+"/sbt-launch.jar" @@ -383,8 +423,6 @@ def downloadUrlForVersion(v: String) = (v split "[^\\d]" flatMap (i => catching( } def makePublishToForConfig(config: Configuration) = { - val v = sbtVersionToRelease - // Add the publish to and ensure global resolvers has the resolver we just configured. inConfig(config)(Seq( name := "sbt", @@ -406,9 +444,7 @@ def makePublishToForConfig(config: Configuration) = { val resolver = Resolver.url(id, new URL(url))(Patterns(pattern)) Some(resolver) } - )) ++ Seq( - resolvers ++= ((publishTo in config) apply (_.toSeq)).value - ) + )) } def publishToSettings = @@ -416,19 +452,29 @@ def publishToSettings = def downloadUrl(uri: URI, out: File): Unit = { - import dispatch.classic._ if(!out.exists) { IO.touch(out) + val url = new java.net.URL(uri.toString) + val connection = url.openConnection() + val input = connection.getInputStream val writer = new java.io.BufferedOutputStream(new java.io.FileOutputStream(out)) - try Http(url(uri.toString) >>> writer) - finally writer.close() + try { + val buffer = new Array[Byte](8192) + var bytesRead = input.read(buffer) + while (bytesRead != -1) { + writer.write(buffer, 0, bytesRead) + bytesRead = input.read(buffer) + } + } finally { + input.close() + writer.close() + } } } def colonName(m: ModuleID): String = s"${m.organization}:${m.name}:${m.revision}" lazy val dist = (project in file("dist")) - .enablePlugins(ExportRepoPlugin) .settings( name := "dist", scalaVersion := { @@ -437,27 +483,27 @@ lazy val dist = (project in file("dist")) }, libraryDependencies ++= Seq(sbtActual, jansi, scala212Compiler, scala212Jline, scala212Xml) ++ sbt013ExtraDeps, exportRepo := { - val old = exportRepo.value + val outDir = exportRepoDirectory.value sbtVersionToRelease match { case v if v.startsWith("1.") => sys.error("sbt 1.x should use coursier") case v if v.startsWith("0.13.") => - val outbase = exportRepoDirectory.value / "org.scala-sbt" / "compiler-interface" / v + val outbase = outDir / "org.scala-sbt" / "compiler-interface" / v val uribase = s"https://repo.typesafe.com/typesafe/ivy-releases/org.scala-sbt/compiler-interface/$v/" downloadUrl(uri(uribase + "ivys/ivy.xml"), outbase / "ivys" / "ivy.xml") downloadUrl(uri(uribase + "jars/compiler-interface.jar"), outbase / "jars" / "compiler-interface.jar") downloadUrl(uri(uribase + "srcs/compiler-interface-sources.jar"), outbase / "srcs" / "compiler-interface-sources.jar") case _ => } - old + outDir }, exportRepoDirectory := target.value / "lib" / "local-preloaded", exportRepoCsrDirectory := exportRepoDirectory.value, exportRepoUsingCoursier := { val outDirectory = exportRepoCsrDirectory.value val csr = - if (isWindows) (baseDirectory in LocalRootProject).value / "bin" / "coursier.bat" - else (baseDirectory in LocalRootProject).value / "bin" / "coursier" + if (isWindows) (LocalRootProject / baseDirectory).value / "bin" / "coursier.bat" + else (LocalRootProject / baseDirectory).value / "bin" / "coursier" val cache = target.value / "coursier" IO.delete(cache) val v = sbtVersionToRelease @@ -481,7 +527,7 @@ lazy val dist = (project in file("dist")) outDirectory }, conflictWarning := ConflictWarning.disable, - publish := (), - publishLocal := (), + publish := {}, + publishLocal := {}, resolvers += Resolver.typesafeIvyRepo("releases") ) diff --git a/launcher-package/integration-test/src/test/scala/RunnerTest.scala b/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala similarity index 75% rename from launcher-package/integration-test/src/test/scala/RunnerTest.scala rename to launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala index 7a9f38da8..1f4051277 100755 --- a/launcher-package/integration-test/src/test/scala/RunnerTest.scala +++ b/launcher-package/integration-test/src/test/scala/ExtendedRunnerTest.scala @@ -1,30 +1,36 @@ package example.test -import minitest._ -import scala.sys.process._ +import scala.sys.process.* import java.io.File import java.util.Locale import sbt.io.IO +import verify.BasicTestSuite -object SbtRunnerTest extends SimpleTestSuite with PowerAssertions { +object ExtendedRunnerTest extends BasicTestSuite: // 1.3.0, 1.3.0-M4 private[test] val versionRegEx = "\\d(\\.\\d+){2}(-\\w+)?" lazy val isWindows: Boolean = sys.props("os.name").toLowerCase(Locale.ENGLISH).contains("windows") lazy val isMac: Boolean = sys.props("os.name").toLowerCase(Locale.ENGLISH).contains("mac") lazy val sbtScript = - if (isWindows) new File("target/universal/stage/bin/sbt.bat") - else new File("target/universal/stage/bin/sbt") + if (isWindows) new File("launcher-package/target/universal/stage/bin/sbt.bat") + else new File("launcher-package/target/universal/stage/bin/sbt") - def sbtProcess(args: String*) = sbtProcessWithOpts(args: _*)("", "") + def sbtProcess(args: String*) = sbtProcessWithOpts(args*)("", "") def sbtProcessWithOpts(args: String*)(javaOpts: String, sbtOpts: String) = - sbt.internal.Process(Seq(sbtScript.getAbsolutePath) ++ args, new File("citest"), + Process( + Seq(sbtScript.getAbsolutePath) ++ args, + new File("launcher-package/citest"), "JAVA_OPTS" -> javaOpts, - "SBT_OPTS" -> sbtOpts) + "SBT_OPTS" -> sbtOpts + ) def sbtProcessInDir(dir: File)(args: String*) = - sbt.internal.Process(Seq(sbtScript.getAbsolutePath) ++ args, dir, + Process( + Seq(sbtScript.getAbsolutePath) ++ args, + dir, "JAVA_OPTS" -> "", - "SBT_OPTS" -> "") + "SBT_OPTS" -> "" + ) test("sbt runs") { assert(sbtScript.exists) @@ -52,12 +58,12 @@ object SbtRunnerTest extends SimpleTestSuite with PowerAssertions { val out3 = sbtProcess("-V").!!.trim testVersion(out3.linesIterator.toList) } - */ + */ test("sbt -V in empty directory") { IO.withTemporaryDirectory { tmp => val out = sbtProcessInDir(tmp)("-V").!!.trim - val expectedVersion = "^"+versionRegEx+"$" + val expectedVersion = "^" + versionRegEx + "$" val targetDir = new File(tmp, "target") assert(!targetDir.exists, "expected target directory to not exist, but existed") } @@ -71,12 +77,18 @@ object SbtRunnerTest extends SimpleTestSuite with PowerAssertions { assert(out.matches(expectedVersion)) () } - */ + */ test("sbt --sbt-jar should run") { - val out = sbtProcess("compile", "-v", "--sbt-jar", "../target/universal/stage/bin/sbt-launch.jar").!!.linesIterator.toList - assert(out.contains[String]("../target/universal/stage/bin/sbt-launch.jar") || - out.contains[String]("\"../target/universal/stage/bin/sbt-launch.jar\"") + val out = sbtProcess( + "compile", + "-v", + "--sbt-jar", + "../target/universal/stage/bin/sbt-launch.jar" + ).!!.linesIterator.toList + assert( + out.contains[String]("../target/universal/stage/bin/sbt-launch.jar") || + out.contains[String]("\"../target/universal/stage/bin/sbt-launch.jar\"") ) () } @@ -105,7 +117,7 @@ object SbtRunnerTest extends SimpleTestSuite with PowerAssertions { test("sbt --script-version in empty directory") { IO.withTemporaryDirectory { tmp => val out = sbtProcessInDir(tmp)("--script-version").!!.trim - val expectedVersion = "^"+versionRegEx+"$" + val expectedVersion = "^" + versionRegEx + "$" assert(out.matches(expectedVersion)) } () @@ -126,4 +138,4 @@ object SbtRunnerTest extends SimpleTestSuite with PowerAssertions { } () } -} +end ExtendedRunnerTest diff --git a/launcher-package/integration-test/src/test/scala/InheritInput.scala b/launcher-package/integration-test/src/test/scala/InheritInput.scala deleted file mode 100644 index d8304926d..000000000 --- a/launcher-package/integration-test/src/test/scala/InheritInput.scala +++ /dev/null @@ -1,17 +0,0 @@ -package sbt.internal - -import java.lang.{ ProcessBuilder => JProcessBuilder } - -private[sbt] object InheritInput { - def apply(p: JProcessBuilder): Boolean = (redirectInput, inherit) match { - case (Some(m), Some(f)) => - m.invoke(p, f); true - case _ => false - } - - private[this] val pbClass = Class.forName("java.lang.ProcessBuilder") - private[this] val redirectClass = pbClass.getClasses find (_.getSimpleName == "Redirect") - - private[this] val redirectInput = redirectClass map (pbClass.getMethod("redirectInput", _)) - private[this] val inherit = redirectClass map (_ getField "INHERIT" get null) -} diff --git a/launcher-package/integration-test/src/test/scala/PowerAssertions.scala b/launcher-package/integration-test/src/test/scala/PowerAssertions.scala deleted file mode 100644 index eb3671b76..000000000 --- a/launcher-package/integration-test/src/test/scala/PowerAssertions.scala +++ /dev/null @@ -1,7 +0,0 @@ -package example.test - -import com.eed3si9n.expecty.Expecty - -trait PowerAssertions { - lazy val assert: Expecty = new Expecty() -} diff --git a/launcher-package/integration-test/src/test/scala/Process.scala b/launcher-package/integration-test/src/test/scala/Process.scala deleted file mode 100644 index 40798e209..000000000 --- a/launcher-package/integration-test/src/test/scala/Process.scala +++ /dev/null @@ -1,216 +0,0 @@ -package sbt.internal - -import java.lang.{ Process => JProcess, ProcessBuilder => JProcessBuilder } -import java.io.{ Closeable, File, IOException } -import java.io.{ BufferedReader, InputStream, InputStreamReader, OutputStream, PipedInputStream, PipedOutputStream } -import java.net.URL - -trait ProcessExtra { - import Process._ - implicit def builderToProcess(builder: JProcessBuilder): ProcessBuilder = apply(builder) - implicit def fileToProcess(file: File): FilePartialBuilder = apply(file) - implicit def urlToProcess(url: URL): URLPartialBuilder = apply(url) - - implicit def buildersToProcess[T](builders: Seq[T])(implicit convert: T => SourcePartialBuilder): Seq[SourcePartialBuilder] = applySeq(builders) - - implicit def stringToProcess(command: String): ProcessBuilder = apply(command) - implicit def stringSeqToProcess(command: Seq[String]): ProcessBuilder = apply(command) -} - -/** Methods for constructing simple commands that can then be combined. */ -object Process extends ProcessExtra { - def apply(command: String): ProcessBuilder = apply(command, None) - - def apply(command: Seq[String]): ProcessBuilder = apply(command.toArray, None) - - def apply(command: String, arguments: Seq[String]): ProcessBuilder = apply(command :: arguments.toList, None) - /** create ProcessBuilder with working dir set to File and extra environment variables */ - def apply(command: String, cwd: File, extraEnv: (String, String)*): ProcessBuilder = - apply(command, Some(cwd), extraEnv: _*) - /** create ProcessBuilder with working dir set to File and extra environment variables */ - def apply(command: Seq[String], cwd: File, extraEnv: (String, String)*): ProcessBuilder = - apply(command, Some(cwd), extraEnv: _*) - /** create ProcessBuilder with working dir optionally set to File and extra environment variables */ - def apply(command: String, cwd: Option[File], extraEnv: (String, String)*): ProcessBuilder = { - apply(command.split("""\s+"""), cwd, extraEnv: _*) - // not smart to use this on windows, because CommandParser uses \ to escape ". - /*CommandParser.parse(command) match { - case Left(errorMsg) => error(errorMsg) - case Right((cmd, args)) => apply(cmd :: args, cwd, extraEnv : _*) - }*/ - } - /** create ProcessBuilder with working dir optionally set to File and extra environment variables */ - def apply(command: Seq[String], cwd: Option[File], extraEnv: (String, String)*): ProcessBuilder = { - val jpb = new JProcessBuilder(command.toArray: _*) - cwd.foreach(jpb directory _) - extraEnv.foreach { case (k, v) => jpb.environment.put(k, v) } - apply(jpb) - } - def apply(builder: JProcessBuilder): ProcessBuilder = new SimpleProcessBuilder(builder) - def apply(file: File): FilePartialBuilder = new FileBuilder(file) - def apply(url: URL): URLPartialBuilder = new URLBuilder(url) - - def applySeq[T](builders: Seq[T])(implicit convert: T => SourcePartialBuilder): Seq[SourcePartialBuilder] = builders.map(convert) - - def apply(value: Boolean): ProcessBuilder = apply(value.toString, if (value) 0 else 1) - def apply(name: String, exitValue: => Int): ProcessBuilder = new DummyProcessBuilder(name, exitValue) - - def cat(file: SourcePartialBuilder, files: SourcePartialBuilder*): ProcessBuilder = cat(file :: files.toList) - def cat(files: Seq[SourcePartialBuilder]): ProcessBuilder = - { - require(files.nonEmpty) - files.map(_.cat).reduceLeft(_ #&& _) - } -} - -trait SourcePartialBuilder extends NotNull { - /** Writes the output stream of this process to the given file. */ - def #>(f: File): ProcessBuilder = toFile(f, false) - /** Appends the output stream of this process to the given file. */ - def #>>(f: File): ProcessBuilder = toFile(f, true) - /** - * Writes the output stream of this process to the given OutputStream. The - * argument is call-by-name, so the stream is recreated, written, and closed each - * time this process is executed. - */ - def #>(out: => OutputStream): ProcessBuilder = #>(new OutputStreamBuilder(out)) - def #>(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(toSource, b, false, ExitCodes.firstIfNonzero) - private def toFile(f: File, append: Boolean) = #>(new FileOutput(f, append)) - def cat = toSource - protected def toSource: ProcessBuilder -} -trait SinkPartialBuilder extends NotNull { - /** Reads the given file into the input stream of this process. */ - def #<(f: File): ProcessBuilder = #<(new FileInput(f)) - /** Reads the given URL into the input stream of this process. */ - def #<(f: URL): ProcessBuilder = #<(new URLInput(f)) - /** - * Reads the given InputStream into the input stream of this process. The - * argument is call-by-name, so the stream is recreated, read, and closed each - * time this process is executed. - */ - def #<(in: => InputStream): ProcessBuilder = #<(new InputStreamBuilder(in)) - def #<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, toSink, false, ExitCodes.firstIfNonzero) - protected def toSink: ProcessBuilder -} - -trait URLPartialBuilder extends SourcePartialBuilder -trait FilePartialBuilder extends SinkPartialBuilder with SourcePartialBuilder { - def #<<(f: File): ProcessBuilder - def #<<(u: URL): ProcessBuilder - def #<<(i: => InputStream): ProcessBuilder - def #<<(p: ProcessBuilder): ProcessBuilder -} - -/** - * Represents a process that is running or has finished running. - * It may be a compound process with several underlying native processes (such as 'a #&& b`). - */ -trait Process extends NotNull { - /** Blocks until this process exits and returns the exit code.*/ - def exitValue(): Int - /** Destroys this process. */ - def destroy(): Unit -} -/** Represents a runnable process. */ -trait ProcessBuilder extends SourcePartialBuilder with SinkPartialBuilder { - /** - * Starts the process represented by this builder, blocks until it exits, and returns the output as a String. Standard error is - * sent to the console. If the exit code is non-zero, an exception is thrown. - */ - def !! : String - /** - * Starts the process represented by this builder, blocks until it exits, and returns the output as a String. Standard error is - * sent to the provided ProcessLogger. If the exit code is non-zero, an exception is thrown. - */ - def !!(log: ProcessLogger): String - /** - * Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available - * but the process has not completed. Standard error is sent to the console. If the process exits with a non-zero value, - * the Stream will provide all lines up to termination and then throw an exception. - */ - def lines: Stream[String] - /** - * Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available - * but the process has not completed. Standard error is sent to the provided ProcessLogger. If the process exits with a non-zero value, - * the Stream will provide all lines up to termination but will not throw an exception. - */ - def lines(log: ProcessLogger): Stream[String] - /** - * Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available - * but the process has not completed. Standard error is sent to the console. If the process exits with a non-zero value, - * the Stream will provide all lines up to termination but will not throw an exception. - */ - def lines_! : Stream[String] - /** - * Starts the process represented by this builder. The output is returned as a Stream that blocks when lines are not available - * but the process has not completed. Standard error is sent to the provided ProcessLogger. If the process exits with a non-zero value, - * the Stream will provide all lines up to termination but will not throw an exception. - */ - def lines_!(log: ProcessLogger): Stream[String] - /** - * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are - * sent to the console. - */ - def ! : Int - /** - * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are - * sent to the given ProcessLogger. - */ - def !(log: ProcessLogger): Int - /** - * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are - * sent to the console. The newly started process reads from standard input of the current process. - */ - def !< : Int - /** - * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are - * sent to the given ProcessLogger. The newly started process reads from standard input of the current process. - */ - def !<(log: ProcessLogger): Int - /** Starts the process represented by this builder. Standard output and error are sent to the console.*/ - def run(): Process - /** Starts the process represented by this builder. Standard output and error are sent to the given ProcessLogger.*/ - def run(log: ProcessLogger): Process - /** Starts the process represented by this builder. I/O is handled by the given ProcessIO instance.*/ - def run(io: ProcessIO): Process - /** - * Starts the process represented by this builder. Standard output and error are sent to the console. - * The newly started process reads from standard input of the current process if `connectInput` is true. - */ - def run(connectInput: Boolean): Process - /** - * Starts the process represented by this builder, blocks until it exits, and returns the exit code. Standard output and error are - * sent to the given ProcessLogger. - * The newly started process reads from standard input of the current process if `connectInput` is true. - */ - def run(log: ProcessLogger, connectInput: Boolean): Process - - def runBuffered(log: ProcessLogger, connectInput: Boolean): Process - - /** Constructs a command that runs this command first and then `other` if this command succeeds.*/ - def #&&(other: ProcessBuilder): ProcessBuilder - /** Constructs a command that runs this command first and then `other` if this command does not succeed.*/ - def #||(other: ProcessBuilder): ProcessBuilder - /** - * Constructs a command that will run this command and pipes the output to `other`. - * `other` must be a simple command. - * The exit code will be that of `other` regardless of whether this command succeeds. - */ - def #|(other: ProcessBuilder): ProcessBuilder - /** Constructs a command that will run this command and then `other`. The exit code will be the exit code of `other`.*/ - def ###(other: ProcessBuilder): ProcessBuilder - - def canPipeTo: Boolean -} -/** Each method will be called in a separate thread.*/ -final class ProcessIO(val writeInput: OutputStream => Unit, val processOutput: InputStream => Unit, val processError: InputStream => Unit, val inheritInput: JProcessBuilder => Boolean) extends NotNull { - def withOutput(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, process, processError, inheritInput) - def withError(process: InputStream => Unit): ProcessIO = new ProcessIO(writeInput, processOutput, process, inheritInput) - def withInput(write: OutputStream => Unit): ProcessIO = new ProcessIO(write, processOutput, processError, inheritInput) -} -trait ProcessLogger { - def info(s: => String): Unit - def error(s: => String): Unit - def buffer[T](f: => T): T -} diff --git a/launcher-package/integration-test/src/test/scala/ProcessImpl.scala b/launcher-package/integration-test/src/test/scala/ProcessImpl.scala deleted file mode 100644 index 7c8e4bc01..000000000 --- a/launcher-package/integration-test/src/test/scala/ProcessImpl.scala +++ /dev/null @@ -1,433 +0,0 @@ -package sbt.internal - -import java.lang.{ Process => JProcess, ProcessBuilder => JProcessBuilder } -import java.io.{ BufferedReader, Closeable, InputStream, InputStreamReader, IOException, OutputStream, PrintStream } -import java.io.{ FilterInputStream, FilterOutputStream, PipedInputStream, PipedOutputStream } -import java.io.{ File, FileInputStream, FileOutputStream } -import java.net.URL - -/** Runs provided code in a new Thread and returns the Thread instance. */ -private object Spawn { - def apply(f: => Unit): Thread = apply(f, false) - def apply(f: => Unit, daemon: Boolean): Thread = - { - val thread = new Thread() { override def run() = { f } } - thread.setDaemon(daemon) - thread.start() - thread - } -} -private object Future { - def apply[T](f: => T): () => T = - { - val result = new SyncVar[Either[Throwable, T]] - def run(): Unit = - try { result.set(Right(f)) } - catch { case e: Exception => result.set(Left(e)) } - Spawn(run) - () => - result.get match { - case Right(value) => value - case Left(exception) => throw exception - } - } -} - -object BasicIO { - def apply(buffer: StringBuffer, log: Option[ProcessLogger], withIn: Boolean) = new ProcessIO(input(withIn), processFully(buffer), getErr(log), inheritInput(withIn)) - def apply(log: ProcessLogger, withIn: Boolean) = new ProcessIO(input(withIn), processInfoFully(log), processErrFully(log), inheritInput(withIn)) - - def getErr(log: Option[ProcessLogger]) = log match { case Some(lg) => processErrFully(lg); case None => toStdErr } - - private def processErrFully(log: ProcessLogger) = processFully(s => log.error(s)) - private def processInfoFully(log: ProcessLogger) = processFully(s => log.info(s)) - - def closeOut = (_: OutputStream).close() - final val BufferSize = 8192 - final val Newline = System.getProperty("line.separator") - - def close(c: java.io.Closeable) = try { c.close() } catch { case _: java.io.IOException => () } - def processFully(buffer: Appendable): InputStream => Unit = processFully(appendLine(buffer)) - def processFully(processLine: String => Unit): InputStream => Unit = - in => - { - val reader = new BufferedReader(new InputStreamReader(in)) - processLinesFully(processLine)(reader.readLine) - reader.close() - } - def processLinesFully(processLine: String => Unit)(readLine: () => String): Unit = { - def readFully(): Unit = { - val line = readLine() - if (line != null) { - processLine(line) - readFully() - } - } - readFully() - } - def connectToIn(o: OutputStream): Unit = transferFully(Uncloseable protect System.in, o) - def input(connect: Boolean): OutputStream => Unit = if (connect) connectToIn else closeOut - def standard(connectInput: Boolean): ProcessIO = standard(input(connectInput), inheritInput(connectInput)) - def standard(in: OutputStream => Unit, inheritIn: JProcessBuilder => Boolean): ProcessIO = new ProcessIO(in, toStdOut, toStdErr, inheritIn) - - def toStdErr = (in: InputStream) => transferFully(in, System.err) - def toStdOut = (in: InputStream) => transferFully(in, System.out) - - def transferFully(in: InputStream, out: OutputStream): Unit = - try { transferFullyImpl(in, out) } - catch { case _: InterruptedException => () } - - private[this] def appendLine(buffer: Appendable): String => Unit = - line => - { - buffer.append(line) - buffer.append(Newline) - } - - private[this] def transferFullyImpl(in: InputStream, out: OutputStream): Unit = { - val continueCount = 1 //if(in.isInstanceOf[PipedInputStream]) 1 else 0 - val buffer = new Array[Byte](BufferSize) - def read(): Unit = { - val byteCount = in.read(buffer) - if (byteCount >= continueCount) { - out.write(buffer, 0, byteCount) - out.flush() - read - } - } - read - in.close() - } - - def inheritInput(connect: Boolean) = { p: JProcessBuilder => if (connect) InheritInput(p) else false } -} -private[sbt] object ExitCodes { - def ignoreFirst: (Int, Int) => Int = (a, b) => b - def firstIfNonzero: (Int, Int) => Int = (a, b) => if (a != 0) a else b -} - -private[sbt] abstract class AbstractProcessBuilder extends ProcessBuilder with SinkPartialBuilder with SourcePartialBuilder { - def #&&(other: ProcessBuilder): ProcessBuilder = new AndProcessBuilder(this, other) - def #||(other: ProcessBuilder): ProcessBuilder = new OrProcessBuilder(this, other) - def #|(other: ProcessBuilder): ProcessBuilder = - { - require(other.canPipeTo, "Piping to multiple processes is not supported.") - new PipedProcessBuilder(this, other, false, exitCode = ExitCodes.ignoreFirst) - } - def ###(other: ProcessBuilder): ProcessBuilder = new SequenceProcessBuilder(this, other) - - protected def toSource = this - protected def toSink = this - - def run(): Process = run(false) - def run(connectInput: Boolean): Process = run(BasicIO.standard(connectInput)) - def run(log: ProcessLogger): Process = run(log, false) - def run(log: ProcessLogger, connectInput: Boolean): Process = run(BasicIO(log, connectInput)) - - private[this] def getString(log: Option[ProcessLogger], withIn: Boolean): String = - { - val buffer = new StringBuffer - val code = this ! BasicIO(buffer, log, withIn) - if (code == 0) buffer.toString else sys.error("Nonzero exit value: " + code) - } - def !! = getString(None, false) - def !!(log: ProcessLogger) = getString(Some(log), false) - def !!< = getString(None, true) - def !!<(log: ProcessLogger) = getString(Some(log), true) - - def lines: Stream[String] = lines(false, true, None) - def lines(log: ProcessLogger): Stream[String] = lines(false, true, Some(log)) - def lines_! : Stream[String] = lines(false, false, None) - def lines_!(log: ProcessLogger): Stream[String] = lines(false, false, Some(log)) - - private[this] def lines(withInput: Boolean, nonZeroException: Boolean, log: Option[ProcessLogger]): Stream[String] = - { - val streamed = Streamed[String](nonZeroException) - val process = run(new ProcessIO(BasicIO.input(withInput), BasicIO.processFully(streamed.process), BasicIO.getErr(log), BasicIO.inheritInput(withInput))) - Spawn { streamed.done(process.exitValue()) } - streamed.stream() - } - - def ! = run(false).exitValue() - def !< = run(true).exitValue() - def !(log: ProcessLogger) = runBuffered(log, false).exitValue() - def !<(log: ProcessLogger) = runBuffered(log, true).exitValue() - def runBuffered(log: ProcessLogger, connectInput: Boolean) = - log.buffer { run(log, connectInput) } - def !(io: ProcessIO) = run(io).exitValue() - - def canPipeTo = false -} - -private[sbt] class URLBuilder(url: URL) extends URLPartialBuilder with SourcePartialBuilder { - protected def toSource = new URLInput(url) -} -private[sbt] class FileBuilder(base: File) extends FilePartialBuilder with SinkPartialBuilder with SourcePartialBuilder { - protected def toSource = new FileInput(base) - protected def toSink = new FileOutput(base, false) - def #<<(f: File): ProcessBuilder = #<<(new FileInput(f)) - def #<<(u: URL): ProcessBuilder = #<<(new URLInput(u)) - def #<<(s: => InputStream): ProcessBuilder = #<<(new InputStreamBuilder(s)) - def #<<(b: ProcessBuilder): ProcessBuilder = new PipedProcessBuilder(b, new FileOutput(base, true), false, ExitCodes.firstIfNonzero) -} - -private abstract class BasicBuilder extends AbstractProcessBuilder { - protected[this] def checkNotThis(a: ProcessBuilder) = require(a != this, "Compound process '" + a + "' cannot contain itself.") - final def run(io: ProcessIO): Process = - { - val p = createProcess(io) - p.start() - p - } - protected[this] def createProcess(io: ProcessIO): BasicProcess -} -private abstract class BasicProcess extends Process { - def start(): Unit -} - -private abstract class CompoundProcess extends BasicProcess { - def destroy(): Unit = destroyer() - def exitValue() = getExitValue().getOrElse(sys.error("No exit code: process destroyed.")) - - def start() = getExitValue - - protected lazy val (getExitValue, destroyer) = - { - val code = new SyncVar[Option[Int]]() - code.set(None) - val thread = Spawn(code.set(runAndExitValue())) - - ( - Future { thread.join(); code.get }, - () => thread.interrupt() - ) - } - - /** Start and block until the exit value is available and then return it in Some. Return None if destroyed (use 'run')*/ - protected[this] def runAndExitValue(): Option[Int] - - protected[this] def runInterruptible[T](action: => T)(destroyImpl: => Unit): Option[T] = - { - try { Some(action) } - catch { case _: InterruptedException => destroyImpl; None } - } -} - -private abstract class SequentialProcessBuilder(a: ProcessBuilder, b: ProcessBuilder, operatorString: String) extends BasicBuilder { - checkNotThis(a) - checkNotThis(b) - override def toString = " ( " + a + " " + operatorString + " " + b + " ) " -} -private class PipedProcessBuilder(first: ProcessBuilder, second: ProcessBuilder, toError: Boolean, exitCode: (Int, Int) => Int) extends SequentialProcessBuilder(first, second, if (toError) "#|!" else "#|") { - override def createProcess(io: ProcessIO) = new PipedProcesses(first, second, io, toError, exitCode) -} -private class AndProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#&&") { - override def createProcess(io: ProcessIO) = new AndProcess(first, second, io) -} -private class OrProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "#||") { - override def createProcess(io: ProcessIO) = new OrProcess(first, second, io) -} -private class SequenceProcessBuilder(first: ProcessBuilder, second: ProcessBuilder) extends SequentialProcessBuilder(first, second, "###") { - override def createProcess(io: ProcessIO) = new ProcessSequence(first, second, io) -} - -private class SequentialProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO, evaluateSecondProcess: Int => Boolean) extends CompoundProcess { - protected[this] override def runAndExitValue() = - { - val first = a.run(io) - runInterruptible(first.exitValue)(first.destroy()) flatMap - { codeA => - if (evaluateSecondProcess(codeA)) { - val second = b.run(io) - runInterruptible(second.exitValue)(second.destroy()) - } else - Some(codeA) - } - } -} -private class AndProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, _ == 0) -private class OrProcess(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, _ != 0) -private class ProcessSequence(a: ProcessBuilder, b: ProcessBuilder, io: ProcessIO) extends SequentialProcess(a, b, io, ignore => true) - -private class PipedProcesses(a: ProcessBuilder, b: ProcessBuilder, defaultIO: ProcessIO, toError: Boolean, exitCode: (Int, Int) => Int) extends CompoundProcess { - protected[this] override def runAndExitValue() = - { - val currentSource = new SyncVar[Option[InputStream]] - val pipeOut = new PipedOutputStream - val source = new PipeSource(currentSource, pipeOut, a.toString) - source.start() - - val pipeIn = new PipedInputStream(pipeOut) - val currentSink = new SyncVar[Option[OutputStream]] - val sink = new PipeSink(pipeIn, currentSink, b.toString) - sink.start() - - def handleOutOrError(fromOutput: InputStream) = currentSource.put(Some(fromOutput)) - - val firstIO = - if (toError) - defaultIO.withError(handleOutOrError) - else - defaultIO.withOutput(handleOutOrError) - val secondIO = defaultIO.withInput(toInput => currentSink.put(Some(toInput))) - - val second = b.run(secondIO) - val first = a.run(firstIO) - try { - runInterruptible { - val firstResult = first.exitValue - currentSource.put(None) - currentSink.put(None) - val secondResult = second.exitValue - exitCode(firstResult, secondResult) - } { - first.destroy() - second.destroy() - } - } finally { - BasicIO.close(pipeIn) - BasicIO.close(pipeOut) - } - } -} -private class PipeSource(currentSource: SyncVar[Option[InputStream]], pipe: PipedOutputStream, label: => String) extends Thread { - final override def run(): Unit = { - currentSource.get match { - case Some(source) => - try { BasicIO.transferFully(source, pipe) } - catch { case e: IOException => println("I/O error " + e.getMessage + " for process: " + label); e.printStackTrace() } - finally { - BasicIO.close(source) - currentSource.unset() - } - run() - case None => - currentSource.unset() - BasicIO.close(pipe) - } - } -} -private class PipeSink(pipe: PipedInputStream, currentSink: SyncVar[Option[OutputStream]], label: => String) extends Thread { - final override def run(): Unit = { - currentSink.get match { - case Some(sink) => - try { BasicIO.transferFully(pipe, sink) } - catch { case e: IOException => println("I/O error " + e.getMessage + " for process: " + label); e.printStackTrace() } - finally { - BasicIO.close(sink) - currentSink.unset() - } - run() - case None => - currentSink.unset() - } - } -} - -private[sbt] class DummyProcessBuilder(override val toString: String, exitValue: => Int) extends AbstractProcessBuilder { - override def run(io: ProcessIO): Process = new DummyProcess(exitValue) - override def canPipeTo = true -} -/** - * A thin wrapper around a java.lang.Process. `ioThreads` are the Threads created to do I/O. - * The implementation of `exitValue` waits until these threads die before returning. - */ -private class DummyProcess(action: => Int) extends Process { - private[this] val exitCode = Future(action) - override def exitValue() = exitCode() - override def destroy(): Unit = () -} -/** Represents a simple command without any redirection or combination. */ -private[sbt] class SimpleProcessBuilder(p: JProcessBuilder) extends AbstractProcessBuilder { - override def run(io: ProcessIO): Process = - { - import io._ - val inherited = inheritInput(p) - val process = p.start() - - // spawn threads that process the output and error streams, and also write input if not inherited. - if (!inherited) - Spawn(writeInput(process.getOutputStream)) - val outThread = Spawn(processOutput(process.getInputStream)) - val errorThread = - if (!p.redirectErrorStream) - Spawn(processError(process.getErrorStream)) :: Nil - else - Nil - new SimpleProcess(process, outThread :: errorThread) - } - override def toString = p.command.toString - override def canPipeTo = true -} - -/** - * A thin wrapper around a java.lang.Process. `outputThreads` are the Threads created to read from the - * output and error streams of the process. - * The implementation of `exitValue` wait for the process to finish and then waits until the threads reading output and error streams die before - * returning. Note that the thread that reads the input stream cannot be interrupted, see https://github.com/sbt/sbt/issues/327 and - * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4514257 - */ -private class SimpleProcess(p: JProcess, outputThreads: List[Thread]) extends Process { - override def exitValue() = - { - try { - p.waitFor() - } catch { - case _: InterruptedException => p.destroy() - } - outputThreads.foreach(_.join()) // this ensures that all output is complete before returning (waitFor does not ensure this) - p.exitValue() - } - override def destroy() = p.destroy() -} - -private[sbt] class FileOutput(file: File, append: Boolean) extends OutputStreamBuilder(new FileOutputStream(file, append), file.getAbsolutePath) -private[sbt] class URLInput(url: URL) extends InputStreamBuilder(url.openStream, url.toString) -private[sbt] class FileInput(file: File) extends InputStreamBuilder(new FileInputStream(file), file.getAbsolutePath) - -import Uncloseable.protect -private[sbt] class OutputStreamBuilder(stream: => OutputStream, label: String) extends ThreadProcessBuilder(label, _.writeInput(protect(stream))) { - def this(stream: => OutputStream) = this(stream, "") -} -private[sbt] class InputStreamBuilder(stream: => InputStream, label: String) extends ThreadProcessBuilder(label, _.processOutput(protect(stream))) { - def this(stream: => InputStream) = this(stream, "") -} - -private[sbt] abstract class ThreadProcessBuilder(override val toString: String, runImpl: ProcessIO => Unit) extends AbstractProcessBuilder { - override def run(io: ProcessIO): Process = - { - val success = new SyncVar[Boolean] - success.put(false) - new ThreadProcess(Spawn { runImpl(io); success.set(true) }, success) - } -} -private[sbt] final class ThreadProcess(thread: Thread, success: SyncVar[Boolean]) extends Process { - override def exitValue() = - { - thread.join() - if (success.get) 0 else 1 - } - override def destroy(): Unit = thread.interrupt() -} - -object Uncloseable { - def apply(in: InputStream): InputStream = new FilterInputStream(in) { override def close(): Unit = () } - def apply(out: OutputStream): OutputStream = new FilterOutputStream(out) { override def close(): Unit = () } - def protect(in: InputStream): InputStream = if (in eq System.in) Uncloseable(in) else in - def protect(out: OutputStream): OutputStream = if ((out eq System.out) || (out eq System.err)) Uncloseable(out) else out -} -private[sbt] object Streamed { - def apply[T](nonzeroException: Boolean): Streamed[T] = - { - val q = new java.util.concurrent.LinkedBlockingQueue[Either[Int, T]] - def next(): Stream[T] = - q.take match { - case Left(0) => Stream.empty - case Left(code) => if (nonzeroException) sys.error("Nonzero exit code: " + code) else Stream.empty - case Right(s) => Stream.cons(s, next) - } - new Streamed((s: T) => q.put(Right(s)), code => q.put(Left(code)), () => next()) - } -} - -private[sbt] final class Streamed[T](val process: T => Unit, val done: Int => Unit, val stream: () => Stream[T]) extends NotNull \ No newline at end of file diff --git a/launcher-package/integration-test/src/test/scala/RunnerMemoryScriptTest.scala b/launcher-package/integration-test/src/test/scala/RunnerMemoryScriptTest.scala new file mode 100644 index 000000000..3978c4fb3 --- /dev/null +++ b/launcher-package/integration-test/src/test/scala/RunnerMemoryScriptTest.scala @@ -0,0 +1,77 @@ +package example.test + +/** + * RunnerMemoryScriptTest is used to test the sbt shell script, for both macOS/Linux and Windows. + */ +object RunnerMemoryScriptTest extends verify.BasicTestSuite with ShellScriptUtil: + + testOutput("sbt -mem 503")("-mem", "503", "-v"): (out: List[String]) => + assert(out.contains[String]("-Xmx503m")) + + testOutput("sbt with -mem 503, -Xmx in JAVA_OPTS", javaOpts = "-Xmx1024m")("-mem", "503", "-v"): + (out: List[String]) => + assert(out.contains[String]("-Xmx503m")) + assert(!out.contains[String]("-Xmx1024m")) + + testOutput("sbt with -mem 503, -Xmx in SBT_OPTS", sbtOpts = "-Xmx1024m")("-mem", "503", "-v"): + (out: List[String]) => + assert(out.contains[String]("-Xmx503m")) + assert(!out.contains[String]("-Xmx1024m")) + + testOutput("sbt with -mem 503, -Xss in JAVA_OPTS", javaOpts = "-Xss6m")("-mem", "503", "-v"): + (out: List[String]) => + assert(out.contains[String]("-Xmx503m")) + assert(!out.contains[String]("-Xss6m")) + + testOutput("sbt with -mem 503, -Xss in SBT_OPTS", sbtOpts = "-Xss6m")("-mem", "503", "-v"): + (out: List[String]) => + assert(out.contains[String]("-Xmx503m")) + assert(!out.contains[String]("-Xss6m")) + + testOutput( + "sbt with -Xms2048M -Xmx2048M -Xss6M in JAVA_OPTS", + javaOpts = "-Xms2048M -Xmx2048M -Xss6M" + )("-v"): (out: List[String]) => + assert(out.contains[String]("-Xms2048M")) + assert(out.contains[String]("-Xmx2048M")) + assert(out.contains[String]("-Xss6M")) + + testOutput( + "sbt with -Xms2048M -Xmx2048M -Xss6M in SBT_OPTS", + sbtOpts = "-Xms2048M -Xmx2048M -Xss6M" + )("-v"): (out: List[String]) => + assert(out.contains[String]("-Xms2048M")) + assert(out.contains[String]("-Xmx2048M")) + assert(out.contains[String]("-Xss6M")) + + testOutput( + "sbt use .sbtopts file for memory options", + sbtOptsFileContents = """-J-XX:MaxInlineLevel=20 + |-J-Xmx222m + |-J-Xms111m + |-J-Xss12m""".stripMargin + )("compile", "-v"): (out: List[String]) => + assert(out.contains[String]("-XX:MaxInlineLevel=20")) + assert(out.contains[String]("-Xmx222m")) + assert(out.contains[String]("-Xms111m")) + assert(out.contains[String]("-Xss12m")) + + testOutput( + "sbt use JAVA_OPTS for memory options", + javaOpts = "-XX:MaxInlineLevel=20 -Xmx222m -Xms111m -Xss12m" + )("compile", "-v"): (out: List[String]) => + assert(out.contains[String]("-XX:MaxInlineLevel=20")) + assert(out.contains[String]("-Xmx222m")) + assert(out.contains[String]("-Xms111m")) + assert(out.contains[String]("-Xss12m")) + + testOutput( + "sbt use JAVA_TOOL_OPTIONS for memory options", + javaToolOptions = "-XX:MaxInlineLevel=20 -Xmx222m -Xms111m -Xss12m" + )("compile", "-v"): (out: List[String]) => + assert(out.contains[String]("-XX:MaxInlineLevel=20")) + assert(out.contains[String]("-Xmx222m")) + assert(out.contains[String]("-Xms111m")) + assert(out.contains[String]("-Xss12m")) + +end RunnerMemoryScriptTest diff --git a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala new file mode 100644 index 000000000..df02ee0ef --- /dev/null +++ b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala @@ -0,0 +1,125 @@ +package example.test + +/** + * RunnerScriptTest is used to test the sbt shell script, for both macOS/Linux and Windows. + */ +object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil: + + testOutput("sbt -no-colors")("compile", "-no-colors", "-v"): (out: List[String]) => + assert(out.contains[String]("-Dsbt.log.noformat=true")) + + testOutput("sbt --no-colors")("compile", "--no-colors", "-v"): (out: List[String]) => + assert(out.contains[String]("-Dsbt.log.noformat=true")) + + testOutput("sbt --color=false")("compile", "--color=false", "-v"): (out: List[String]) => + assert(out.contains[String]("-Dsbt.color=false")) + + testOutput("sbt --no-colors in SBT_OPTS", sbtOpts = "--no-colors")("compile", "-v"): + (out: List[String]) => + if (isWindows) cancel("Test not supported on windows") + assert(out.contains[String]("-Dsbt.log.noformat=true")) + + testOutput("sbt --no-server")("compile", "--no-server", "-v"): (out: List[String]) => + assert(out.contains[String]("-Dsbt.server.autostart=false")) + + testOutput("sbt --debug-inc")("compile", "--debug-inc", "-v"): (out: List[String]) => + assert(out.contains[String]("-Dxsbt.inc.debug=true")) + + testOutput("sbt --supershell=never")("compile", "--supershell=never", "-v"): + (out: List[String]) => assert(out.contains[String]("-Dsbt.supershell=never")) + + testOutput("sbt --timings")("compile", "--timings", "-v"): (out: List[String]) => + assert(out.contains[String]("-Dsbt.task.timings=true")) + + testOutput("sbt -D arguments")("-Dsbt.supershell=false", "compile", "-v"): (out: List[String]) => + assert(out.contains[String]("-Dsbt.supershell=false")) + + testOutput("sbt --sbt-version")("--sbt-version", "1.3.13", "-v"): (out: List[String]) => + assert(out.contains[String]("-Dsbt.version=1.3.13")) + + testOutput( + name = "sbt with -Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080 in SBT_OPTS", + sbtOpts = "-Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080", + )("-v"): (out: List[String]) => + assert(out.contains[String]("-Dhttp.proxyHost=proxy")) + assert(out.contains[String]("-Dhttp.proxyPort=8080")) + + testOutput( + name = "sbt with -XX:ParallelGCThreads=16 -XX:PermSize=128M in SBT_OPTS", + sbtOpts = "-XX:ParallelGCThreads=16 -XX:PermSize=128M", + )("-v"): (out: List[String]) => + assert(out.contains[String]("-XX:ParallelGCThreads=16")) + assert(out.contains[String]("-XX:PermSize=128M")) + + testOutput( + "sbt with -XX:+UseG1GC -XX:+PrintGC in JAVA_OPTS", + javaOpts = "-XX:+UseG1GC -XX:+PrintGC" + )("-v"): (out: List[String]) => + assert(out.contains[String]("-XX:+UseG1GC")) + assert(out.contains[String]("-XX:+PrintGC")) + assert(!out.contains[String]("-XX:+UseG1GC=-XX:+PrintGC")) + + testOutput( + "sbt with -XX:-UseG1GC -XX:-PrintGC in SBT_OPTS", + sbtOpts = "-XX:+UseG1GC -XX:+PrintGC" + )( + "-v" + ): (out: List[String]) => + assert(out.contains[String]("-XX:+UseG1GC")) + assert(out.contains[String]("-XX:+PrintGC")) + assert(!out.contains[String]("-XX:+UseG1GC=-XX:+PrintGC")) + + testOutput( + "sbt with -debug in SBT_OPTS appears in sbt commands", + javaOpts = "", + sbtOpts = "-debug" + )("compile", "-v"): (out: List[String]) => + if (isWindows) cancel("Test not supported on windows") + + // Debug argument must appear in the 'commands' section (after the sbt-launch.jar argument) to work + val sbtLaunchMatcher = """^.+sbt-launch.jar["]{0,1}$""".r + val locationOfSbtLaunchJarArg = out.zipWithIndex.collectFirst { + case (arg, index) if sbtLaunchMatcher.findFirstIn(arg).nonEmpty => index + } + + assert(locationOfSbtLaunchJarArg.nonEmpty) + + val argsAfterSbtLaunch = out.drop(locationOfSbtLaunchJarArg.get) + assert(argsAfterSbtLaunch.contains("-debug")) + () + + testOutput("sbt --jvm-debug ")("--jvm-debug", "12345", "-v"): (out: List[String]) => + assert( + out.contains[String]("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=12345") + ) + + // Regression test for https://github.com/sbt/sbt/issues/8100 + // Debug agent output in SBT_OPTS should not break the launcher on Windows + testOutput( + "sbt with debug agent in SBT_OPTS", + sbtOpts = "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=12346" + )("-v"): (out: List[String]) => + assert( + out.contains[String]("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=12346") + ) + + testOutput("sbt --no-share adds three system properties")("--no-share"): (out: List[String]) => + assert(out.contains[String]("-Dsbt.global.base=project/.sbtboot")) + assert(out.contains[String]("-Dsbt.boot.directory=project/.boot")) + assert(out.contains[String]("-Dsbt.ivy.home=project/.ivy")) + + testOutput("accept `--ivy` in `SBT_OPTS`", sbtOpts = "--ivy /ivy/dir")("-v"): + (out: List[String]) => + if (isWindows) cancel("Test not supported on windows") + else assert(out.contains[String]("-Dsbt.ivy.home=/ivy/dir")) + + testOutput("sbt --script-version should print sbtVersion")("--script-version"): + (out: List[String]) => + val expectedVersion = "^" + ExtendedRunnerTest.versionRegEx + "$" + assert(out.mkString(System.lineSeparator()).trim.matches(expectedVersion)) + () + + testOutput("--sbt-cache")("--sbt-cache", "./cachePath"): (out: List[String]) => + assert(out.contains[String]("-Dsbt.global.localcache=./cachePath")) + +end RunnerScriptTest diff --git a/launcher-package/integration-test/src/test/scala/ScriptTest.scala b/launcher-package/integration-test/src/test/scala/ScriptTest.scala deleted file mode 100644 index 222987887..000000000 --- a/launcher-package/integration-test/src/test/scala/ScriptTest.scala +++ /dev/null @@ -1,263 +0,0 @@ -package example.test - -import minitest._ -import sbt.io.IO - -import java.io.File -import java.io.PrintWriter -import java.nio.file.Files - -object SbtScriptTest extends SimpleTestSuite with PowerAssertions { - lazy val isWindows: Boolean = - sys.props("os.name").toLowerCase(java.util.Locale.ENGLISH).contains("windows") - lazy val sbtScript = - if (isWindows) new File("target/universal/stage/bin/sbt.bat") - else new File("target/universal/stage/bin/sbt") - - private val javaBinDir = new File("integration-test", "bin").getAbsolutePath - - private def retry[A1](f: () => A1, maxAttempt: Int = 10): A1 = - try { - f() - } catch { - case _ if maxAttempt <= 1 => - Thread.sleep(100) - retry(f, maxAttempt - 1) - } - - def makeTest( - name: String, - javaOpts: String = "", - sbtOpts: String = "", - sbtOptsFileContents: String = "", - javaToolOptions: String = "" - )(args: String*)(f: List[String] => Any) = { - test(name) { - val workingDirectory = Files.createTempDirectory("sbt-launcher-package-test").toFile - retry(() => IO.copyDirectory(new File("citest"), workingDirectory)) - - try { - val sbtOptsFile = new File(workingDirectory, ".sbtopts") - sbtOptsFile.createNewFile() - val writer = new PrintWriter(sbtOptsFile) - try { - writer.write(sbtOptsFileContents) - } finally { - writer.close() - } - val path = sys.env.getOrElse("PATH", sys.env("Path")) - val out = sbt.internal.Process( - Seq(sbtScript.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) - ).!!.linesIterator.toList - f(out) - () - } finally { - IO.delete(workingDirectory) - } - } - } - - makeTest("sbt -no-colors")("compile", "-no-colors", "-v") { out: List[String] => - assert(out.contains[String]("-Dsbt.log.noformat=true")) - } - - makeTest("sbt --no-colors")("compile", "--no-colors", "-v") { out: List[String] => - assert(out.contains[String]("-Dsbt.log.noformat=true")) - } - - makeTest("sbt --color=false")("compile", "--color=false", "-v") { out: List[String] => - assert(out.contains[String]("-Dsbt.color=false")) - } - - makeTest("sbt --no-colors in SBT_OPTS", sbtOpts = "--no-colors")("compile", "-v") { - out: List[String] => - if (isWindows) cancel("Test not supported on windows") - assert(out.contains[String]("-Dsbt.log.noformat=true")) - } - - makeTest("sbt --no-server")("compile", "--no-server", "-v") { out: List[String] => - assert(out.contains[String]("-Dsbt.server.autostart=false")) - } - - makeTest("sbt --debug-inc")("compile", "--debug-inc", "-v") { out: List[String] => - assert(out.contains[String]("-Dxsbt.inc.debug=true")) - } - - makeTest("sbt --supershell=never")("compile", "--supershell=never", "-v") { out: List[String] => - assert(out.contains[String]("-Dsbt.supershell=never")) - } - - makeTest("sbt --timings")("compile", "--timings", "-v") { out: List[String] => - assert(out.contains[String]("-Dsbt.task.timings=true")) - } - - makeTest("sbt -D arguments")("-Dsbt.supershell=false", "compile", "-v") { out: List[String] => - assert(out.contains[String]("-Dsbt.supershell=false")) - } - - makeTest("sbt --sbt-version")("--sbt-version", "1.3.13", "-v") { out: List[String] => - assert(out.contains[String]("-Dsbt.version=1.3.13")) - } - - makeTest("sbt -mem 503")("-mem", "503", "-v") { out: List[String] => - assert(out.contains[String]("-Xmx503m")) - } - - makeTest("sbt with -mem 503, -Xmx in JAVA_OPTS", javaOpts = "-Xmx1024m")("-mem", "503", "-v") { - out: List[String] => - assert(out.contains[String]("-Xmx503m")) - assert(!out.contains[String]("-Xmx1024m")) - } - - makeTest("sbt with -mem 503, -Xmx in SBT_OPTS", sbtOpts = "-Xmx1024m")("-mem", "503", "-v") { - out: List[String] => - assert(out.contains[String]("-Xmx503m")) - assert(!out.contains[String]("-Xmx1024m")) - } - - makeTest("sbt with -mem 503, -Xss in JAVA_OPTS", javaOpts = "-Xss6m")("-mem", "503", "-v") { - out: List[String] => - assert(out.contains[String]("-Xmx503m")) - assert(!out.contains[String]("-Xss6m")) - } - - makeTest("sbt with -mem 503, -Xss in SBT_OPTS", sbtOpts = "-Xss6m")("-mem", "503", "-v") { - out: List[String] => - assert(out.contains[String]("-Xmx503m")) - assert(!out.contains[String]("-Xss6m")) - } - - makeTest( - "sbt with -Xms2048M -Xmx2048M -Xss6M in JAVA_OPTS", - javaOpts = "-Xms2048M -Xmx2048M -Xss6M" - )("-v") { out: List[String] => - assert(out.contains[String]("-Xms2048M")) - assert(out.contains[String]("-Xmx2048M")) - assert(out.contains[String]("-Xss6M")) - } - - makeTest( - "sbt with -Xms2048M -Xmx2048M -Xss6M in SBT_OPTS", - sbtOpts = "-Xms2048M -Xmx2048M -Xss6M" - )("-v") { out: List[String] => - assert(out.contains[String]("-Xms2048M")) - assert(out.contains[String]("-Xmx2048M")) - assert(out.contains[String]("-Xss6M")) - } - - makeTest( - name = "sbt with -Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080 in SBT_OPTS", - sbtOpts = "-Dhttp.proxyHost=proxy -Dhttp.proxyPort=8080", - )("-v") { out: List[String] => - assert(out.contains[String]("-Dhttp.proxyHost=proxy")) - assert(out.contains[String]("-Dhttp.proxyPort=8080")) - } - - makeTest( - name = "sbt with -XX:ParallelGCThreads=16 -XX:PermSize=128M in SBT_OPTS", - sbtOpts = "-XX:ParallelGCThreads=16 -XX:PermSize=128M", - )("-v") { out: List[String] => - assert(out.contains[String]("-XX:ParallelGCThreads=16")) - assert(out.contains[String]("-XX:PermSize=128M")) - } - - makeTest( - "sbt with -XX:+UseG1GC -XX:+PrintGC in JAVA_OPTS", - javaOpts = "-XX:+UseG1GC -XX:+PrintGC" - )("-v") { out: List[String] => - assert(out.contains[String]("-XX:+UseG1GC")) - assert(out.contains[String]("-XX:+PrintGC")) - assert(!out.contains[String]("-XX:+UseG1GC=-XX:+PrintGC")) - } - - makeTest("sbt with -XX:-UseG1GC -XX:-PrintGC in SBT_OPTS", sbtOpts = "-XX:+UseG1GC -XX:+PrintGC")( - "-v" - ) { out: List[String] => - assert(out.contains[String]("-XX:+UseG1GC")) - assert(out.contains[String]("-XX:+PrintGC")) - assert(!out.contains[String]("-XX:+UseG1GC=-XX:+PrintGC")) - } - - makeTest("sbt with -debug in SBT_OPTS appears in sbt commands", javaOpts = "", sbtOpts = "-debug")("compile", "-v") {out: List[String] => - if (isWindows) cancel("Test not supported on windows") - - // Debug argument must appear in the 'commands' section (after the sbt-launch.jar argument) to work - val sbtLaunchMatcher = """^.+sbt-launch.jar["]{0,1}$""".r - val locationOfSbtLaunchJarArg = out.zipWithIndex.collectFirst { - case (arg, index) if sbtLaunchMatcher.findFirstIn(arg).nonEmpty => index - } - - assert(locationOfSbtLaunchJarArg.nonEmpty) - - val argsAfterSbtLaunch = out.drop(locationOfSbtLaunchJarArg.get) - assert(argsAfterSbtLaunch.contains("-debug")) - () - } - - makeTest("sbt --jvm-debug ")("--jvm-debug", "12345", "-v") { out: List[String] => - assert( - out.contains[String]("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=12345") - ) - } - - makeTest("sbt --no-share adds three system properties")("--no-share") { out: List[String] => - assert(out.contains[String]("-Dsbt.global.base=project/.sbtboot")) - assert(out.contains[String]("-Dsbt.boot.directory=project/.boot")) - assert(out.contains[String]("-Dsbt.ivy.home=project/.ivy")) - } - - makeTest("accept `--ivy` in `SBT_OPTS`", sbtOpts = "--ivy /ivy/dir")("-v") { out: List[String] => - if (isWindows) cancel("Test not supported on windows") - assert(out.contains[String]("-Dsbt.ivy.home=/ivy/dir")) - } - - makeTest("sbt --script-version should print sbtVersion")("--script-version") { out: List[String] => - val expectedVersion = "^" + SbtRunnerTest.versionRegEx + "$" - assert(out.mkString(System.lineSeparator()).trim.matches(expectedVersion)) - () - } - - makeTest("--sbt-cache")("--sbt-cache", "./cachePath") { out: List[String] => - assert(out.contains[String](s"-Dsbt.global.localcache=./cachePath")) - } - - makeTest( - "sbt use .sbtopts file for memory options", sbtOptsFileContents = - """-J-XX:MaxInlineLevel=20 - |-J-Xmx222m - |-J-Xms111m - |-J-Xss12m""".stripMargin - - )("compile", "-v") { out: List[String] => - assert(out.contains[String]("-XX:MaxInlineLevel=20")) - assert(out.contains[String]("-Xmx222m")) - assert(out.contains[String]("-Xms111m")) - assert(out.contains[String]("-Xss12m")) - } - - makeTest( - "sbt use JAVA_OPTS for memory options", javaOpts = "-XX:MaxInlineLevel=20 -Xmx222m -Xms111m -Xss12m" - )("compile", "-v") { out: List[String] => - assert(out.contains[String]("-XX:MaxInlineLevel=20")) - assert(out.contains[String]("-Xmx222m")) - assert(out.contains[String]("-Xms111m")) - assert(out.contains[String]("-Xss12m")) - } - - makeTest( - "sbt use JAVA_TOOL_OPTIONS for memory options", javaToolOptions = "-XX:MaxInlineLevel=20 -Xmx222m -Xms111m -Xss12m" - )("compile", "-v") { out: List[String] => - assert(out.contains[String]("-XX:MaxInlineLevel=20")) - assert(out.contains[String]("-Xmx222m")) - assert(out.contains[String]("-Xms111m")) - assert(out.contains[String]("-Xss12m")) - } -} diff --git a/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala b/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala new file mode 100644 index 000000000..87dcc8830 --- /dev/null +++ b/launcher-package/integration-test/src/test/scala/ShellScriptUtil.scala @@ -0,0 +1,71 @@ +package example.test + +import java.io.File +import java.io.PrintWriter +import java.nio.file.Files +import sbt.io.IO +import verify.BasicTestSuite + +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 + + private def retry[A1](f: () => A1, maxAttempt: Int = 10): A1 = + try { + f() + } catch { + case _ if maxAttempt <= 1 => + Thread.sleep(100) + retry(f, maxAttempt - 1) + } + + val sbtScript = + if (isWindows) new File("launcher-package/target/universal/stage/bin/sbt.bat") + else new File("launcher-package/target/universal/stage/bin/sbt") + + /** + * testOutput is a helper function to create a test for shell script. + */ + inline def testOutput( + name: String, + javaOpts: String = "", + sbtOpts: String = "", + sbtOptsFileContents: String = "", + javaToolOptions: 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)) + + try + val sbtOptsFile = new File(workingDirectory, ".sbtopts") + sbtOptsFile.createNewFile() + val writer = new PrintWriter(sbtOptsFile) + try { + writer.write(sbtOptsFileContents) + } finally { + writer.close() + } + val path = sys.env.getOrElse("PATH", sys.env("Path")) + val out = scala.sys.process + .Process( + Seq(sbtScript.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) + ) + .!! + .linesIterator + .toList + f(out) + () + finally IO.delete(workingDirectory) + } +} diff --git a/launcher-package/integration-test/src/test/scala/SyncVar.scala b/launcher-package/integration-test/src/test/scala/SyncVar.scala deleted file mode 100644 index 5754e6da0..000000000 --- a/launcher-package/integration-test/src/test/scala/SyncVar.scala +++ /dev/null @@ -1,38 +0,0 @@ -package sbt.internal - -// minimal copy of scala.concurrent.SyncVar since that version deprecated put and unset -private[sbt] final class SyncVar[A] { - private[this] var isDefined: Boolean = false - private[this] var value: Option[A] = None - - /** Waits until a value is set and then gets it. Does not clear the value */ - def get: A = synchronized { - while (!isDefined) wait() - value.get - } - - /** Waits until a value is set, gets it, and finally clears the value. */ - def take(): A = synchronized { - try get finally unset() - } - - /** Sets the value, whether or not it is currently defined. */ - def set(x: A): Unit = synchronized { - isDefined = true - value = Some(x) - notifyAll() - } - - /** Sets the value, first waiting until it is undefined if it is currently defined. */ - def put(x: A): Unit = synchronized { - while (isDefined) wait() - set(x) - } - - /** Clears the value, whether or not it is current defined. */ - def unset(): Unit = synchronized { - isDefined = false - value = None - notifyAll() - } -} diff --git a/launcher-package/project/PackageSignerPlugin.scala b/launcher-package/project/PackageSignerPlugin.scala deleted file mode 100644 index c81401872..000000000 --- a/launcher-package/project/PackageSignerPlugin.scala +++ /dev/null @@ -1,60 +0,0 @@ -import sbt._ -import Keys._ -import com.jsuereth.sbtpgp.SbtPgp -import com.typesafe.sbt.packager.universal.{ UniversalPlugin, UniversalDeployPlugin } -import com.typesafe.sbt.packager.debian.{ DebianPlugin, DebianDeployPlugin } -import com.typesafe.sbt.packager.rpm.{ RpmPlugin, RpmDeployPlugin } -import com.jsuereth.sbtpgp.gpgExtension - -object PackageSignerPlugin extends sbt.AutoPlugin { - override def trigger = allRequirements - override def requires = SbtPgp && UniversalDeployPlugin && DebianDeployPlugin && RpmDeployPlugin - - import com.jsuereth.sbtpgp.PgpKeys._ - import UniversalPlugin.autoImport._ - import DebianPlugin.autoImport._ - import RpmPlugin.autoImport._ - - override def projectSettings: Seq[Setting[_]] = - inConfig(Universal)(packageSignerSettings) ++ - inConfig(Debian)(packageSignerSettings) ++ - inConfig(Rpm)(packageSignerSettings) - - def subExtension(art: Artifact, ext: String): Artifact = - art.copy(extension = ext) - - def packageSignerSettings: Seq[Setting[_]] = Seq( - signedArtifacts := { - val artifacts = packagedArtifacts.value - val r = pgpSigner.value - val skipZ = (skip in pgpSigner).value - val s = streams.value - if (!skipZ) { - artifacts flatMap { case (art, f) => - Seq(art -> f, - subExtension(art, art.extension + gpgExtension) -> - r.sign(f, file(f.getAbsolutePath + gpgExtension), s)) - } - } - else artifacts - }, - publishSignedConfiguration := Classpaths.publishConfig( - signedArtifacts.value, - None, - resolverName = Classpaths.getPublishTo(publishTo.value).name, - checksums = (checksums in publish).value, - logging = ivyLoggingLevel.value, - overwrite = isSnapshot.value), - publishLocalSignedConfiguration := Classpaths.publishConfig( - signedArtifacts.value, - None, - resolverName = "local", - checksums = (checksums in publish).value, - logging = ivyLoggingLevel.value, - overwrite = isSnapshot.value), - publishSigned := Classpaths.publishTask(publishSignedConfiguration, deliver).value, - publishLocalSigned := Classpaths.publishTask(publishLocalSignedConfiguration, deliver).value - ) - -} - diff --git a/launcher-package/project/build.properties b/launcher-package/project/build.properties deleted file mode 100644 index 8e682c526..000000000 --- a/launcher-package/project/build.properties +++ /dev/null @@ -1 +0,0 @@ -sbt.version=0.13.18 diff --git a/launcher-package/project/plugins.sbt b/launcher-package/project/plugins.sbt deleted file mode 100644 index e4069706c..000000000 --- a/launcher-package/project/plugins.sbt +++ /dev/null @@ -1,4 +0,0 @@ -addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.7.6") -libraryDependencies += "net.databinder" %% "dispatch-http" % "0.8.10" -addSbtPlugin("com.eed3si9n" % "sbt-export-repo" % "0.1.1") -addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 389f0bbb0..1ed315c61 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -6,6 +6,7 @@ object Dependencies { // WARNING: Please Scala update versions in PluginCross.scala too val scala212 = "2.12.21" val scala213 = "2.13.18" + val scala3 = "3.7.4" val checkPluginCross = settingKey[Unit]("Make sure scalaVersion match up") val baseScalaVersion = scala212 def nightlyVersion: Option[String] = diff --git a/project/PackageSignerPlugin.scala b/project/PackageSignerPlugin.scala new file mode 100644 index 000000000..23c4b1b42 --- /dev/null +++ b/project/PackageSignerPlugin.scala @@ -0,0 +1,84 @@ +import sbt.* +import Keys.* +import sbt.internal.librarymanagement.IvyActions +import com.jsuereth.sbtpgp.SbtPgp +import com.typesafe.sbt.packager.universal.{ UniversalPlugin, UniversalDeployPlugin } +import com.typesafe.sbt.packager.debian.{ DebianPlugin, DebianDeployPlugin } +import com.typesafe.sbt.packager.rpm.{ RpmPlugin, RpmDeployPlugin } +import com.jsuereth.sbtpgp.gpgExtension + +object PackageSignerPlugin extends sbt.AutoPlugin { + override def trigger = allRequirements + override def requires = SbtPgp && UniversalDeployPlugin && DebianDeployPlugin && RpmDeployPlugin + + import com.jsuereth.sbtpgp.PgpKeys.* + import UniversalPlugin.autoImport.* + import DebianPlugin.autoImport.* + import RpmPlugin.autoImport.* + + override def projectSettings: Seq[Setting[?]] = + inConfig(Universal)(packageSignerSettings) ++ + inConfig(Debian)(packageSignerSettings) ++ + inConfig(Rpm)(packageSignerSettings) + + def subExtension(art: Artifact, ext: String): Artifact = + art.withExtension(ext) + + def packageSignerSettings: Seq[Setting[?]] = Seq( + signedArtifacts := { + val artifacts = packagedArtifacts.value + val r = pgpSigner.value + val skipZ = (pgpSigner / skip).value + val s = streams.value + if (!skipZ) { + artifacts flatMap { + case (art, f) => + Seq( + art -> f, + subExtension(art, art.extension + gpgExtension) -> + r.sign(f, file(f.getAbsolutePath + gpgExtension), s) + ) + } + } else artifacts + }, + publishSignedConfiguration := Classpaths.publishConfig( + publishMavenStyle = publishMavenStyle.value, + deliverIvyPattern = + (Compile / packageBin / artifactPath).value.getParent + "/[artifact]-[revision](-[classifier]).[ext]", + status = if (isSnapshot.value) "integration" else "release", + configurations = Vector.empty, + artifacts = signedArtifacts.value.toVector, + checksums = (publish / checksums).value.toVector, + resolverName = Classpaths.getPublishTo(publishTo.value).name, + logging = ivyLoggingLevel.value, + overwrite = isSnapshot.value + ), + publishLocalSignedConfiguration := Classpaths.publishConfig( + publishMavenStyle = publishMavenStyle.value, + deliverIvyPattern = + (Compile / packageBin / artifactPath).value.getParent + "/[artifact]-[revision](-[classifier]).[ext]", + status = if (isSnapshot.value) "integration" else "release", + configurations = Vector.empty, + artifacts = signedArtifacts.value.toVector, + checksums = (publish / checksums).value.toVector, + resolverName = "local", + logging = ivyLoggingLevel.value, + overwrite = isSnapshot.value + ), + publishSigned := Def.taskDyn { + val config = publishSignedConfiguration.value + val s = streams.value + Def.task { + IvyActions.publish(ivyModule.value, config, s.log) + } + }.value, + publishLocalSigned := Def.taskDyn { + val config = publishLocalSignedConfiguration.value + val s = streams.value + Def.task { + IvyActions.publish(ivyModule.value, config, s.log) + } + }.value + ) + +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 6e2346199..663914ec5 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -12,3 +12,5 @@ addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.8.1") addSbtPlugin("com.swoval" % "sbt-java-format" % "0.3.1") addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.1") addDependencyTreePlugin +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.3") +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.4")