├── .java-version ├── ci-test ├── app0 │ ├── build.sbt │ └── sbt.1.3.13.boot.properties ├── app1 │ ├── build.sbt │ └── sbt.0.13.18.boot.properties ├── app2 │ ├── build.sbt │ └── sbt.1.4.0.boot.properties └── test.sh ├── .gitignore ├── project ├── build.properties ├── Release.scala ├── plugins.sbt ├── Deps.scala ├── Util.scala └── Transform.scala ├── launcher-interface ├── NOTICE └── src │ └── main │ └── java │ └── xsbti │ ├── Repository.java │ ├── Manage.java │ ├── CrossValue.java │ ├── PredefinedRepository.java │ ├── GlobalLock.java │ ├── MavenRepository.java │ ├── AppConfiguration.java │ ├── LibraryClassLoader.java │ ├── ExtendedScalaProvider.java │ ├── Continue.java │ ├── Exit.java │ ├── RetrieveException.java │ ├── IvyRepository.java │ ├── MainResult.java │ ├── FullReload.java │ ├── Reboot.java │ ├── ServerMain.java │ ├── AppMain.java │ ├── ScalaProvider.java │ ├── Predefined.java │ ├── Server.java │ ├── ApplicationID.java │ ├── ComponentProvider.java │ ├── AppProvider.java │ └── Launcher.java ├── .github ├── dependabot.yml ├── ISSUE_TEMPLATE │ └── no-new-issues.md └── workflows │ └── ci.yml ├── launcher-implementation └── src │ ├── main │ ├── scala │ │ └── xsbt │ │ │ └── boot │ │ │ ├── Exceptions.scala │ │ │ ├── LibraryClassLoader.scala │ │ │ ├── ParallelExecution.scala │ │ │ ├── Cache.scala │ │ │ ├── Enumeration.scala │ │ │ ├── Using.scala │ │ │ ├── JAnsi.scala │ │ │ ├── CheckProxy.scala │ │ │ ├── Update.scala │ │ │ ├── ListMap.scala │ │ │ ├── ResolveValues.scala │ │ │ ├── FilteredLoader.scala │ │ │ ├── ModuleDefinition.scala │ │ │ ├── PlainApplication.scala │ │ │ ├── Find.scala │ │ │ ├── Create.scala │ │ │ ├── Boot.scala │ │ │ ├── Locks.scala │ │ │ ├── Pre.scala │ │ │ ├── BootConfiguration.scala │ │ │ ├── Configuration.scala │ │ │ ├── ServerApplication.scala │ │ │ ├── LaunchConfiguration.scala │ │ │ ├── CoursierUpdate.scala │ │ │ └── ConfigurationParser.scala │ ├── java │ │ └── xsbt │ │ │ └── boot │ │ │ └── IO.java │ └── input_sources │ │ └── CrossVersionUtil.scala │ └── test │ └── scala │ ├── CacheTest.scala │ ├── ListMapTest.scala │ ├── ServerLocatorTest.scala │ ├── EnumerationTest.scala │ ├── VersionParts.scala │ ├── LocksTest.scala │ ├── PreTest.scala │ ├── URITests.scala │ ├── ScalaProviderTest.scala │ └── ConfigurationParserTest.scala ├── .scalafmt.conf ├── NOTICE ├── licenses ├── LICENSE_Scala └── LICENSE_Apache ├── test-sample └── src │ └── main │ └── scala │ └── xsbt │ └── boot │ └── test │ ├── Apps.scala │ └── Servers.scala ├── README.md └── LICENSE /.java-version: -------------------------------------------------------------------------------- 1 | 1.8 2 | -------------------------------------------------------------------------------- /ci-test/app0/build.sbt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ci-test/app1/build.sbt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ci-test/app2/build.sbt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | metals.sbt 3 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.5 2 | -------------------------------------------------------------------------------- /launcher-interface/NOTICE: -------------------------------------------------------------------------------- 1 | Simple Build Tool: Launcher Interface Component 2 | Copyright 2009, 2010 Mark Harrah 3 | Licensed under BSD-style license (see LICENSE) -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/Repository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | public interface Repository {} -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/Manage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | enum Manage 9 | { 10 | Nop, Clean, Refresh; 11 | } -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/CrossValue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | public enum CrossValue 9 | { 10 | Disabled, Full, Binary; 11 | } 12 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/PredefinedRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | public interface PredefinedRepository extends Repository 9 | { 10 | Predefined id(); 11 | } 12 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/GlobalLock.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.io.File; 9 | import java.util.concurrent.Callable; 10 | 11 | public interface GlobalLock 12 | { 13 | public T apply(File lockFile, Callable run); 14 | } -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/MavenRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.net.URL; 9 | 10 | public interface MavenRepository extends Repository 11 | { 12 | String id(); 13 | URL url(); 14 | boolean allowInsecureProtocol(); 15 | } -------------------------------------------------------------------------------- /project/Release.scala: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.JavaVersionCheckPlugin.autoImport.* 2 | import sbt.* 3 | 4 | object Release { 5 | def settings: Seq[Setting[?]] = javaVersionCheckSettings 6 | 7 | // Validation for java verison 8 | def javaVersionCheckSettings = Seq( 9 | javaVersionCheck / javaVersionPrefix := Some("1.8") 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/no-new-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: No new issues 3 | about: Use https://github.com/sbt/sbt/issues/new/choose instead 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | No new issues should be opened against this repo. 11 | 12 | Please use https://github.com/sbt/sbt/issues/new/choose to file an issue against sbt. 13 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/AppConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.io.File; 9 | 10 | public interface AppConfiguration 11 | { 12 | public String[] arguments(); 13 | public File baseDirectory(); 14 | public AppProvider provider(); 15 | } -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/LibraryClassLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.io.File; 9 | 10 | /** 11 | * Marker interface for classloader with just scala-library. 12 | */ 13 | public interface LibraryClassLoader 14 | { 15 | public String scalaVersion(); 16 | } 17 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/ExtendedScalaProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | public interface ExtendedScalaProvider extends ScalaProvider 9 | { 10 | /** A ClassLoader that loads the classes from scala-library.jar. It will be the parent of `loader` .*/ 11 | public ClassLoader loaderLibraryOnly(); 12 | } 13 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/Continue.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | /** A launched application returns an instance of this class in order to communicate to the launcher 9 | * that the application's main thread is finished and the launcher's work is complete, but it should not exit.*/ 10 | public interface Continue extends MainResult {} -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/Exit.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | /** 9 | * A launched application returns an instance of this class in order to communicate to the launcher 10 | * that the application finished and the launcher should exit with the given exit code. 11 | */ 12 | public interface Exit extends MainResult 13 | { 14 | public int code(); 15 | } -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/RetrieveException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | public final class RetrieveException extends RuntimeException 9 | { 10 | private final String version; 11 | public RetrieveException(String version, String msg) 12 | { 13 | super(msg); 14 | this.version = version; 15 | } 16 | public String version() { return version; } 17 | } -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Exceptions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | // The exception to use when an error occurs at the launcher level (and not a nested exception). 9 | // This indicates overrides toString because the exception class name is not needed to understand 10 | // the error message. 11 | class BootException(override val toString: String) extends RuntimeException(toString) 12 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/IvyRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.net.URL; 9 | 10 | public interface IvyRepository extends Repository 11 | { 12 | String id(); 13 | URL url(); 14 | String ivyPattern(); 15 | String artifactPattern(); 16 | boolean mavenCompatible(); 17 | boolean skipConsistencyCheck(); 18 | boolean descriptorOptional(); 19 | boolean allowInsecureProtocol(); 20 | } 21 | -------------------------------------------------------------------------------- /ci-test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | LAUNCHER="../../launcher-implementation/target/proguard/launcher-implementation-1.4.0-SNAPSHOT-shading.jar" 4 | 5 | pushd ci-test/app0 6 | COURSIER_CACHE=/tmp/cache/ java -jar $LAUNCHER @sbt.1.3.13.boot.properties exit 7 | popd 8 | 9 | pushd ci-test/app1 10 | COURSIER_CACHE=/tmp/cache/ java -jar $LAUNCHER @sbt.0.13.18.boot.properties exit 11 | popd 12 | 13 | pushd ci-test/app2 14 | COURSIER_CACHE=/tmp/cache/ java -jar $LAUNCHER @sbt.1.4.0.boot.properties exit 15 | popd 16 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/LibraryClassLoader.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.net.{ URL, URLClassLoader } 9 | 10 | final class LibraryClassLoader(urls: Array[URL], parent: ClassLoader, val scalaVersion: String) 11 | extends URLClassLoader(urls, parent) 12 | with xsbti.LibraryClassLoader: 13 | override def toString: String = s"LibraryClassLoader(jar = ${urls.head}, parent = $parent)" 14 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/MainResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | /** 9 | * A launched application should return an instance of this from its 'run' method 10 | * to communicate to the launcher what should be done now that the application 11 | * has completed. This interface should be treated as 'sealed', with Exit and Reboot the only 12 | * direct subtypes. 13 | * 14 | * @see xsbti.Exit 15 | * @see xsbti.Reboot 16 | */ 17 | public interface MainResult {} -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/CacheTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import org.scalacheck.* 9 | import Prop.* 10 | 11 | object CacheTest extends Properties("Cache"): 12 | property("Cache") = Prop.forAll { (_: Int, keys: List[Int], map: Int => Int) => 13 | val cache = new Cache((i: Int, _: Unit) => map(i)) 14 | def toProperty(key: Int) = 15 | ("Key " + key) |: ("Value: " + map(key)) |: (cache.apply(key, ()) == map(key)) 16 | Prop.all(keys.map(toProperty)*) 17 | } 18 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/ParallelExecution.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.util.concurrent.Executors 9 | 10 | import scala.concurrent.ExecutionContext 11 | 12 | private[xsbt] object ParallelExecution: 13 | protected[xsbt] val executionContext = 14 | // ExecutionContext.fromExecutor(Executors.newCachedThreadPool()) 15 | ExecutionContext.fromExecutor( 16 | Executors.newFixedThreadPool( 17 | Runtime.getRuntime.availableProcessors() 18 | ) 19 | ) 20 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.typesafe.sbt" % "sbt-javaversioncheck" % "0.1.0") 2 | 3 | addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") 4 | addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.3.1") 5 | addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "1.1.4") 6 | addSbtPlugin("com.github.sbt" % "sbt-proguard" % "0.5.0") 7 | addSbtPlugin("com.eed3si9n" % "sbt-nocomma" % "0.1.2") 8 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.5") 9 | addSbtPlugin("io.get-coursier" % "sbt-shading" % "2.1.5") 10 | addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") 11 | 12 | scalacOptions += "-feature" 13 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/FullReload.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | public final class FullReload extends RuntimeException 9 | { 10 | private final String[] arguments; 11 | private final boolean clean; 12 | public FullReload(String[] arguments) 13 | { 14 | this.arguments = arguments; 15 | this.clean = false; 16 | } 17 | public FullReload(String[] arguments, boolean clean) 18 | { 19 | this.arguments = arguments; 20 | this.clean = clean; 21 | } 22 | public boolean clean() { return clean; } 23 | public String[] arguments() { return arguments; } 24 | } -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/Reboot.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.io.File; 9 | 10 | /** 11 | * A launched application returns an instance of this class in order to communicate to the launcher 12 | * that the application should be restarted. Different versions of the application and Scala can be used. 13 | * The application can be given different arguments as well as a new working directory. 14 | */ 15 | public interface Reboot extends MainResult 16 | { 17 | public String[] arguments(); 18 | public File baseDirectory(); 19 | public String scalaVersion(); 20 | public ApplicationID app(); 21 | } -------------------------------------------------------------------------------- /project/Deps.scala: -------------------------------------------------------------------------------- 1 | import sbt.* 2 | import sbt.Keys.* 3 | 4 | object Deps { 5 | def lib(m: ModuleID) = libraryDependencies += m 6 | lazy val sbtIo = "org.scala-sbt" %% "io" % "1.10.5" 7 | lazy val scalacheck = "org.scalacheck" %% "scalacheck" % "1.18.1" 8 | lazy val junit = "junit" % "junit" % "4.13.2" 9 | lazy val verify = "com.eed3si9n.verify" %% "verify" % "1.0.0" 10 | 11 | val coursierVersion = "2.1.23" 12 | lazy val coursier = ("io.get-coursier" %% "coursier" % coursierVersion) 13 | .cross(CrossVersion.for3Use2_13) 14 | .exclude("org.codehaus.plexus", "plexus-archiver") 15 | .exclude("org.codehaus.plexus", "plexus-container-default") 16 | .exclude("org.codehaus.plexus", "plexus-container-default") 17 | } 18 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/ServerMain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | /** The main entry point for a launched service. This allows applciations 9 | * to instantiate server instances. 10 | */ 11 | public interface ServerMain { 12 | /** 13 | * This method should launch one or more thread(s) which run the service. After the service has 14 | * been started, it should return the port/URI it is listening for connections on. 15 | * 16 | * @param configuration 17 | * The configuration used to launch this service. 18 | * @return 19 | * A running server. 20 | */ 21 | public Server start(AppConfiguration configuration); 22 | } -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Cache.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.lang.ref.{ Reference, SoftReference } 9 | import java.util.HashMap 10 | 11 | final class Cache[K, X, V](create: (K, X) => V): 12 | private val delegate = new HashMap[K, Reference[V]] 13 | def apply(k: K, x: X): V = synchronized { getFromReference(k, x, delegate.get(k)) } 14 | private def getFromReference(k: K, x: X, existingRef: Reference[V]) = 15 | if existingRef eq null then newEntry(k, x) else get(k, x, existingRef.get) 16 | private def get(k: K, x: X, existing: V) = 17 | if existing == null then newEntry(k, x) else existing 18 | private def newEntry(k: K, x: X): V = 19 | val v = create(k, x) 20 | Pre.assert(v != null, "Value for key " + k + " was null") 21 | delegate.put(k, new SoftReference(v)) 22 | v 23 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/AppMain.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | /** 9 | * The main entry interface for launching applications. Classes which implement this interface 10 | * can be launched via the sbt launcher. 11 | * 12 | * In addition, classes can be adapted into this interface by the launcher if they have a static method 13 | * matching one of these signatures: 14 | * 15 | * - public static void main(String[] args) 16 | * - public static int main(String[] args) 17 | * - public static xsbti.Exit main(String[] args) 18 | * 19 | */ 20 | public interface AppMain 21 | { 22 | /** Run the application and return the result. 23 | * 24 | * @param configuration The configuration used to run the application. Includes arguments and access to launcher features. 25 | * @return 26 | * The result of running this app. 27 | * Note: the result can be things like "Please reboot this application". 28 | */ 29 | public MainResult run(AppConfiguration configuration); 30 | } -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.9.9 2 | runner.dialect = scala3 3 | maxColumn = 100 4 | project.git = true 5 | lineEndings = preserve 6 | 7 | fileOverride { 8 | "glob:**/project/**" { 9 | runner.dialect = scala212source3 10 | } 11 | "glob:**/*.sbt" { 12 | runner.dialect = scala212source3 13 | } 14 | } 15 | 16 | # https://docs.scala-lang.org/style/scaladoc.html recommends the JavaDoc style. 17 | # scala/scala is written that way too https://github.com/scala/scala/blob/v2.12.2/src/library/scala/Predef.scala 18 | docstrings.style = Asterisk 19 | docstrings.wrap = false 20 | 21 | # This also seems more idiomatic to include whitespace in import x.{ yyy } 22 | spaces.inImportCurlyBraces = true 23 | 24 | # This is more idiomatic Scala. 25 | # https://docs.scala-lang.org/style/indentation.html#methods-with-numerous-arguments 26 | align.openParenCallSite = false 27 | align.openParenDefnSite = false 28 | 29 | # For better code clarity 30 | danglingParentheses.preset = true 31 | 32 | trailingCommas = preserve 33 | 34 | rewrite.scala3.convertToNewSyntax = true 35 | rewrite.scala3.newSyntax.control = true 36 | rewrite.scala3.removeOptionalBraces = yes 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | 6 | jobs: 7 | test: 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - os: ubuntu-latest 13 | java: 8 14 | distribution: zulu 15 | runs-on: ${{ matrix.os }} 16 | env: 17 | # define Java options for both official sbt and sbt-extras 18 | JAVA_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 19 | JVM_OPTS: -Xms2048M -Xmx2048M -Xss6M -XX:ReservedCodeCacheSize=256M -Dfile.encoding=UTF-8 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v6 23 | - name: Setup JDK 24 | uses: actions/setup-java@v5 25 | with: 26 | distribution: "${{ matrix.distribution }}" 27 | java-version: "${{ matrix.java }}" 28 | cache: sbt 29 | - name: Setup sbt 30 | uses: sbt/setup-sbt@v1 31 | - name: Build and test 32 | run: | 33 | sbt -v "mimaReportBinaryIssues; scalafmtSbtCheck; scalafmtCheckAll; packageBin; shadedPackageBin; test;" 34 | ci-test/test.sh 35 | shell: bash 36 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Enumeration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import scala.collection.immutable.List 10 | 11 | class Enumeration extends Serializable: 12 | def elements: List[Value] = members 13 | private lazy val members: List[Value] = 14 | val c = getClass 15 | val correspondingFields = ListMap(c.getDeclaredFields.map(f => (f.getName, f))*) 16 | c.getMethods.toList flatMap { method => 17 | if method.getParameterTypes.length == 0 && classOf[Value].isAssignableFrom( 18 | method.getReturnType 19 | ) 20 | then 21 | for ( 22 | field <- correspondingFields.get(method.getName) 23 | if field.getType == method.getReturnType 24 | ) yield method.invoke(this).asInstanceOf[Value] 25 | else Nil 26 | } 27 | def value(s: String) = new Value(s, 0) 28 | def value(s: String, i: Int) = new Value(s, i) 29 | final class Value(override val toString: String, val id: Int) extends Serializable 30 | def toValue(s: String): Value = 31 | elements 32 | .find(_.toString == s) 33 | .getOrElse(error("expected one of " + elements.mkString(",") + " (got: " + s + ")")) 34 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/ScalaProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.io.File; 9 | 10 | /** Provides access to the jars and classes for a particular version of Scala.*/ 11 | public interface ScalaProvider 12 | { 13 | public Launcher launcher(); 14 | /** The version of Scala this instance provides.*/ 15 | public String version(); 16 | 17 | /** A ClassLoader that loads the classes from scala-library.jar and scala-compiler.jar.*/ 18 | public ClassLoader loader(); 19 | /** Returns the scala-library.jar and scala-compiler.jar for this version of Scala. */ 20 | public File[] jars(); 21 | 22 | /**@deprecated Only `jars` can be reliably provided for modularized Scala. (Since 0.13.0) */ 23 | @Deprecated 24 | public File libraryJar(); 25 | 26 | /**@deprecated Only `jars` can be reliably provided for modularized Scala. (Since 0.13.0) */ 27 | @Deprecated 28 | public File compilerJar(); 29 | 30 | /** Creates an application provider that will use 'loader()' as the parent ClassLoader for 31 | * the application given by 'id'. This method will retrieve the application if it has not already 32 | * been retrieved.*/ 33 | public AppProvider app(ApplicationID id); 34 | } 35 | -------------------------------------------------------------------------------- /ci-test/app2/sbt.1.4.0.boot.properties: -------------------------------------------------------------------------------- 1 | [scala] 2 | version: ${sbt.scala.version-auto} 3 | 4 | [app] 5 | org: ${sbt.organization-org.scala-sbt} 6 | name: sbt 7 | version: ${sbt.version-read(sbt.version)[1.4.0]} 8 | class: ${sbt.main.class-sbt.xMain} 9 | components: xsbti,extra 10 | cross-versioned: ${sbt.cross.versioned-false} 11 | resources: ${sbt.extraClasspath-} 12 | 13 | [repositories] 14 | local 15 | maven-central 16 | sbt-maven-releases: https://repo.scala-sbt.org/scalasbt/maven-releases/, bootOnly 17 | sbt-maven-snapshots: https://repo.scala-sbt.org/scalasbt/maven-snapshots/, bootOnly 18 | typesafe-ivy-releases: https://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly 19 | sbt-ivy-snapshots: https://repo.scala-sbt.org/scalasbt/ivy-snapshots/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly 20 | 21 | [boot] 22 | directory: /tmp/boot0/ 23 | lock: ${sbt.boot.lock-true} 24 | 25 | [ivy] 26 | ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/} 27 | checksums: ${sbt.checksums-sha1,md5} 28 | override-build-repos: ${sbt.override.build.repos-false} 29 | repository-config: ${sbt.repository.config-${sbt.global.base-${user.home}/.sbt}/repositories} 30 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Using.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.io.{ Closeable, File, FileInputStream, FileOutputStream, InputStream, OutputStream } 9 | 10 | object Using: 11 | def apply[R <: Closeable, T](create: R)(f: R => T): T = withResource(create)(f) 12 | def withResource[R <: Closeable, T](r: R)(f: R => T): T = 13 | try 14 | f(r) 15 | finally 16 | r.close() 17 | 18 | object Copy: 19 | def apply(files: List[File], toDirectory: File): Boolean = 20 | files.map(file => apply(file, toDirectory)).contains(true) 21 | def apply(file: File, toDirectory: File): Boolean = 22 | toDirectory.mkdirs() 23 | val to = new File(toDirectory, file.getName) 24 | val missing = !to.exists 25 | if missing then 26 | Using(new FileInputStream(file)) { in => 27 | Using(new FileOutputStream(to)) { out => 28 | transfer(in, out) 29 | } 30 | } 31 | missing 32 | def transfer(in: InputStream, out: OutputStream): Unit = 33 | val buffer = new Array[Byte](8192) 34 | def next(): Unit = 35 | val read = in.read(buffer) 36 | if read > 0 then 37 | out.write(buffer, 0, read) 38 | next() 39 | next() 40 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/Predefined.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | public enum Predefined { 9 | Local("local"), MavenLocal("maven-local"), MavenCentral("maven-central"), 10 | /** 11 | * @deprecated use {@link #SonatypeOSSReleases} instead. 12 | */ 13 | @Deprecated 14 | ScalaToolsReleases("scala-tools-releases"), 15 | /** 16 | * @deprecated use {@link #SonatypeOSSSnapshots} instead. 17 | */ 18 | @Deprecated 19 | ScalaToolsSnapshots("scala-tools-snapshots"), SonatypeOSSReleases("sonatype-oss-releases"), 20 | SonatypeOSSSnapshots("sonatype-oss-snapshots"), 21 | /** 22 | * @deprecated 23 | */ 24 | @Deprecated 25 | Jcenter("jcenter"); 26 | 27 | private final String label; 28 | 29 | private Predefined(String label) { 30 | this.label = label; 31 | } 32 | 33 | public String toString() { 34 | return label; 35 | } 36 | 37 | public static Predefined toValue(String s) { 38 | for (Predefined p : values()) 39 | if (s.equals(p.toString())) 40 | return p; 41 | 42 | StringBuilder msg = new StringBuilder("Expected one of "); 43 | for (Predefined p : values()) 44 | msg.append(p.toString()).append(", "); 45 | msg.append("got '").append(s).append("'."); 46 | throw new RuntimeException(msg.toString()); 47 | } 48 | } -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/JAnsi.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import xsbt.boot.Pre.* 9 | 10 | object JAnsi: 11 | def uninstall(loader: ClassLoader): Unit = callJAnsi("systemUninstall", loader) 12 | def install(loader: ClassLoader): Unit = callJAnsi("systemInstall", loader) 13 | 14 | private def callJAnsi(methodName: String, loader: ClassLoader): Unit = 15 | if isWindows && !isCygwin then callJAnsiMethod(methodName, loader) 16 | private def callJAnsiMethod(methodName: String, loader: ClassLoader): Unit = 17 | try 18 | val c = 19 | Class.forName(Seq("org", "fusesource", "jansi", "AnsiConsole").mkString("."), true, loader) 20 | c.getMethod(methodName).invoke(null) 21 | () 22 | catch 23 | case _: ClassNotFoundException => 24 | /* The below code intentionally traps everything. It technically shouldn't trap the 25 | * non-StackOverflowError VirtualMachineErrors and AWTError would be weird, but this is PermGen 26 | * mitigation code that should not render sbt completely unusable if jansi initialization fails. 27 | * [From Mark Harrah, https://github.com/sbt/sbt/pull/633#issuecomment-11957578]. 28 | */ 29 | case ex: Throwable => 30 | Console.err.println( 31 | "[error] [launcher] Jansi found on class path but initialization failed: " + ex 32 | ) 33 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/CheckProxy.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import java.net.{ MalformedURLException, URL } 10 | 11 | object CheckProxy: 12 | def apply(): Unit = 13 | import ProxyProperties.* 14 | for pp <- Seq(http, https, ftp) do setFromEnv(pp) 15 | 16 | private def setFromEnv(conf: ProxyProperties): Unit = 17 | import conf.* 18 | val proxyURL = System.getenv(envURL) 19 | if isDefined(proxyURL) && !isPropertyDefined(sysHost) && !isPropertyDefined(sysPort) then 20 | try 21 | val proxy = new URL(proxyURL) 22 | setProperty(sysHost, proxy.getHost) 23 | val port = proxy.getPort 24 | if port >= 0 then System.setProperty(sysPort, port.toString) 25 | copyEnv(envUser, sysUser) 26 | copyEnv(envPassword, sysPassword) 27 | catch 28 | case e: MalformedURLException => 29 | Console.err.println(s"[warn] [launcher] could not parse $envURL setting: ${e.toString}") 30 | 31 | private def copyEnv(envKey: String, sysKey: String): Unit = 32 | setProperty(sysKey, System.getenv(envKey)) 33 | private def setProperty(key: String, value: String): Unit = 34 | if value != null then System.setProperty(key, value) 35 | () 36 | private def isPropertyDefined(k: String) = isDefined(System.getProperty(k)) 37 | private def isDefined(s: String) = s != null && isNonEmpty(s) 38 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/Server.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | /** A running server. 9 | * 10 | * A class implementing this must: 11 | * 12 | * 1. Expose an HTTP port that clients can connect to, returned via the uri method. 13 | * 2. Accept HTTP HEAD requests against the returned URI. These are used as "ping" messages to ensure 14 | * a server is still alive, when new clients connect. 15 | * 3. Create a new thread to execute its service 16 | * 4. Block the calling thread until the server is shutdown via awaitTermination() 17 | */ 18 | public interface Server { 19 | /** 20 | * @return 21 | * A URI denoting the Port which clients can connect to. 22 | * 23 | * Note: we use a URI so that the server can bind to different IP addresses (even a public one) if desired. 24 | * Note: To verify that a server is "up", the sbt launcher will attempt to connect to 25 | * this URI's address and port with a socket. If the connection is accepted, the server is assumed to 26 | * be working. 27 | */ 28 | public java.net.URI uri(); 29 | /** 30 | * This should block the calling thread until the server is shutdown. 31 | * 32 | * @return 33 | * The result that should occur from the server. 34 | * Can be: 35 | * - xsbti.Exit: Shutdown this launch 36 | * - xsbti.Reboot: Restart the server 37 | * 38 | * 39 | */ 40 | public xsbti.MainResult awaitTermination(); 41 | } -------------------------------------------------------------------------------- /ci-test/app0/sbt.1.3.13.boot.properties: -------------------------------------------------------------------------------- 1 | [scala] 2 | version: ${sbt.scala.version-auto} 3 | 4 | [app] 5 | org: ${sbt.organization-org.scala-sbt} 6 | name: sbt 7 | version: ${sbt.version-read(sbt.version)[1.3.13]} 8 | class: ${sbt.main.class-sbt.xMain} 9 | components: xsbti,extra 10 | cross-versioned: ${sbt.cross.versioned-false} 11 | resources: ${sbt.extraClasspath-} 12 | 13 | [repositories] 14 | local 15 | local-preloaded-ivy: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext] 16 | local-preloaded: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/} 17 | maven-central 18 | sbt-maven-releases: https://repo.scala-sbt.org/scalasbt/maven-releases/, bootOnly 19 | sbt-maven-snapshots: https://repo.scala-sbt.org/scalasbt/maven-snapshots/, bootOnly 20 | typesafe-ivy-releases: https://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly 21 | sbt-ivy-snapshots: https://repo.scala-sbt.org/scalasbt/ivy-snapshots/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly 22 | 23 | [boot] 24 | directory: /tmp/boot0/ 25 | lock: ${sbt.boot.lock-true} 26 | 27 | [ivy] 28 | ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/} 29 | checksums: ${sbt.checksums-sha1,md5} 30 | override-build-repos: ${sbt.override.build.repos-false} 31 | repository-config: ${sbt.repository.config-${sbt.global.base-${user.home}/.sbt}/repositories} 32 | -------------------------------------------------------------------------------- /ci-test/app1/sbt.0.13.18.boot.properties: -------------------------------------------------------------------------------- 1 | [scala] 2 | version: ${sbt.scala.version-auto} 3 | 4 | [app] 5 | org: ${sbt.organization-org.scala-sbt} 6 | name: sbt 7 | version: ${sbt.version-read(sbt.version)[0.13.18]} 8 | class: ${sbt.main.class-sbt.xMain} 9 | components: xsbti,extra 10 | cross-versioned: ${sbt.cross.versioned-false} 11 | resources: ${sbt.extraClasspath-} 12 | 13 | [repositories] 14 | local 15 | local-preloaded-ivy: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/}, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext] 16 | local-preloaded: file:///${sbt.preloaded-${sbt.global.base-${user.home}/.sbt}/preloaded/} 17 | maven-central 18 | sbt-maven-releases: https://repo.scala-sbt.org/scalasbt/maven-releases/, bootOnly 19 | sbt-maven-snapshots: https://repo.scala-sbt.org/scalasbt/maven-snapshots/, bootOnly 20 | typesafe-ivy-releases: https://repo.typesafe.com/typesafe/ivy-releases/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly 21 | sbt-ivy-snapshots: https://repo.scala-sbt.org/scalasbt/ivy-snapshots/, [organization]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext], bootOnly 22 | 23 | [boot] 24 | directory: /tmp/boot1/ 25 | lock: ${sbt.boot.lock-true} 26 | 27 | [ivy] 28 | ivy-home: ${sbt.ivy.home-${user.home}/.ivy2/} 29 | checksums: ${sbt.checksums-sha1,md5} 30 | override-build-repos: ${sbt.override.build.repos-false} 31 | repository-config: ${sbt.repository.config-${sbt.global.base-${user.home}/.sbt}/repositories} 32 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | sbt launcher 2 | Copyright 2011 - 2019 Lightbend, Inc. 3 | Copyright 2008, 2009, 2010 Mark Harrah, David MacIver 4 | Licensed under Apache v2 license (see LICENSE) 5 | 6 | Classes from the Scala library are distributed with the launcher. 7 | Copyright 2002-2013 EPFL, Lausanne 8 | Licensed under BSD-style license (see licenses/LICENSE_Scala) 9 | 10 | Classes from Apache Ivy, licensed under the Apache License, Version 2.0 11 | (see licenses/LICENSE_Apache) are distributed with the launcher. 12 | It requires the following notice: 13 | 14 | This product includes software developed by 15 | The Apache Software Foundation (http://www.apache.org/). 16 | 17 | Portions of Ivy were originally developed by 18 | Jayasoft SARL (http://www.jayasoft.fr/) 19 | and are licensed to the Apache Software Foundation under the 20 | "Software Grant License Agreement" 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR 23 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 24 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 25 | IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, 26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 27 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 29 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 31 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Update.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.io.File 9 | 10 | enum UpdateTarget: 11 | case UpdateScala(classifiers: List[String]) 12 | case UpdateApp(id: Application, classifiers: List[String], override val tpe: String) 13 | def tpe: String = this match 14 | case UpdateScala(_) => "scala" 15 | case UpdateApp(_, _, tpe) => tpe 16 | 17 | final class UpdateConfiguration( 18 | val bootDirectory: File, 19 | val ivyHome: Option[File], 20 | val scalaOrg: String, 21 | val scalaVersion: Option[String], 22 | val repositories: List[xsbti.Repository], 23 | val checksums: List[String] 24 | ): 25 | val resolutionCacheBase = new File(bootDirectory, "resolution-cache") 26 | def getScalaVersion = scalaVersion match 27 | case Some(sv) => sv; 28 | case None => "" 29 | 30 | final class UpdateResult( 31 | val success: Boolean, 32 | val scalaVersion: Option[String], 33 | val appVersion: Option[String] 34 | ) 35 | 36 | /** Ensures that the Scala and application jars exist for the given versions or else downloads them. */ 37 | final class Update(config: UpdateConfiguration): 38 | import config.bootDirectory 39 | bootDirectory.mkdirs 40 | 41 | lazy val coursierUpdate = new CousierUpdate(config) 42 | 43 | /** The main entry point of this class for use by the Update module. It runs Ivy */ 44 | def apply(target: UpdateTarget, reason: String): UpdateResult = 45 | coursierUpdate(target, reason) 46 | end Update 47 | -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/ListMapTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import org.scalacheck.* 9 | 10 | object ListMapProperties extends Properties("ListMap"): 11 | given Arbitrary[ListMap[Int, Int]] = Arbitrary( 12 | for (list <- Arbitrary.arbitrary[List[(Int, Int)]]) yield ListMap(list*) 13 | ) 14 | 15 | property("ListMap from List contains all members of that List") = Prop.forAll { 16 | (list: List[(Int, Int)]) => 17 | val map = ListMap(list*) 18 | list forall { entry => 19 | map.contains(entry._1) 20 | } 21 | } 22 | property("contains added entry") = Prop.forAll { (map: ListMap[Int, Int], key: Int, value: Int) => 23 | { (map + (key -> value)).contains(key) } && { (map + (key -> value))(key) == value } && { 24 | (map + (key -> value)).get(key) == Some(value) 25 | } 26 | } 27 | property("remove") = Prop.forAll { (map: ListMap[Int, Int], key: Int) => 28 | { Prop.throws(classOf[Exception])((map - key)(key)) } && { !(map - key).contains(key) } && { 29 | (map - key).get(key).isEmpty 30 | } 31 | } 32 | property("empty") = Prop.forAll { (key: Int) => 33 | { Prop.throws(classOf[Exception])(ListMap.empty(key)) } 34 | { !ListMap.empty.contains(key) } && { ListMap.empty.get(key).isEmpty } 35 | } 36 | 37 | object ListMapEmpty extends Properties("ListMap.empty"): 38 | import ListMap.empty 39 | property("isEmpty") = empty.isEmpty 40 | property("toList.isEmpty") = empty.toList.isEmpty 41 | property("toSeq.isEmpty") = empty.toSeq.isEmpty 42 | property("keys.isEmpty") = empty.keys.isEmpty 43 | property("iterator.isEmpty") = empty.iterator.isEmpty 44 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/ListMap.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import scala.collection.Iterable 10 | import scala.collection.immutable.List 11 | 12 | // preserves iteration order 13 | sealed class ListMap[K, V] private (backing: List[(K, V)]) 14 | extends Iterable[(K, V)] // use Iterable because Traversable.toStream loops 15 | : 16 | import ListMap.remove 17 | def update(k: K, v: V) = this.+((k, v)) 18 | def +(pair: (K, V)) = copy(pair :: remove(backing, pair._1)) 19 | def -(k: K) = copy(remove(backing, k)) 20 | def get(k: K): Option[V] = backing.find(_._1 == k).map(_._2) 21 | def keys: List[K] = backing.reverse.map(_._1) 22 | def apply(k: K): V = getOrError(get(k), "Key " + k + " not found") 23 | def contains(k: K): Boolean = get(k).isDefined 24 | def iterator = backing.reverse.iterator 25 | override def isEmpty: Boolean = backing.isEmpty 26 | override def toList = backing.reverse 27 | override def toSeq: Seq[(K, V)] = toList 28 | protected def copy(newBacking: List[(K, V)]): ListMap[K, V] = new ListMap(newBacking) 29 | def default(defaultF: K => V): ListMap[K, V] = 30 | new ListMap[K, V](backing): 31 | override def apply(k: K) = super.get(k).getOrElse(defaultF(k)) 32 | override def copy(newBacking: List[(K, V)]) = super.copy(newBacking).default(defaultF) 33 | override def toString = backing.mkString("ListMap(", ",", ")") 34 | object ListMap: 35 | def apply[K, V](pairs: (K, V)*) = new ListMap[K, V](pairs.toList.distinct) 36 | def empty[K, V] = new ListMap[K, V](Nil) 37 | private def remove[K, V](backing: List[(K, V)], k: K) = backing.filter(_._1 != k) 38 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/ApplicationID.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.io.File; 9 | 10 | /** 11 | * This represents an identification for the sbt launcher to load and run 12 | * an sbt launched application using ivy. 13 | */ 14 | public interface ApplicationID 15 | { 16 | /** 17 | * @return 18 | * The Ivy orgnaization / Maven groupId where we can find the application to launch. 19 | */ 20 | public String groupID(); 21 | /** 22 | * @return 23 | * The ivy module name / Maven artifactId where we can find the application to launch. 24 | */ 25 | public String name(); 26 | /** 27 | * @return 28 | * The ivy/maven version of the module we should resolve. 29 | */ 30 | public String version(); 31 | 32 | /** 33 | * @return 34 | * The fully qualified name of the class that extends xsbti.AppMain 35 | */ 36 | public String mainClass(); 37 | /** 38 | * @return 39 | * Additional ivy components we should resolve with the main application artifacts. 40 | */ 41 | public String[] mainComponents(); 42 | /** 43 | * @deprecated 44 | * This method is no longer used if the crossVersionedValue method is available. 45 | * 46 | * @return 47 | * True if the application is cross-versioned by binary-compatible version string, 48 | * False if there is no cross-versioning. 49 | */ 50 | @Deprecated 51 | public boolean crossVersioned(); 52 | 53 | /** 54 | * 55 | * @since 0.13.0 56 | * @return 57 | * The type of cross-versioning the launcher should use to resolve this artifact. 58 | */ 59 | public CrossValue crossVersionedValue(); 60 | 61 | /** Files to add to the application classpath. */ 62 | public File[] classpathExtra(); 63 | } -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/ResolveValues.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | 10 | object ResolveValues: 11 | def apply(conf: LaunchConfiguration): LaunchConfiguration = (new ResolveValues(conf))() 12 | private def trim(s: String): Option[String] = if s eq null then None else notEmpty(s.trim) 13 | private def notEmpty(s: String): Option[String] = if isEmpty(s) then None else Some(s) 14 | 15 | import ResolveValues.trim 16 | final class ResolveValues(conf: LaunchConfiguration): 17 | private def propertiesFile = conf.boot.properties 18 | private lazy val properties = readProperties(propertiesFile) 19 | def apply(): LaunchConfiguration = 20 | import conf.* 21 | val scalaVersion = resolve(conf.scalaVersion) 22 | val appVersion = resolve(app.version) 23 | val appName = resolve(app.name) 24 | val classifiers = resolveClassifiers(ivyConfiguration.classifiers) 25 | withNameAndVersions(scalaVersion, appVersion, appName, classifiers) 26 | def resolveClassifiers(classifiers: Classifiers): Classifiers = 27 | import ConfigurationParser.readIDs 28 | // the added "" ensures that the main jars are retrieved 29 | val scalaClassifiers = "" :: resolve(classifiers.forScala) 30 | val appClassifiers = "" :: resolve(classifiers.app) 31 | Classifiers(Value.Explicit(scalaClassifiers), Value.Explicit(appClassifiers)) 32 | def resolve[T](v: Value[T])(using read: String => T): T = 33 | v match 34 | case e: Value.Explicit[?] => e.value 35 | case i: Value.Implicit[?] => 36 | trim(properties.getProperty(i.name)) 37 | .map(read) 38 | .orElse(i.default) 39 | .getOrElse(sys.error("no " + i.name + " specified in " + propertiesFile)) 40 | -------------------------------------------------------------------------------- /licenses/LICENSE_Scala: -------------------------------------------------------------------------------- 1 | SCALA LICENSE 2 | 3 | Copyright (c) 2002-2008 EPFL, Lausanne, unless otherwise specified. 4 | All rights reserved. 5 | 6 | This software was developed by the Programming Methods Laboratory of the 7 | Swiss Federal Institute of Technology (EPFL), Lausanne, Switzerland. 8 | 9 | Permission to use, copy, modify, and distribute this software in source 10 | or binary form for any purpose with or without fee is hereby granted, 11 | provided that the following conditions are met: 12 | 13 | 1. Redistributions of source code must retain the above copyright 14 | notice, this list of conditions and the following disclaimer. 15 | 16 | 2. Redistributions in binary form must reproduce the above copyright 17 | notice, this list of conditions and the following disclaimer in the 18 | documentation and/or other materials provided with the distribution. 19 | 20 | 3. Neither the name of the EPFL nor the names of its contributors 21 | may be used to endorse or promote products derived from this 22 | software without specific prior written permission. 23 | 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND 26 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 27 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 28 | ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE 29 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 30 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 31 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 32 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 33 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 34 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 35 | SUCH DAMAGE. -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/FilteredLoader.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import BootConfiguration.{ FjbgPackage, IvyPackage, SbtBootPackage, ScalaPackage } 9 | import scala.collection.immutable.LazyList 10 | 11 | /** 12 | * A custom class loader to ensure the main part of sbt doesn't load any Scala or 13 | * Ivy classes from the jar containing the loader. 14 | */ 15 | private[boot] final class BootFilteredLoader(parent: ClassLoader) extends ClassLoader(parent): 16 | @throws(classOf[ClassNotFoundException]) 17 | override final def loadClass(className: String, resolve: Boolean): Class[?] = 18 | // note that we allow xsbti.* 19 | if className.startsWith(ScalaPackage) || className.startsWith(IvyPackage) || className 20 | .startsWith(SbtBootPackage) || className.startsWith(FjbgPackage) 21 | then throw new ClassNotFoundException(className) 22 | else super.loadClass(className, resolve) 23 | override def getResources(name: String) = excludedLoader.getResources(name) 24 | override def getResource(name: String) = excludedLoader.getResource(name) 25 | 26 | // the loader to use when a resource is excluded. This needs to be at least parent.getParent so that it skips parent. parent contains 27 | // resources included in the launcher, which need to be ignored. Now that the launcher can be unrooted (not the application entry point), 28 | // this needs to be the Java extension loader (the loader with getParent == null) 29 | private val excludedLoader = Loaders(parent.getParent).head 30 | 31 | object Loaders: 32 | def apply(loader: ClassLoader): LazyList[ClassLoader] = 33 | def loaders(loader: ClassLoader, accum: LazyList[ClassLoader]): LazyList[ClassLoader] = 34 | if loader eq null then accum else loaders(loader.getParent, loader #:: accum) 35 | loaders(loader, LazyList.empty) 36 | -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/ServerLocatorTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.io.File 9 | import sbt.io.IO.withTemporaryDirectory 10 | 11 | object ServerLocatorTest extends verify.BasicTestSuite: 12 | 13 | // TODO - Maybe use scalacheck to randomnly generate URIs 14 | test("ServerLocator read and write server URI properties") { 15 | withTemporaryDirectory { dir => 16 | val propFile = new File(dir, "server.properties") 17 | val expected = new java.net.URI("http://localhost:8080") 18 | ServerLocator.writeProperties(propFile, expected) 19 | assert(ServerLocator.readProperties(propFile) == Some(expected)) 20 | } 21 | } 22 | 23 | test("ServerLocator detect listening ports") { 24 | val serverSocket = new java.net.ServerSocket(0) 25 | object serverThread extends Thread: 26 | override def run(): Unit = 27 | // Accept one connection. 28 | val result = serverSocket.accept() 29 | result.close() 30 | serverSocket.close() 31 | serverThread.start() 32 | val uri = new java.net.URI( 33 | s"http://${serverSocket.getInetAddress.getHostAddress}:${serverSocket.getLocalPort}" 34 | ) 35 | assert(ServerLocator.isReachable(uri)) 36 | } 37 | 38 | test("ServerLauncher detect start URI from reader") { 39 | val expected = new java.net.URI("http://localhost:8080") 40 | val input = s"""|Some random text 41 | |to start the server 42 | |${ServerApplication.SERVER_SYNCH_TEXT}${expected.toASCIIString} 43 | |Some more output.""".stripMargin 44 | val inputStream = new java.io.BufferedReader(new java.io.StringReader(input)) 45 | val result = 46 | try ServerLauncher.readUntilSynch(inputStream) 47 | finally inputStream.close() 48 | assert(result == Some(expected)) 49 | } 50 | -------------------------------------------------------------------------------- /test-sample/src/main/scala/xsbt/boot/test/Apps.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot.test 7 | 8 | import scala.annotation.nowarn 9 | 10 | class Exit(val code: Int) extends xsbti.Exit 11 | final class MainException(message: String) extends RuntimeException(message) 12 | final class ArgumentTest extends xsbti.AppMain: 13 | def run(configuration: xsbti.AppConfiguration) = 14 | if configuration.arguments.length == 0 then throw new MainException("Arguments were empty") 15 | else new Exit(0) 16 | class AppVersionTest extends xsbti.AppMain: 17 | def run(configuration: xsbti.AppConfiguration) = 18 | val expected = configuration.arguments.headOption.getOrElse("") 19 | if configuration.provider.id.version == expected then new Exit(0) 20 | else 21 | throw new MainException( 22 | "app version was " + configuration.provider.id.version + ", expected: " + expected 23 | ) 24 | class ExtraTest extends xsbti.AppMain: 25 | def run(configuration: xsbti.AppConfiguration): xsbti.Exit = 26 | configuration.arguments.foreach { arg => 27 | if getClass.getClassLoader.getResource(arg) eq null then 28 | throw new MainException("Could not find '" + arg + "'") 29 | } 30 | new Exit(0) 31 | 32 | class PriorityTest extends xsbti.AppMain: 33 | def run(configuration: xsbti.AppConfiguration): xsbti.Exit = 34 | PriorityTest.run(configuration) 35 | 36 | object PriorityTest: 37 | @nowarn 38 | def run(configuration: xsbti.AppConfiguration) = 39 | new Exit(0) 40 | def main(args: Array[String]): Unit = 41 | throw new MainException("This should not be called") 42 | object PlainArgumentTestWithReturn: 43 | def main(args: Array[String]): Unit = 44 | if args.length == 0 then 1 45 | else 0 46 | () 47 | object PlainArgumentTest: 48 | def main(args: Array[String]): Unit = 49 | if args.length == 0 then throw new MainException("Arguments were empty") 50 | else () 51 | -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/EnumerationTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import org.scalacheck.* 9 | import Prop.{ Exception as _, * } 10 | 11 | object EnumerationTest extends Properties("Enumeration"): 12 | property("MultiEnum.toValue") = checkToValue(MultiEnum, multiElements*) 13 | property("MultiEnum.elements") = checkElements(MultiEnum, multiElements*) 14 | property("EmptyEnum.toValue") = checkToValue(EmptyEnum) 15 | property("EmptyEnum.elements") = EmptyEnum.elements.isEmpty 16 | property("SingleEnum.toValue") = checkToValue(SingleEnum, singleElements) 17 | property("SingleEnum.elements") = checkElements(SingleEnum, singleElements) 18 | 19 | def singleElements = ("A", SingleEnum.a) 20 | def multiElements = 21 | import MultiEnum.{ a, b, c } 22 | List(("A" -> a), ("B" -> b), ("C" -> c)) 23 | 24 | def checkElements(`enum`: Enumeration, mapped: (String, Enumeration#Value)*) = 25 | val elements = `enum`.elements 26 | ("elements: " + elements) |: 27 | (mapped.forall { case (_, v) => elements.contains(v) } && (elements.length == mapped.length)) 28 | 29 | def checkToValue(`enum`: Enumeration, mapped: (String, Enumeration#Value)*) = 30 | def invalid(s: String) = 31 | ("valueOf(" + s + ")") |: 32 | Prop.throws(classOf[Exception])(`enum`.toValue(s)) 33 | def valid(s: String, expected: Enumeration#Value) = 34 | ("valueOf(" + s + ")") |: 35 | ("Expected " + expected) |: 36 | (`enum`.toValue(s) == expected) 37 | val map = Map(mapped*) 38 | Prop.forAll((s: String) => 39 | map.get(s) match 40 | case Some(v) => valid(s, v) 41 | case None => invalid(s) 42 | ) 43 | object MultiEnum extends Enumeration: 44 | val a = value("A") 45 | val b = value("B") 46 | val c = value("C") 47 | object SingleEnum extends Enumeration: 48 | val a = value("A") 49 | object EmptyEnum extends Enumeration 50 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/ComponentProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.io.File; 9 | 10 | 11 | /** 12 | * A service to locate, install and modify "Components". 13 | * 14 | * A component is essentially a directory and a set of files attached to a unique string id. 15 | */ 16 | public interface ComponentProvider 17 | { 18 | /** 19 | * @param id The component's id string. 20 | * @return 21 | * The "working directory" or base directory for the component. You should perform temporary work here for the component. 22 | */ 23 | public File componentLocation(String id); 24 | /** 25 | * Grab the current component definition. 26 | * 27 | * @param componentID The component's id string. 28 | * @return 29 | * The set of files attached to this component. 30 | */ 31 | public File[] component(String componentID); 32 | /** 33 | * This will define a new component using the files passed in. 34 | * 35 | * Note: The component will copy/move the files into a cache location. You should not use them directly, but 36 | * look them up using the `component` method. 37 | * 38 | * @param componentID The component's id string 39 | * @param components The set of files which defines the component. 40 | */ 41 | public void defineComponent(String componentID, File[] components); 42 | /** 43 | * Modify an existing component by adding files to it. 44 | * 45 | * @param componentID The component's id string 46 | * @param components The set of new files to add to the component. 47 | * @return true if any files were copied and false otherwise. 48 | * 49 | */ 50 | public boolean addToComponent(String componentID, File[] components); 51 | /** 52 | * @return The lockfile you should use to ensure your component cache does not become corrupted. 53 | * May return null if there is no lockfile for this provider. 54 | */ 55 | public File lockFile(); 56 | } -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/ModuleDefinition.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import java.io.File 10 | import java.net.URLClassLoader 11 | import UpdateTarget.* 12 | 13 | final class ModuleDefinition( 14 | val configuration: UpdateConfiguration, 15 | val extraClasspath: Array[File], 16 | val target: UpdateTarget, 17 | val failLabel: String 18 | ): 19 | def retrieveFailed: Nothing = fail("") 20 | def retrieveCorrupt(missing: Iterable[String]): Nothing = 21 | fail(": missing " + missing.mkString(", ")) 22 | private def fail(extra: String) = 23 | throw new xsbti.RetrieveException(versionString, "could not retrieve " + failLabel + extra) 24 | private def versionString: String = target match 25 | case _: UpdateScala => configuration.getScalaVersion; 26 | case a: UpdateApp => Value.get(a.id.version) 27 | 28 | final class RetrievedModule( 29 | val fresh: Boolean, 30 | val definition: ModuleDefinition, 31 | val detectedScalaVersion: Option[String], 32 | val resolvedAppVersion: Option[String], 33 | val baseDirectories: List[File] 34 | ): 35 | 36 | /** Use this constructor only when the module exists already, or when its version is not dynamic (so its resolved version would be the same) */ 37 | def this( 38 | fresh: Boolean, 39 | definition: ModuleDefinition, 40 | detectedScalaVersion: Option[String], 41 | baseDirectories: List[File] 42 | ) = 43 | this(fresh, definition, detectedScalaVersion, None, baseDirectories) 44 | 45 | lazy val monkeys: Array[File] = 46 | sys.props.get("sbt.launcher.cp.prepend").toArray.flatMap(ms => ms.split(",").map(new File(_))) 47 | lazy val classpath: Array[File] = getJars(baseDirectories) 48 | lazy val fullClasspath: Array[File] = 49 | concat(concat(monkeys, classpath), definition.extraClasspath) 50 | 51 | def createLoader(parentLoader: ClassLoader): ClassLoader = 52 | new URLClassLoader(toURLs(fullClasspath), parentLoader) 53 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/PlainApplication.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt 7 | package boot 8 | 9 | /** A wrapper around 'raw' static methods to meet the sbt application interface. */ 10 | class PlainApplication private (mainMethod: java.lang.reflect.Method) extends xsbti.AppMain: 11 | override def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = 12 | // TODO - Figure out if the main method returns an Int... 13 | val IntClass = classOf[Int] 14 | val ExitClass = classOf[xsbti.Exit] 15 | // It seems we may need to wrap exceptions here... 16 | try 17 | mainMethod.getReturnType match 18 | case ExitClass => 19 | mainMethod.invoke(null, configuration.arguments).asInstanceOf[xsbti.Exit] 20 | case IntClass => 21 | PlainApplication.Exit(mainMethod.invoke(null, configuration.arguments).asInstanceOf[Int]) 22 | case _ => 23 | // Here we still invoke, but return 0 if sucessful (no exceptions). 24 | mainMethod.invoke(null, configuration.arguments) 25 | PlainApplication.Exit(0) 26 | catch 27 | // This is only thrown if the underlying reflective call throws. 28 | // Let's expose the underlying error. 29 | case e: java.lang.reflect.InvocationTargetException if e.getCause != null => 30 | throw e.getCause 31 | 32 | /** An object that lets us detect compatible "plain" applications and launch them reflectively. */ 33 | object PlainApplication: 34 | def isPlainApplication(clazz: Class[?]): Boolean = findMainMethod(clazz).isDefined 35 | def apply(clazz: Class[?]): xsbti.AppMain = 36 | findMainMethod(clazz) match 37 | case Some(method) => new PlainApplication(method) 38 | case None => sys.error("class: " + clazz + " does not have a main method!") 39 | private def findMainMethod(clazz: Class[?]): Option[java.lang.reflect.Method] = 40 | try 41 | val method = 42 | clazz.getMethod("main", classOf[Array[String]]) 43 | if java.lang.reflect.Modifier.isStatic(method.getModifiers) then Some(method) 44 | else None 45 | catch case _: NoSuchMethodException => None 46 | 47 | case class Exit(code: Int) extends xsbti.Exit 48 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/AppProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.io.File; 9 | 10 | /** 11 | * This represents an interface that can generate applications or servers. 12 | * 13 | * This provider grants access to launcher related features associated with 14 | * the id. 15 | */ 16 | public interface AppProvider 17 | { 18 | /** Returns the ScalaProvider that this AppProvider will use. */ 19 | public ScalaProvider scalaProvider(); 20 | /** The ID of the application that will be created by 'newMain' or 'mainClass'.*/ 21 | public ApplicationID id(); 22 | /** The classloader used to load this application. */ 23 | public ClassLoader loader(); 24 | /** Loads the class for the entry point for the application given by 'id'. 25 | * This method will return the same class every invocation. 26 | * That is, the ClassLoader is not recreated each call. 27 | * @deprecated("use entryPoint instead") 28 | * 29 | * Note: This will throw an exception if the launched application does not extend AppMain. 30 | */ 31 | @Deprecated 32 | public Class mainClass(); 33 | /** Loads the class for the entry point for the application given by 'id'. 34 | * This method will return the same class every invocation. 35 | * That is, the ClassLoader is not recreated each call. 36 | */ 37 | public Class entryPoint(); 38 | /** Creates a new instance of the entry point of the application given by 'id'. 39 | * It is NOT guaranteed that newMain().getClass() == mainClass(). 40 | * The sbt launcher can wrap generic static main methods. In this case, there will be a wrapper class, 41 | * and you must use the `entryPoint` method. 42 | * @throws IncompatibleClassChangeError if the configuration used for this Application does not 43 | * represent a launched application. 44 | */ 45 | public AppMain newMain(); 46 | 47 | /** The classpath from which the main class is loaded, excluding Scala jars.*/ 48 | public File[] mainClasspath(); 49 | 50 | /** Returns a mechanism you can use to install/find/resolve components. 51 | * A component is just a related group of files. 52 | */ 53 | public ComponentProvider components(); 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The sbt launcher module 2 | 3 | This project is the componetized sbt launcher. It can be used to launch many Maven/Ivy deployed applications 4 | and utilities, and forms the basis of [sbt](https://github.com/sbt/sbt) 5 | and [conscript](https://github.com/foundweekends/conscript)'s launching 6 | abilities. 7 | 8 | For the full set of documentation, read: . 9 | 10 | ## Rebundling 11 | 12 | This project provides two modules for general use: 13 | 14 | 1. A library for interacting with Launcher features as a launched application, or for defining a launched Server. 15 | 2. A minimal JAR that can be used to lookup your application, using Ivy, and load/run it. 16 | 17 | This minimal JAR file is designed to be: 18 | 19 | * Less than 1MB in size 20 | * Able to launch any application on the JVM 21 | * Isolate classloaders and allow re-use of Scala library classloader for Scala applications. 22 | * Rebundled as a "wrapper" or "launcher" for your specific project. 23 | 24 | To rebundle the JAR for your project, first you'll need a launcher properties file (specified [here](https://www.scala-sbt.org/1.x/docs/Launcher-Configuration.html)). 25 | 26 | You can test your launch configuration file by running: 27 | 28 | ``` 29 | java -jar @ 30 | ``` 31 | 32 | Once you've verified your properties file is complete you can inject your launcher properties file into the "launch jar" 33 | as the file `sbt/sbt.boot.properties`. The launcher will look for this file in lieu of any command-line arguments to 34 | launch an application. 35 | 36 | 37 | If you've not done this correctly, you will see the following: 38 | 39 | ``` 40 | $ java -jar target/sbt-launch-1.0.0-SNAPSHOT.jar 41 | Error during sbt execution: Could not finder sbt launch configuration. Searched classpath for: 42 | /sbt.boot.properties0.13.7 43 | /sbt/sbt.boot.properties0.13.7 44 | /sbt.boot.properties0.13 45 | /sbt/sbt.boot.properties0.13 46 | /sbt.boot.properties 47 | /sbt/sbt.boot.properties 48 | ``` 49 | 50 | 51 | Additionally, we recommend renaming your bundled launch jar for your application (e.g. activator calls theirs 52 | "activator-launch-.jar"). 53 | 54 | ## building 55 | 56 | ``` 57 | sbt -J-XX:MaxPermSize=256M 58 | ``` 59 | 60 | # License 61 | 62 | This software is under Apache 2.0 license. 63 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/java/xsbt/boot/IO.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot; 7 | 8 | import java.io.IOException; 9 | import java.nio.ByteBuffer; 10 | import java.nio.file.*; 11 | import java.nio.file.attribute.*; 12 | import java.util.EnumSet; 13 | 14 | import static java.nio.file.StandardCopyOption.COPY_ATTRIBUTES; 15 | import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; 16 | 17 | public class IO { 18 | public static void copyDirectory(final Path source, final Path target) 19 | throws IOException { 20 | Files.walkFileTree(source, EnumSet.of(FileVisitOption.FOLLOW_LINKS), 21 | Integer.MAX_VALUE, new FileVisitor() { 22 | 23 | @Override 24 | public FileVisitResult preVisitDirectory(Path dir, 25 | BasicFileAttributes sourceBasic) throws IOException { 26 | 27 | String relative = source.relativize(dir).toString(); 28 | if (!Files.exists(target.getFileSystem().getPath(relative))) 29 | Files.createDirectory(target.getFileSystem().getPath(relative)); 30 | return FileVisitResult.CONTINUE; 31 | } 32 | 33 | @Override 34 | public FileVisitResult visitFile(Path file, 35 | BasicFileAttributes attrs) throws IOException { 36 | String relative = source.relativize(file).toString(); 37 | Files.copy(file, target.getFileSystem().getPath(relative), COPY_ATTRIBUTES, REPLACE_EXISTING); 38 | return FileVisitResult.CONTINUE; 39 | } 40 | 41 | @Override 42 | public FileVisitResult visitFileFailed(Path file, IOException e) throws IOException { 43 | throw e; 44 | } 45 | 46 | @Override 47 | public FileVisitResult postVisitDirectory(Path dir, IOException e) throws IOException { 48 | if (e != null) throw e; 49 | return FileVisitResult.CONTINUE; 50 | } 51 | }); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Find.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import java.io.File 10 | import java.net.URI 11 | import scala.collection.immutable.List 12 | 13 | object Find: 14 | def apply(config: LaunchConfiguration, currentDirectory: File) = 15 | (new Find(config))(currentDirectory) 16 | class Find(config: LaunchConfiguration): 17 | import config.boot.search 18 | def apply(currentDirectory: File) = 19 | val current = currentDirectory.getCanonicalFile 20 | assert(current.isDirectory) 21 | 22 | lazy val fromRoot = path(current, Nil).filter(hasProject).map(_.getCanonicalFile) 23 | val found: Option[File] = 24 | search.tpe match 25 | case Search.RootFirst => fromRoot.headOption 26 | case Search.Nearest => fromRoot.lastOption 27 | case Search.Only => 28 | if hasProject(current) then Some(current) 29 | else 30 | fromRoot match 31 | case Nil => Some(current) 32 | case head :: Nil => Some(head) 33 | case _ => 34 | Console.err.println( 35 | "[error] [launcher] search method is 'only' and multiple ancestor directories match:\n\t" + fromRoot 36 | .mkString("\n\t") 37 | ) 38 | System.exit(1) 39 | None 40 | case _ => Some(current) 41 | val baseDirectory = orElse(found, current) 42 | System.setProperty("user.dir", baseDirectory.getAbsolutePath) 43 | (ResolvePaths(config, baseDirectory), baseDirectory) 44 | private def hasProject(f: File) = 45 | f.isDirectory && search.paths.forall(p => ResolvePaths(f, p).exists) 46 | private def path(f: File, acc: List[File]): List[File] = 47 | if f eq null then acc else path(f.getParentFile, f :: acc) 48 | object ResolvePaths: 49 | def apply(config: LaunchConfiguration, baseDirectory: File): LaunchConfiguration = 50 | config.map(f => apply(baseDirectory, f)) 51 | def apply(baseDirectory: File, f: File): File = 52 | if f.isAbsolute then f 53 | else 54 | assert( 55 | baseDirectory.isDirectory 56 | ) // if base directory is not a directory, URI.resolve will not work properly 57 | val uri = new URI(null, null, f.getPath, null) 58 | new File(baseDirectory.toURI.resolve(uri)) 59 | -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/VersionParts.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import org.scalacheck.* 9 | import Prop.* 10 | 11 | object VersionParts extends Properties("VersionParts"): 12 | property("Valid version, no qualifier") = Prop.forAll { (x0: Int, y0: Int, z0: Int) => 13 | val (x, y, z) = (norm(x0), norm(y0), norm(z0)) 14 | val str = s"$x.$y.$z" 15 | val expected = 16 | s"$x.$y.$z" :: 17 | s"$x.$y" :: 18 | "" :: 19 | Nil 20 | check(str, expected) 21 | } 22 | 23 | property("Valid version with qualifier") = Prop.forAll { 24 | (x0: Int, y0: Int, z0: Int, q0: String) => 25 | val (x, y, z, q) = (norm(x0), norm(y0), norm(z0), normS(q0)) 26 | val str = s"$x.$y.$z-$q" 27 | val expected = 28 | s"$x.$y.$z-$q" :: 29 | s"$x.$y.$z" :: 30 | s"$x.$y" :: 31 | "" :: 32 | Nil 33 | check(str, expected) 34 | } 35 | 36 | property("Invalid version") = Prop.forAll { (x0: Int, y0: Int, z0: Int, q0: String) => 37 | val (x, y, z, q) = (norm(x0), norm(y0), norm(z0), normS(q0)) 38 | val strings = 39 | x.toString :: 40 | s"$x.$y" :: 41 | s"$x.$y-$q" :: 42 | s"$x.$y.$z.$q" :: 43 | Nil 44 | all(strings.map(str => check(str, Configuration.noMatchParts))*) 45 | } 46 | 47 | private def check(versionString: String, expectedParts: List[String]) = 48 | def printParts(s: List[String]): String = s.map("'" + _ + "'").mkString("(", ", ", ")") 49 | val actual = Configuration.versionParts(versionString) 50 | s"Version string '$versionString'" |: 51 | s"Expected '${printParts(expectedParts)}'" |: 52 | s"Actual'${printParts(actual)}'" |: 53 | (actual == expectedParts) 54 | 55 | // Make `i` non-negative 56 | private def norm(i: Int): Int = 57 | if i == Int.MinValue then Int.MaxValue else math.abs(i) 58 | 59 | // Make `s` non-empty and suitable for java.util.regex input 60 | private def normS(s: String): String = 61 | val filtered = s filter validChar 62 | if filtered.isEmpty then "q" else filtered 63 | 64 | // strip whitespace and characters not supported by Pattern 65 | private def validChar(c: Char) = 66 | !java.lang.Character.isWhitespace(c) && 67 | !java.lang.Character.isISOControl(c) && 68 | !Character.isHighSurrogate(c) && 69 | !Character.isLowSurrogate(c) 70 | -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/LocksTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import org.scalacheck.* 9 | import Prop.* 10 | import java.io.File 11 | import sbt.io.IO.withTemporaryDirectory 12 | 13 | /** 14 | * These mainly test that things work in the uncontested case and that no OverlappingFileLockExceptions occur. 15 | * There is no real locking testing, just the coordination of locking. 16 | */ 17 | object LocksTest extends Properties("Locks"): 18 | property("Lock in nonexisting directory") = spec { 19 | withTemporaryDirectory { dir => 20 | val lockFile = new File(dir, "doesntexist/lock") 21 | Locks(lockFile, callTrue) 22 | } 23 | } 24 | 25 | property("Uncontested re-entrant lock") = spec { 26 | withTemporaryDirectory { dir => 27 | val lockFile = new File(dir, "lock") 28 | Locks(lockFile, callLocked(lockFile)) && 29 | Locks(lockFile, callLocked(lockFile)) 30 | } 31 | } 32 | 33 | property("Uncontested double lock") = spec { 34 | withTemporaryDirectory { dir => 35 | val lockFileA = new File(dir, "lockA") 36 | val lockFileB = new File(dir, "lockB") 37 | Locks(lockFileA, callLocked(lockFileB)) && 38 | Locks(lockFileB, callLocked(lockFileA)) 39 | } 40 | } 41 | 42 | property("Contested single lock") = spec { 43 | withTemporaryDirectory { dir => 44 | val lockFile = new File(dir, "lock") 45 | forkFold(2000) { _ => 46 | Locks(lockFile, callTrue) 47 | } 48 | } 49 | } 50 | 51 | private def spec(f: => Boolean): Prop = Prop { _ => 52 | Result(if f then True else False) 53 | } 54 | 55 | private def call[T](impl: => T) = new java.util.concurrent.Callable[T]: 56 | def call = impl 57 | 58 | private def callLocked(lockFile: File) = call { Locks(lockFile, callTrue) } 59 | 60 | private lazy val callTrue = call { true } 61 | 62 | private def forkFold(n: Int)(impl: Int => Boolean): Boolean = 63 | forkWait(n)(impl).foldLeft(true) { _ && _ } 64 | 65 | private def forkWait(n: Int)(impl: Int => Boolean): Iterable[Boolean] = 66 | import scala.concurrent.Future 67 | import scala.concurrent.ExecutionContext.Implicits.global 68 | import scala.concurrent.Await 69 | import scala.concurrent.duration.Duration.Inf 70 | // TODO - Don't wait forever... 71 | val futures = (0 until n).map { i => 72 | Future { impl(i) } 73 | } 74 | futures.toList.map(f => Await.result(f, Inf)) 75 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Create.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import java.io.File 10 | import java.util.{ Locale, Properties } 11 | import scala.collection.immutable.List 12 | import PropertyInit.* 13 | 14 | object Initialize: 15 | lazy val selectCreate = (_: AppProperty).create 16 | lazy val selectQuick = (_: AppProperty).quick 17 | lazy val selectFill = (_: AppProperty).fill 18 | def create( 19 | file: File, 20 | promptCreate: String, 21 | enableQuick: Boolean, 22 | spec: List[AppProperty] 23 | ): Unit = 24 | readLine(promptCreate + " (y/N" + (if enableQuick then "/s" else "") + ") ") match 25 | case None => declined("") 26 | case Some(line) => 27 | line.toLowerCase(Locale.ENGLISH) match 28 | case "y" | "yes" => process(file, spec, selectCreate) 29 | case "s" => process(file, spec, selectQuick) 30 | case "n" | "no" | "" => declined("") 31 | case x => 32 | Console.err.println(" '" + x + "' not understood.") 33 | create(file, promptCreate, enableQuick, spec) 34 | 35 | def fill(file: File, spec: List[AppProperty]): Unit = process(file, spec, selectFill) 36 | 37 | def process( 38 | file: File, 39 | appProperties: List[AppProperty], 40 | select: AppProperty => Option[PropertyInit] 41 | ): Unit = 42 | val properties = readProperties(file) 43 | val uninitialized = 44 | for ( 45 | property <- appProperties; init <- select(property) 46 | if properties.getProperty(property.name) == null 47 | ) 48 | yield initialize(properties, property.name, init) 49 | if !uninitialized.isEmpty then writeProperties(properties, file, "") 50 | 51 | def initialize(properties: Properties, name: String, init: PropertyInit): Unit = 52 | init match 53 | case set: SetProperty => properties.setProperty(name, set.value) 54 | case prompt: PromptProperty => 55 | def noValue = declined("no value provided for " + prompt.label) 56 | readLine(prompt.label + prompt.default.toList.map(" [" + _ + "]").mkString + ": ") match 57 | case None => noValue 58 | case Some(line) => 59 | val value = 60 | if isEmpty(line) then orElse(prompt.default, noValue) 61 | else line 62 | properties.setProperty(name, value) 63 | () 64 | -------------------------------------------------------------------------------- /test-sample/src/main/scala/xsbt/boot/test/Servers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot.test 7 | 8 | import java.net.Socket 9 | import java.net.SocketTimeoutException 10 | 11 | class EchoServer extends xsbti.ServerMain: 12 | def start(configuration: xsbti.AppConfiguration): xsbti.Server = 13 | object server extends xsbti.Server: 14 | // TODO - Start a server. 15 | val serverSocket = new java.net.ServerSocket(0) 16 | val port = serverSocket.getLocalPort 17 | val addr = serverSocket.getInetAddress.getHostAddress 18 | override val uri = new java.net.URI(s"http://${addr}:${port}") 19 | // Check for stop every second. 20 | serverSocket.setSoTimeout(1000) 21 | object serverThread extends Thread: 22 | private val running = new java.util.concurrent.atomic.AtomicBoolean(true) 23 | override def run(): Unit = 24 | while running.get do 25 | try 26 | val clientSocket = serverSocket.accept() 27 | // Handle client connections 28 | object clientSocketThread extends Thread: 29 | override def run(): Unit = 30 | echoTo(clientSocket) 31 | clientSocketThread.start() 32 | catch 33 | case _: SocketTimeoutException => // Ignore 34 | // Simple mechanism to dump input to output. 35 | private def echoTo(socket: Socket): Unit = 36 | val input = 37 | new java.io.BufferedReader(new java.io.InputStreamReader(socket.getInputStream)) 38 | val output = 39 | new java.io.BufferedWriter(new java.io.OutputStreamWriter(socket.getOutputStream)) 40 | import scala.util.control.Breaks.* 41 | try 42 | // Lame way to break out. 43 | breakable { 44 | def read(): Unit = input.readLine match 45 | case null => () 46 | case "kill" => 47 | running.set(false) 48 | serverSocket.close() 49 | break() 50 | case line => 51 | output.write(line) 52 | output.flush() 53 | read() 54 | read() 55 | } 56 | finally 57 | output.close() 58 | input.close() 59 | socket.close() 60 | // Start the thread immediately 61 | serverThread.start() 62 | override def awaitTermination(): xsbti.MainResult = 63 | serverThread.join() 64 | new Exit(0) 65 | server 66 | -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/PreTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.io.File 9 | import java.util.Arrays.equals as arrEquals 10 | import org.scalacheck.* 11 | 12 | object PreTest extends Properties("Pre"): 13 | import Pre.* 14 | property("isEmpty") = Prop.forAll((s: String) => (s.isEmpty == isEmpty(s))) 15 | property("isNonEmpty") = Prop.forAll((s: String) => (isEmpty(s) != isNonEmpty(s))) 16 | property("assert true") = 17 | assert(true); true 18 | property("assert false") = Prop.throws(classOf[AssertionError])(assert(false)) 19 | property("assert true with message") = Prop.forAll { (s: String) => 20 | assert(true, s); true 21 | } 22 | property("assert false with message") = 23 | Prop.forAll((s: String) => Prop.throws(classOf[AssertionError])(assert(false, s))) 24 | property("require false") = 25 | Prop.forAll((s: String) => Prop.throws(classOf[IllegalArgumentException])(require(false, s))) 26 | property("require true") = Prop.forAll { (s: String) => 27 | require(true, s); true 28 | } 29 | property("error") = Prop.forAll((s: String) => Prop.throws(classOf[BootException])(error(s))) 30 | property("toBoolean") = 31 | Prop.forAll((s: String) => trap(toBoolean(s)) == trap(java.lang.Boolean.parseBoolean(s))) 32 | property("toArray") = Prop.forAll((list: List[Int]) => arrEquals(list.toArray, toArray(list))) 33 | property("toArray") = 34 | Prop.forAll((list: List[String]) => objArrEquals(list.toArray, toArray(list))) 35 | property("concat") = Prop.forAll(genFiles, genFiles) { (a: Array[File], b: Array[File]) => 36 | (a ++ b) sameElements concat(a, b) 37 | } 38 | property("array") = Prop.forAll(genFiles) { (a: Array[File]) => 39 | array(a.toList*) sameElements Array(a*) 40 | } 41 | property("substituteTilde") = 42 | val userHome = System.getProperty("user.home") 43 | assert(substituteTilde("~/path") == s"$userHome/path") 44 | assert(substituteTilde("~\\") == s"$userHome\\") 45 | assert(substituteTilde("~") == userHome) 46 | assert(substituteTilde("~x") == "~x") 47 | assert(substituteTilde("x~/") == "x~/") 48 | true 49 | 50 | given Arbitrary[File] = Arbitrary { 51 | for (i <- Arbitrary.arbitrary[Int]) yield new File(i.toString) 52 | } 53 | val genFiles: Gen[Array[File]] = Arbitrary.arbitrary[Array[File]] 54 | 55 | def trap[T](t: => T): Option[T] = 56 | try Some(t) 57 | catch case _: Exception => None 58 | 59 | private def objArrEquals[T <: AnyRef](a: Array[T], b: Array[T]): Boolean = 60 | arrEquals(a.asInstanceOf[Array[AnyRef]], b.asInstanceOf[Array[AnyRef]]) 61 | -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/URITests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import org.scalacheck.* 9 | import Prop.* 10 | import Configuration.* 11 | import java.io.File 12 | import java.net.URI 13 | 14 | object URITests extends Properties("URI Tests"): 15 | // Need a platform-specific root, otherwise URI will not be absolute (e.g. if we use a "/a/b/c" path in Windows) 16 | // Note: 17 | // If I use "C:" instead of "/C:", then isAbsolute == true for the resulting URI, but resolve is broken: 18 | // e.g. scala> new URI("file", "c:/a/b'/has spaces", null).resolve("a") broken 19 | // res0: java.net.URI = a 20 | // scala> new URI("file", "/c:/a/b'/has spaces", null).resolve("a") working 21 | // res1: java.net.URI = file:/c:/a/b'/a 22 | val Root = if xsbt.boot.Pre.isWindows then "/C:/" else "/" 23 | 24 | val FileProtocol = "file" 25 | property("directoryURI adds trailing slash") = secure { 26 | val dirURI = directoryURI(new File(Root + "a/b/c")) 27 | val directURI = filePathURI(Root + "a/b/c/") 28 | dirURI == directURI 29 | } 30 | property("directoryURI preserves trailing slash") = secure { 31 | directoryURI(new File(Root + "a/b/c/")) == filePathURI(Root + "a/b/c/") 32 | } 33 | 34 | property("filePathURI encodes spaces") = secure { 35 | val decoded = "has spaces" 36 | val encoded = "has%20spaces" 37 | val fpURI = filePathURI(decoded) 38 | val directURI = new URI(encoded) 39 | s"filePathURI: $fpURI" |: 40 | s"direct URI: $directURI" |: 41 | s"getPath: ${fpURI.getPath}" |: 42 | s"getRawPath: ${fpURI.getRawPath}" |: 43 | (fpURI == directURI) && 44 | (fpURI.getPath == decoded) && 45 | (fpURI.getRawPath == encoded) 46 | } 47 | 48 | property("filePathURI and File.toURI agree for absolute file") = secure { 49 | val s = Root + "a/b'/has spaces" 50 | val viaPath = filePathURI(s) 51 | val viaFile = new File(s).toURI 52 | s"via path: $viaPath" |: 53 | s"via file: $viaFile" |: 54 | (viaPath == viaFile) 55 | } 56 | 57 | property("filePathURI supports URIs") = secure { 58 | val s = s"file://${Root}is/a/uri/with%20spaces" 59 | val decoded = Root + "is/a/uri/with spaces" 60 | val encoded = Root + "is/a/uri/with%20spaces" 61 | val fpURI = filePathURI(s) 62 | val directURI = new URI(s) 63 | s"filePathURI: $fpURI" |: 64 | s"direct URI: $directURI" |: 65 | s"getPath: ${fpURI.getPath}" |: 66 | s"getRawPath: ${fpURI.getRawPath}" |: 67 | (fpURI == directURI) && 68 | (fpURI.getPath == decoded) && 69 | (fpURI.getRawPath == encoded) 70 | } 71 | -------------------------------------------------------------------------------- /launcher-interface/src/main/java/xsbti/Launcher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbti; 7 | 8 | import java.io.File; 9 | 10 | public interface Launcher 11 | { 12 | public static final int InterfaceVersion = 1; 13 | public ScalaProvider getScala(String version); 14 | public ScalaProvider getScala(String version, String reason); 15 | public ScalaProvider getScala(String version, String reason, String scalaOrg); 16 | /** 17 | * returns an `AppProvider` which is able to resolve an application 18 | * and instantiate its `xsbti.Main` in a new classloader. 19 | * See [AppProvider] for more details. 20 | * @param id The artifact coordinates of the application. 21 | * @param version The version to resolve 22 | */ 23 | public AppProvider app(ApplicationID id, String version); 24 | /** 25 | * This returns the "top" classloader for a launched application. This classlaoder 26 | * lives somewhere *above* that used for the application. This classloader 27 | * is used for doing any sort of JNA/native library loads so that downstream 28 | * loaders can share native libraries rather than run into "load-once" restrictions. 29 | */ 30 | public ClassLoader topLoader(); 31 | /** 32 | * Return the global lock for interacting with the file system. 33 | * 34 | * A mechanism to do file-based locking correctly on the JVM. See 35 | * the [[GlobalLock]] class for more details. 36 | */ 37 | public GlobalLock globalLock(); 38 | /** Value of the `sbt.boot.dir` property, or the default 39 | * boot configuration defined in `boot.directory`. 40 | */ 41 | public File bootDirectory(); 42 | /** Configured launcher repositories. These repositories 43 | * are the same ones used to load the launcher. 44 | */ 45 | public xsbti.Repository[] ivyRepositories(); 46 | /** These are the repositories configured by this launcher 47 | * which should be used by the application when resolving 48 | * further artifacts. 49 | */ 50 | public xsbti.Repository[] appRepositories(); 51 | /** The user has configured the launcher with the only repositories 52 | * it wants to use for this applciation. 53 | */ 54 | public boolean isOverrideRepositories(); 55 | /** 56 | * The value of `ivy.ivy-home` of the boot properties file. 57 | * This defaults to the `sbt.ivy.home` property, or `~/.ivy2`. 58 | * Use this setting in an application when using Ivy to resolve 59 | * more artifacts. 60 | * 61 | * @return a file, or null if not set. 62 | */ 63 | public File ivyHome(); 64 | /** An array of the checksums that should be checked when retreiving artifacts. 65 | * Configured via the the `ivy.checksums` section of the boot configuration. 66 | * Defaults to sha1, md5 or the value of the `sbt.checksums` property. 67 | */ 68 | public String[] checksums(); 69 | } 70 | -------------------------------------------------------------------------------- /project/Util.scala: -------------------------------------------------------------------------------- 1 | import sbt.* 2 | import Keys.* 3 | import sbt.internal.inc.Analysis 4 | import xsbti.compile.CompileAnalysis 5 | 6 | object Util { 7 | val publishSigned = 8 | TaskKey[Unit]("publish-signed", "Publishing all artifacts, but SIGNED using PGP.") 9 | 10 | def commonSettings(nameString: String) = Seq( 11 | name := nameString, 12 | resolvers += Resolver.typesafeIvyRepo("releases"), 13 | publishMavenStyle := true 14 | ) 15 | 16 | /** Configures a project to be java only. */ 17 | lazy val javaOnly = Seq[Setting[?]]( 18 | /*crossPaths := false, */ compileOrder := CompileOrder.JavaThenScala, 19 | Compile / unmanagedSourceDirectories := Seq((Compile / javaSource).value), 20 | autoScalaLibrary := false 21 | ) 22 | lazy val base: Seq[Setting[?]] = baseScalacOptions ++ Licensed.settings 23 | lazy val baseScalacOptions = Seq( 24 | scalacOptions ++= { 25 | Seq( 26 | "-deprecation", 27 | "-feature", 28 | "-language:implicitConversions", 29 | "-language:postfixOps", 30 | "-language:higherKinds", 31 | "-language:existentials", 32 | "-Werror", 33 | "-Wunused:all", 34 | ) 35 | }, 36 | ) 37 | 38 | def lastCompilationTime(analysis: Analysis): Long = { 39 | val lastCompilation = analysis.compilations.allCompilations.lastOption 40 | lastCompilation.map(_.getStartTime) getOrElse 0L 41 | } 42 | def generateVersionFile( 43 | fileName: String 44 | )(version: String, dir: File, s: TaskStreams, a0: CompileAnalysis): Seq[File] = { 45 | import java.util.{ Date, TimeZone } 46 | val analysis = a0 match { case a: Analysis => a } 47 | val formatter = new java.text.SimpleDateFormat("yyyyMMdd'T'HHmmss") 48 | formatter.setTimeZone(TimeZone.getTimeZone("GMT")) 49 | val timestamp = formatter.format(new Date) 50 | val content = versionLine(version) + "\ntimestamp=" + timestamp 51 | val f = dir / fileName 52 | if ( 53 | !f.exists || f.lastModified < lastCompilationTime(analysis) || !containsVersion(f, version) 54 | ) { 55 | s.log.info("Writing version information to " + f + " :\n" + content) 56 | IO.write(f, content) 57 | } 58 | f :: Nil 59 | } 60 | def versionLine(version: String): String = "version=" + version 61 | def containsVersion(propFile: File, version: String): Boolean = 62 | IO.read(propFile).contains(versionLine(version)) 63 | } 64 | 65 | object Licensed { 66 | lazy val notice = SettingKey[File]("notice") 67 | lazy val extractLicenses = TaskKey[Seq[File]]("extract-licenses") 68 | 69 | lazy val seeRegex = """\(see (.*?)\)""".r 70 | def licensePath(base: File, str: String): File = { 71 | val path = base / str; 72 | if (path.exists) path else sys.error("Referenced license '" + str + "' not found at " + path) 73 | } 74 | def seePaths(base: File, noticeString: String): Seq[File] = 75 | seeRegex.findAllIn(noticeString).matchData.map(d => licensePath(base, d.group(1))).toList 76 | 77 | def settings: Seq[Setting[?]] = Seq( 78 | notice := baseDirectory.value / "NOTICE", 79 | Compile / unmanagedResources ++= (notice.value +: extractLicenses.value), 80 | extractLicenses := extractLicenses0( 81 | (ThisBuild / baseDirectory).value, 82 | notice.value, 83 | streams.value 84 | ) 85 | ) 86 | def extractLicenses0(base: File, note: File, s: TaskStreams): Seq[File] = 87 | if (!note.exists) Nil 88 | else 89 | try { 90 | seePaths(base, IO.read(note)) 91 | } catch { 92 | case e: Exception => s.log.warn("Could not read NOTICE"); Nil 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/input_sources/CrossVersionUtil.scala: -------------------------------------------------------------------------------- 1 | package ${{cross.package0}}.${{cross.package1}} 2 | 3 | object CrossVersionUtil 4 | { 5 | val trueString = "true" 6 | val falseString = "false" 7 | val fullString = "full" 8 | val noneString = "none" 9 | val disabledString = "disabled" 10 | val binaryString = "binary" 11 | val TransitionScalaVersion = "2.10" 12 | val TransitionSbtVersion = "0.12" 13 | 14 | def isFull(s: String): Boolean = (s == trueString) || (s == fullString) 15 | def isDisabled(s: String): Boolean = (s == falseString) || (s == noneString) || (s == disabledString) 16 | def isBinary(s: String): Boolean = (s == binaryString) 17 | 18 | private[${{cross.package0}}] def isSbtApiCompatible(v: String): Boolean = sbtApiVersion(v).isDefined 19 | /** Returns sbt binary interface x.y API compatible with the given version string v. 20 | * RCs for x.y.0 are considered API compatible. 21 | * Compatibile versions include 0.12.0-1 and 0.12.0-RC1 for Some(0, 12). 22 | */ 23 | private[${{cross.package0}}] def sbtApiVersion(v: String): Option[(Int, Int)] = 24 | { 25 | val ReleaseV = """(\d+)\.(\d+)\.(\d+)(-\d+)?""".r 26 | val CandidateV = """(\d+)\.(\d+)\.(\d+)(-RC\d+)""".r 27 | val NonReleaseV = """(\d+)\.(\d+)\.(\d+)([-\w+]*)""".r 28 | v match { 29 | case ReleaseV(x, y, _, _) => Some((x.toInt, y.toInt)) 30 | case CandidateV(x, y, _, _) => Some((x.toInt, y.toInt)) 31 | case NonReleaseV(x, y, z, _) if z.toInt > 0 => Some((x.toInt, y.toInt)) 32 | case _ => None 33 | } 34 | } 35 | private[${{cross.package0}}] def isScalaApiCompatible(v: String): Boolean = scalaApiVersion(v).isDefined 36 | /** Returns Scala binary interface x.y API compatible with the given version string v. 37 | * Compatibile versions include 2.10.0-1 and 2.10.1-M1 for Some(2, 10), but not 2.10.0-RC1. 38 | */ 39 | private[${{cross.package0}}] def scalaApiVersion(v: String): Option[(Int, Int)] = 40 | { 41 | val ReleaseV = """(\d+)\.(\d+)\.(\d+)(-\d+)?""".r 42 | val BinCompatV = """(\d+)\.(\d+)\.(\d+)-bin(-.*)?""".r 43 | val NonReleaseV = """(\d+)\.(\d+)\.(\d+)(-\w+)""".r 44 | v match { 45 | case ReleaseV(x, y, _, _) => Some((x.toInt, y.toInt)) 46 | case BinCompatV(x, y, _, _) => Some((x.toInt, y.toInt)) 47 | case NonReleaseV(x, y, z, _) if z.toInt > 0 => Some((x.toInt, y.toInt)) 48 | case _ => None 49 | } 50 | } 51 | private[${{cross.package0}}] val PartialVersion = """(\d+)\.(\d+)(?:\..+)?""".r 52 | private[${{cross.package0}}] def partialVersion(s: String): Option[(Int,Int)] = 53 | s match { 54 | case PartialVersion(major, minor) => Some((major.toInt, minor.toInt)) 55 | case _ => None 56 | } 57 | def binaryScalaVersion(full: String): String = binaryVersionWithApi(full, TransitionScalaVersion)(scalaApiVersion) 58 | def binarySbtVersion(full: String): String = binaryVersionWithApi(full, TransitionSbtVersion)(sbtApiVersion) 59 | private[${{cross.package0}}] def binaryVersion(full: String, cutoff: String): String = binaryVersionWithApi(full, cutoff)(scalaApiVersion) 60 | private def isNewer(major: Int, minor: Int, minMajor: Int, minMinor: Int): Boolean = 61 | major > minMajor || (major == minMajor && minor >= minMinor) 62 | private def binaryVersionWithApi(full: String, cutoff: String)(apiVersion: String => Option[(Int,Int)]): String = 63 | { 64 | def sub(major: Int, minor: Int) = major.toString() + "." + minor 65 | (apiVersion(full), partialVersion(cutoff)) match { 66 | case (Some((major, minor)), None) => sub(major, minor) 67 | case (Some((major, minor)), Some((minMajor, minMinor))) if isNewer(major, minor, minMajor, minMinor) => sub(major, minor) 68 | case _ => full 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Boot.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.nio.file.{ Path, Paths } 9 | 10 | // The entry point to the launcher 11 | object Boot: 12 | lazy val defaultGlobalBase: Path = Paths.get(sys.props("user.home"), ".sbt", "1.0") 13 | lazy val globalBase = sys.props.get("sbt.global.base").getOrElse(defaultGlobalBase.toString) 14 | 15 | def main(args: Array[String]): Unit = 16 | standBy() 17 | val config = parseArgs(args) 18 | // If we havne't exited, we set up some hooks and launch 19 | System.clearProperty("scala.home") // avoid errors from mixing Scala versions in the same JVM 20 | System.setProperty("jline.shutdownhook", "false") // shutdown hooks cause class loader leaks 21 | System.setProperty("jline.esc.timeout", "0") // starts up a thread otherwise 22 | CheckProxy() 23 | run(config) 24 | 25 | def standBy(): Unit = 26 | import scala.concurrent.duration.Duration 27 | val x = System.getProperty("sbt.launcher.standby") 28 | if x == null then () 29 | else 30 | val sec = Duration(x).toSeconds 31 | if sec >= 1 then 32 | (sec to 1 by -1) foreach { i => 33 | Console.err.println(s"[info] [launcher] standing by: $i") 34 | Thread.sleep(1000) 35 | } 36 | 37 | def parseArgs(args: Array[String]): LauncherArguments = 38 | @annotation.tailrec 39 | def parse( 40 | args: List[String], 41 | isLocate: Boolean, 42 | isExportRt: Boolean, 43 | remaining: List[String] 44 | ): LauncherArguments = 45 | args match 46 | case "--launcher-version" :: _ => 47 | Console.err.println( 48 | "sbt launcher version " + Package.getPackage("xsbt.boot").getImplementationVersion 49 | ) 50 | exit(0) 51 | case "--rt-ext-dir" :: _ => 52 | var v = sys.props("java.vendor") + "_" + sys.props("java.version") 53 | v = v.replaceAll("\\W", "_").toLowerCase 54 | /* 55 | * The launch script greps for output starting with "java9-rt-ext-" so changing this 56 | * string will require changing the grep command in sbt-launch-lib.bash. 57 | */ 58 | val rtExtDir = Paths.get(globalBase, "java9-rt-ext-" + v) 59 | Console.out.println(rtExtDir.toString) 60 | exit(0) 61 | case "--locate" :: rest => parse(rest, true, isExportRt, remaining) 62 | case "--export-rt" :: rest => parse(rest, isLocate, true, remaining) 63 | case next :: rest => parse(rest, isLocate, isExportRt, next :: remaining) 64 | case Nil => new LauncherArguments(remaining.reverse, isLocate, isExportRt) 65 | parse(args.toList, false, false, Nil) 66 | 67 | // this arrangement is because Scala does not always properly optimize away 68 | // the tail recursion in a catch statement 69 | final def run(args: LauncherArguments): Unit = runImpl(args) match 70 | case Some(newArgs) => run(newArgs) 71 | case None => () 72 | private def runImpl(args: LauncherArguments): Option[LauncherArguments] = 73 | try Launch(args) map exit 74 | catch 75 | case b: BootException => errorAndExit(b.toString) 76 | case r: xsbti.RetrieveException => errorAndExit(r.getMessage) 77 | case r: xsbti.FullReload => Some(new LauncherArguments(r.arguments.toList, false, false)) 78 | case e: Throwable => 79 | e.printStackTrace 80 | errorAndExit(Pre.prefixError(e.toString)) 81 | 82 | private def errorAndExit(msg: String): Nothing = 83 | msg.linesIterator.toList foreach { line => 84 | Console.err.println("[error] [launcher] " + line) 85 | } 86 | exit(1) 87 | 88 | private def exit(code: Int): Nothing = 89 | sys.exit(code) 90 | -------------------------------------------------------------------------------- /project/Transform.scala: -------------------------------------------------------------------------------- 1 | import sbt.* 2 | import Keys.* 3 | import Path.* 4 | 5 | object Transform { 6 | lazy val transformSources = TaskKey[Seq[File]]("transform-sources") 7 | lazy val inputSourceDirectories = SettingKey[Seq[File]]("input-source-directories") 8 | lazy val inputSourceDirectory = SettingKey[File]("input-source-directory") 9 | lazy val inputSources = TaskKey[Seq[File]]("input-sources") 10 | lazy val sourceProperties = TaskKey[Map[String, String]]("source-properties") 11 | 12 | lazy val transformResources = TaskKey[Seq[File]]("transform-resources") 13 | lazy val inputResourceDirectories = SettingKey[Seq[File]]("input-resource-directories") 14 | lazy val inputResourceDirectory = SettingKey[File]("input-resource-directory") 15 | lazy val inputResources = TaskKey[Seq[File]]("input-resources") 16 | lazy val resourceProperties = TaskKey[Map[String, String]]("resource-properties") 17 | 18 | lazy val conscriptConfigs = TaskKey[Unit]("conscript-configs") 19 | 20 | def transSourceSettings = Seq( 21 | inputSourceDirectory := sourceDirectory.value / "input_sources", 22 | inputSourceDirectories := Seq(inputSourceDirectory.value), 23 | inputSources := (inputSourceDirectories.value ** (-DirectoryFilter)).get(), 24 | transformSources / fileMappings := transformSourceMappings.value, 25 | transformSources := { 26 | (transformSources / fileMappings).value.map { case (in, out) => 27 | transform(in, out, sourceProperties.value) 28 | } 29 | }, 30 | sourceGenerators += transformSources.taskValue 31 | ) 32 | def transformSourceMappings = Def.task { 33 | val ss = inputSources.value 34 | val sdirs = inputSourceDirectories.value 35 | val sm = sourceManaged.value 36 | (ss --- sdirs).pair(rebase(sdirs, sm) | flat(sm)).toSeq 37 | } 38 | def configSettings = transResourceSettings ++ Seq( 39 | resourceProperties := { 40 | Map( 41 | "org" -> organization.value, 42 | "sbt.version" -> version.value, 43 | "sbt.name" -> name.value, 44 | "scala.version" -> scalaVersion.value, 45 | "repositories" -> repositories(isSnapshot.value).mkString(IO.Newline) 46 | ) 47 | } 48 | ) 49 | def transResourceSettings = Seq( 50 | inputResourceDirectory := sourceDirectory.value / "input_resources", 51 | inputResourceDirectories := Seq(inputResourceDirectory.value), 52 | inputResources := (inputResourceDirectories.value ** (-DirectoryFilter)).get(), 53 | transformResources / fileMappings := transformResourceMappings.value, 54 | transformResources := { 55 | (transformResources / fileMappings).value.map { case (in, out) => 56 | transform(in, out, resourceProperties.value) 57 | } 58 | }, 59 | resourceGenerators += transformResources.taskValue 60 | ) 61 | def transformResourceMappings = Def.task { 62 | val rs = inputResources.value 63 | val rdirs = inputResourceDirectories.value 64 | val rm = resourceManaged.value 65 | (rs --- rdirs).pair(rebase(rdirs, rm) | flat(rm)).toSeq 66 | } 67 | 68 | def transform(in: File, out: File, map: Map[String, String]): File = { 69 | def get(key: String): String = 70 | map.getOrElse(key, sys.error("No value defined for key '" + key + "'")) 71 | val newString = Property.replaceAllIn(IO.read(in), mtch => get(mtch.group(1))) 72 | if (Some(newString) != read(out)) 73 | IO.write(out, newString) 74 | out 75 | } 76 | def read(file: File): Option[String] = 77 | try { 78 | Some(IO.read(file)) 79 | } catch { case _: java.io.IOException => None } 80 | lazy val Property = """\$\{\{([\w.-]+)\}\}""".r 81 | 82 | def repositories(isSnapshot: Boolean) = Releases :: (if (isSnapshot) Snapshots :: Nil else Nil) 83 | lazy val Releases = typesafeRepository("releases") 84 | lazy val Snapshots = typesafeRepository("snapshots") 85 | def typesafeRepository(status: String) = 86 | """ typesafe-ivy-%s: https://repo.typesafe.com/typesafe/ivy-% Nil 25 | private def getLocks0(loader: ClassLoader) = 26 | Class 27 | .forName("xsbt.boot.Locks$", true, loader) 28 | .getField("MODULE$") 29 | .get(null) 30 | .asInstanceOf[xsbti.GlobalLock] 31 | 32 | // gets a file lock by first getting a JVM-wide lock. 33 | object Locks extends xsbti.GlobalLock: 34 | private val locks = new Cache[File, Unit, GlobalLock]((f, _) => new GlobalLock(f)) 35 | def apply[T](file: File, action: Callable[T]): T = 36 | if file eq null then action.call else apply0(file, action) 37 | private def apply0[T](file: File, action: Callable[T]): T = 38 | val lock = 39 | synchronized { 40 | file.getParentFile.mkdirs() 41 | try file.createNewFile() 42 | catch case e: IOException => throw new IOException(s"failed to create lock file $file", e) 43 | locks(file.getCanonicalFile, ()) 44 | } 45 | lock.withLock(action) 46 | 47 | private class GlobalLock(file: File): 48 | private var fileLocked = false 49 | def withLock[T](run: Callable[T]): T = 50 | synchronized { 51 | if fileLocked then run.call 52 | else 53 | fileLocked = true 54 | try 55 | ignoringDeadlockAvoided(run) 56 | finally 57 | fileLocked = false 58 | } 59 | 60 | // https://github.com/sbt/sbt/issues/650 61 | // This approach means a real deadlock won't be detected 62 | @tailrec private def ignoringDeadlockAvoided[T](run: Callable[T]): T = 63 | val result = 64 | try Some(withFileLock(run)) 65 | catch 66 | case i: IOException if isDeadlockAvoided(i) => 67 | // there should be a timeout to the deadlock avoidance, so this is just a backup 68 | Thread.sleep(200) 69 | None 70 | result match // workaround for no tailrec optimization in the above try/catch 71 | case Some(t) => t 72 | case None => ignoringDeadlockAvoided(run) 73 | 74 | // The actual message is not specified by FileChannel.lock, so this may need to be adjusted for different JVMs 75 | private def isDeadlockAvoided(i: IOException): Boolean = 76 | i.getMessage == "Resource deadlock avoided" 77 | 78 | private def withFileLock[T](run: Callable[T]): T = 79 | def withChannelRetries(retries: Int)(channel: FileChannel): T = 80 | try withChannel(channel) 81 | catch 82 | case i: InternalLockNPE => 83 | if retries > 0 then withChannelRetries(retries - 1)(channel) else throw i 84 | 85 | def withChannel(channel: FileChannel) = 86 | val freeLock = 87 | try channel.tryLock 88 | catch case e: NullPointerException => throw new InternalLockNPE(e) 89 | if freeLock eq null then 90 | Console.err.println("[info] waiting for lock on " + file + " to be available..."); 91 | val lock = 92 | try channel.lock 93 | catch case e: NullPointerException => throw new InternalLockNPE(e) 94 | try 95 | run.call 96 | finally 97 | lock.release() 98 | else 99 | try 100 | run.call 101 | finally 102 | freeLock.release() 103 | Using(new FileOutputStream(file).getChannel)(withChannelRetries(5)) 104 | private final class InternalLockNPE(cause: Exception) extends RuntimeException(cause) 105 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Pre.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.io.{ File, FileFilter } 9 | import java.net.URL 10 | import java.util.Locale 11 | import scala.collection.immutable.List 12 | import scala.reflect.ClassTag 13 | 14 | object Pre: 15 | def readLine(prompt: String): Option[String] = 16 | val c = System.console() 17 | if c eq null then None else Option(c.readLine(prompt)) 18 | def trimLeading(line: String) = 19 | def newStart(i: Int): Int = 20 | if i >= line.length || !Character.isWhitespace(line.charAt(i)) then i else newStart(i + 1) 21 | line.substring(newStart(0)) 22 | def isEmpty(line: String) = line.length == 0 23 | def isNonEmpty(line: String) = line.length > 0 24 | def assert(condition: Boolean, msg: => String): Unit = 25 | if !condition then throw new AssertionError(msg) 26 | def assert(condition: Boolean): Unit = assert(condition, "assertion failed") 27 | def require(condition: Boolean, msg: => String): Unit = 28 | if !condition then throw new IllegalArgumentException(msg) 29 | def error(msg: String): Nothing = throw new BootException(prefixError(msg)) 30 | def declined(msg: String): Nothing = throw new BootException(msg) 31 | def prefixError(msg: String): String = "error during sbt launcher: " + msg 32 | def toBoolean(s: String) = java.lang.Boolean.parseBoolean(s) 33 | 34 | def toArray[T: ClassTag](list: List[T]) = 35 | val arr = new Array[T](list.length) 36 | def copy(i: Int, rem: List[T]): Unit = 37 | if i < arr.length then 38 | arr(i) = rem.head 39 | copy(i + 1, rem.tail) 40 | copy(0, list) 41 | arr 42 | /* These exist in order to avoid bringing in dependencies on RichInt and ArrayBuffer, among others. */ 43 | def concat(a: Array[File], b: Array[File]): Array[File] = 44 | val n = new Array[File](a.length + b.length) 45 | java.lang.System.arraycopy(a, 0, n, 0, a.length) 46 | java.lang.System.arraycopy(b, 0, n, a.length, b.length) 47 | n 48 | def array(files: File*): Array[File] = toArray(files.toList) 49 | /* Saves creating a closure for default if it has already been evaluated*/ 50 | def orElse[T](opt: Option[T], default: T) = if opt.isDefined then opt.get else default 51 | 52 | def wrapNull(a: Array[File]): Array[File] = if a == null then new Array[File](0) else a 53 | def const[B](b: B): Any => B = _ => b 54 | def strictOr[T](a: Option[T], b: Option[T]): Option[T] = a match 55 | case None => b; 56 | case _ => a 57 | def getOrError[T](a: Option[T], msg: String): T = a match 58 | case None => error(msg); 59 | case Some(x) => x 60 | def orNull[T >: Null](t: Option[T]): T = t match 61 | case None => null; 62 | case Some(x) => x 63 | 64 | def getJars(directories: List[File]): Array[File] = 65 | toArray(directories.flatMap(directory => wrapNull(directory.listFiles(JarFilter)))) 66 | 67 | object JarFilter extends FileFilter: 68 | def accept(file: File) = !file.isDirectory && file.getName.endsWith(".jar") 69 | def getMissing(loader: ClassLoader, classes: Iterable[String]): Iterable[String] = 70 | def classMissing(c: String) = 71 | try 72 | Class.forName(c, false, loader); false 73 | catch case _: ClassNotFoundException => true 74 | classes.toList.filter(classMissing) 75 | def toURLs(files: Array[File]): Array[URL] = files.map(_.toURI.toURL) 76 | def toFile(url: URL): File = 77 | try new File(url.toURI) 78 | catch case _: java.net.URISyntaxException => new File(url.getPath) 79 | 80 | def delete(f: File): Unit = 81 | if f.isDirectory then 82 | val fs = f.listFiles() 83 | if fs ne null then fs foreach delete 84 | if f.exists then f.delete() 85 | () 86 | final val isWindows: Boolean = 87 | System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows") 88 | final val isCygwin: Boolean = isWindows && java.lang.Boolean.getBoolean("sbt.cygwin") 89 | 90 | private[boot] def substituteTilde(path: String): String = 91 | path.replaceFirst("^~(/|\\\\|$)", System.getProperty("user.home") + "$1") 92 | 93 | import java.util.Properties 94 | import java.io.{ FileInputStream, FileOutputStream } 95 | private[boot] def readProperties(propertiesFile: File) = 96 | val properties = new Properties 97 | if propertiesFile.exists then Using(new FileInputStream(propertiesFile))(properties.load) 98 | properties 99 | private[boot] def writeProperties(properties: Properties, file: File, msg: String): Unit = 100 | file.getParentFile.mkdirs() 101 | Using(new FileOutputStream(file))(out => properties.store(out, msg)) 102 | private[boot] def setSystemProperties(properties: Properties): Unit = 103 | val nameItr = properties.stringPropertyNames.iterator 104 | while nameItr.hasNext do 105 | val propName = nameItr.next 106 | System.setProperty(propName, properties.getProperty(propName)) 107 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/BootConfiguration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.io.File 9 | 10 | // 11 | // [.]scala-/ [baseDirectoryName] 12 | // lib/ [ScalaDirectoryName] 13 | // -/ [appDirectoryName] 14 | // 15 | // see also ProjectProperties for the set of constants that apply to the build.properties file in a project 16 | // The scala organization is used as a prefix in baseDirectoryName when a non-standard organization is used. 17 | private[boot] object BootConfiguration: 18 | // these are the Scala module identifiers to resolve/retrieve 19 | val ScalaOrg = "org.scala-lang" 20 | val CompilerModuleName = "scala-compiler" 21 | val Compiler3ModuleName = "scala3-compiler_3" 22 | val LibraryModuleName = "scala-library" 23 | val Library3ModuleName = "scala3-library_3" 24 | 25 | val JUnitName = "junit" 26 | val JAnsiVersion = "1.18" 27 | 28 | val SbtOrg = "org.scala-sbt" 29 | 30 | /** The Ivy conflict manager to use for updating. */ 31 | val ConflictManagerName = "latest-revision" 32 | 33 | /** The name of the local Ivy repository, which is used when compiling sbt from source. */ 34 | val LocalIvyName = "local" 35 | 36 | /** The pattern used for the local Ivy repository, which is used when compiling sbt from source. */ 37 | val LocalPattern = "[organisation]/[module]/[revision]/[type]s/[artifact](-[classifier]).[ext]" 38 | 39 | /** The artifact pattern used for the local Ivy repository. */ 40 | def LocalArtifactPattern = LocalPattern 41 | 42 | /** The Ivy pattern used for the local Ivy repository. */ 43 | def LocalIvyPattern = LocalPattern 44 | 45 | final val FjbgPackage = "ch.epfl.lamp.fjbg." 46 | 47 | /** The class name prefix used to hide the Scala classes used by this loader from the application */ 48 | final val ScalaPackage = "scala." 49 | 50 | /** The class name prefix used to hide the Ivy classes used by this loader from the application */ 51 | final val IvyPackage = "org.apache.ivy." 52 | 53 | /** 54 | * The class name prefix used to hide the launcher classes from the application. 55 | * Note that access to xsbti classes are allowed. 56 | */ 57 | final val SbtBootPackage = "xsbt.boot." 58 | 59 | /** 60 | * The loader will check that these classes can be loaded and will assume that their presence indicates 61 | * the Scala compiler and library have been downloaded. 62 | */ 63 | val TestLoadScala2Classes = "scala.Option" :: "scala.tools.nsc.Global" :: Nil 64 | val TestLoadScala3Classes = "scala.Option" :: "dotty.tools.dotc.Driver" :: Nil 65 | 66 | val ScalaHomeProperty = "scala.home" 67 | val UpdateLogName = "update.log" 68 | val DefaultChecksums = "sha1" :: "md5" :: Nil 69 | 70 | val DefaultIvyConfiguration = "default" 71 | 72 | /** The name of the directory within the boot directory to retrieve scala to. */ 73 | val ScalaDirectoryName = "lib" 74 | 75 | /** 76 | * The Ivy pattern to use for retrieving the scala compiler and library. It is relative to the directory 77 | * containing all jars for the requested version of scala. 78 | */ 79 | val scalaRetrievePattern = ScalaDirectoryName + "/[artifact](-[classifier]).[ext]" 80 | 81 | def artifactType(classifier: String) = 82 | classifier match 83 | case "sources" => "src" 84 | case "javadoc" => "doc" 85 | case _ => "jar" 86 | 87 | /** 88 | * The Ivy pattern to use for retrieving the application and its dependencies. It is relative to the directory 89 | * containing all jars for the requested version of scala. 90 | */ 91 | def appRetrievePattern(appID: xsbti.ApplicationID) = 92 | appDirectoryName(appID, "/") + "(/[component])/[artifact]-[revision](-[classifier]).[ext]" 93 | 94 | val ScalaVersionPrefix = "scala-" 95 | 96 | /** The name of the directory to retrieve the application and its dependencies to. */ 97 | def appDirectoryName(appID: xsbti.ApplicationID, sep: String) = 98 | appID.groupID + sep + appID.name + sep + appID.version 99 | 100 | /** The name of the directory in the boot directory to put all jars for the given version of scala in. */ 101 | def baseDirectoryName(scalaOrg: String, scalaVersion: Option[String]) = scalaVersion match 102 | case None => "other" 103 | case Some(sv) => (if scalaOrg == ScalaOrg then "" else scalaOrg + ".") + ScalaVersionPrefix + sv 104 | 105 | def extractScalaVersion(dir: File): Option[String] = 106 | val name = dir.getName 107 | if name.contains(ScalaVersionPrefix) then 108 | Some(name.substring(name.lastIndexOf(ScalaVersionPrefix) + ScalaVersionPrefix.length)) 109 | else None 110 | 111 | private[boot] final class ProxyProperties( 112 | val envURL: String, 113 | val envUser: String, 114 | val envPassword: String, 115 | val sysHost: String, 116 | val sysPort: String, 117 | val sysUser: String, 118 | val sysPassword: String 119 | ) 120 | 121 | private[boot] object ProxyProperties: 122 | val http = apply("http") 123 | val https = apply("https") 124 | val ftp = apply("ftp") 125 | 126 | def apply(pre: String) = new ProxyProperties( 127 | pre + "_proxy", 128 | pre + "_proxy_user", 129 | pre + "_proxy_pass", 130 | pre + ".proxyHost", 131 | pre + ".proxyPort", 132 | pre + ".proxyUser", 133 | pre + ".proxyPassword" 134 | ) 135 | -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/ScalaProviderTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.io.{ File, InputStream } 9 | import java.util.Properties 10 | import xsbti.{ Repository as _, Launcher as _, * } 11 | import LaunchTest.* 12 | import sbt.io.IO.{ createDirectory, touch, withTemporaryDirectory } 13 | 14 | object ScalaProviderTest extends verify.BasicTestSuite: 15 | test("Launch should provide ClassLoader for Scala 2.10.7") { 16 | checkScalaLoader("2.10.7") 17 | } 18 | 19 | test("Launch should provide ClassLoader for Scala 2.11.12") { 20 | checkScalaLoader("2.11.12") 21 | } 22 | 23 | test("Launch should provide ClassLoader for Scala 2.12.17") { 24 | checkScalaLoader("2.12.17") 25 | } 26 | 27 | test("Launch should provide ClassLoader for Scala 2.13.9") { 28 | checkScalaLoader("2.13.9") 29 | } 30 | 31 | // Scala version detection picks up 2.13 32 | // test("Launch should provide ClassLoader for Scala 3.2.0") { 33 | // checkScalaLoader("3.2.0") 34 | // } 35 | 36 | test( 37 | "Launch should successfully load an application from local repository and run it with correct arguments" 38 | ) { 39 | assert(checkLoad(List("test"), "xsbt.boot.test.ArgumentTest").asInstanceOf[Exit].code == 0) 40 | intercept[RuntimeException] { 41 | checkLoad(List(), "xsbt.boot.test.ArgumentTest") 42 | () 43 | } 44 | } 45 | 46 | test( 47 | "Launch should successfully load an plain application from local repository and run it with correct arguments" 48 | ) { 49 | assert(checkLoad(List("test"), "xsbt.boot.test.PlainArgumentTest").asInstanceOf[Exit].code == 0) 50 | intercept[RuntimeException] { 51 | checkLoad(List(), "xsbt.boot.test.PlainArgumentTest") 52 | () 53 | } 54 | } 55 | 56 | test("Launch should successfully load an application instead of the plain application") { 57 | assert(checkLoad(List(), "xsbt.boot.test.PriorityTest").asInstanceOf[Exit].code == 0) 58 | } 59 | 60 | test( 61 | "Launch should successfully load an application from local repository and run it with correct sbt version" 62 | ) { 63 | assert( 64 | checkLoad(List(AppVersion), "xsbt.boot.test.AppVersionTest").asInstanceOf[Exit].code == 0 65 | ) 66 | } 67 | 68 | test("Launch should add extra resources to the classpath") { 69 | assert( 70 | checkLoad(testResources, "xsbt.boot.test.ExtraTest", createExtra).asInstanceOf[Exit].code == 0 71 | ) 72 | } 73 | 74 | def checkLoad(arguments: List[String], mainClassName: String): MainResult = 75 | checkLoad(arguments, mainClassName, _ => Array[File]()) 76 | 77 | def checkLoad( 78 | arguments: List[String], 79 | mainClassName: String, 80 | extra: File => Array[File] 81 | ): MainResult = 82 | withTemporaryDirectory { currentDirectory => 83 | withLauncher { launcher => 84 | Launch.run(launcher)( 85 | new RunConfiguration( 86 | Some(LaunchTest.getScalaVersion), 87 | LaunchTest.testApp(mainClassName, extra(currentDirectory)).toID, 88 | currentDirectory, 89 | arguments 90 | ) 91 | ) 92 | } 93 | } 94 | 95 | private def testResources = List("test-resourceA", "a/b/test-resourceB", "sub/test-resource") 96 | 97 | private def createExtra(currentDirectory: File) = 98 | val resourceDirectory = new File(currentDirectory, "resources") 99 | createDirectory(resourceDirectory) 100 | testResources.foreach(resource => 101 | touch(new File(resourceDirectory, resource.replace('/', File.separatorChar))) 102 | ) 103 | Array(resourceDirectory) 104 | 105 | private def checkScalaLoader(version: String) = 106 | withLauncher(checkLauncher(version, version)) 107 | 108 | private def checkLauncher(version: String, versionValue: String)(launcher: xsbti.Launcher) = 109 | import scala.reflect.Selectable.reflectiveSelectable 110 | val provider = launcher.getScala(version) 111 | val loader = provider.loader 112 | // ensure that this loader can load Scala classes by trying scala.ScalaObject. 113 | tryScala(loader, loader.getParent) 114 | assert(getScalaVersion(loader) == versionValue) 115 | 116 | val libraryLoader = provider.loader.getParent 117 | // Test the structural type 118 | libraryLoader match 119 | case x: (ClassLoader & LibraryLoader) @unchecked => 120 | assert(x.scalaVersion == version) 121 | tryScala(libraryLoader, libraryLoader) 122 | 123 | private def tryScala(loader: ClassLoader, libraryLoader: ClassLoader) = 124 | assert(Class.forName("scala.Product", false, loader).getClassLoader == libraryLoader) 125 | 126 | type LibraryLoader = { def scalaVersion: String } 127 | 128 | object LaunchTest: 129 | def testApp(main: String): Application = testApp(main, Array[File]()) 130 | def testApp(main: String, extra: Array[File]): Application = 131 | Application( 132 | "org.scala-sbt", 133 | Value.Explicit("launch-test"), 134 | Value.Explicit(AppVersion), 135 | main, 136 | Nil, 137 | CrossValue.Disabled, 138 | extra 139 | ) 140 | import Predefined.* 141 | def testRepositories = 142 | List(Local, MavenCentral, SonatypeOSSSnapshots).map(Repository.Predefined(_)) 143 | def withLauncher[T](f: xsbti.Launcher => T): T = 144 | withTemporaryDirectory { bootDirectory => 145 | f(Launcher(bootDirectory, testRepositories)) 146 | } 147 | 148 | def getScalaVersion: String = "3.7.2" // getScalaVersion(getClass.getClassLoader) 149 | def getScalaVersion(loader: ClassLoader): String = 150 | getProperty(loader, "library.properties", "version.number") 151 | lazy val AppVersion = 152 | getProperty(getClass.getClassLoader, "sbt.launcher.version.properties", "version") 153 | 154 | private def getProperty(loader: ClassLoader, res: String, prop: String) = 155 | loadProperties(loader.getResourceAsStream(res)).getProperty(prop) 156 | private def loadProperties(propertiesStream: InputStream): Properties = 157 | val properties = new Properties 158 | try 159 | properties.load(propertiesStream) 160 | finally 161 | propertiesStream.close() 162 | properties 163 | -------------------------------------------------------------------------------- /launcher-implementation/src/test/scala/ConfigurationParserTest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import java.net.URI 9 | 10 | object ConfigurationParserTest extends verify.BasicTestSuite: 11 | test("Configuration parser should correct parse bootOnly") { 12 | repoFileContains( 13 | """|[repositories] 14 | | local: bootOnly""".stripMargin, 15 | Repository.Predefined("local", true) 16 | ) 17 | 18 | repoFileContains( 19 | """|[repositories] 20 | | local""".stripMargin, 21 | Repository.Predefined("local", false) 22 | ) 23 | 24 | repoFileContains( 25 | """|[repositories] 26 | | id: https://repo1.maven.org""".stripMargin, 27 | Repository.Maven("id", new URI("https://repo1.maven.org").toURL, false) 28 | ) 29 | 30 | repoFileContains( 31 | """|[repositories] 32 | | id: https://repo1.maven.org, bootOnly""".stripMargin, 33 | Repository.Maven("id", new URI("https://repo1.maven.org").toURL, true) 34 | ) 35 | 36 | repoFileContains( 37 | """|[repositories] 38 | | id: http://repo1.maven.org, bootOnly, allowInsecureProtocol""".stripMargin, 39 | Repository.Maven("id", new URI("http://repo1.maven.org").toURL, true, true) 40 | ) 41 | 42 | repoFileContains( 43 | """|[repositories] 44 | | id: https://repo1.maven.org, [orgPath]""".stripMargin, 45 | Repository 46 | .Ivy("id", new URI("https://repo1.maven.org").toURL, "[orgPath]", "[orgPath]", false, false) 47 | ) 48 | 49 | repoFileContains( 50 | """|[repositories] 51 | | id: https://repo1.maven.org, [orgPath], mavenCompatible""".stripMargin, 52 | Repository 53 | .Ivy("id", new URI("https://repo1.maven.org").toURL, "[orgPath]", "[orgPath]", true, false) 54 | ) 55 | 56 | repoFileContains( 57 | """|[repositories] 58 | | id: https://repo1.maven.org, [orgPath], mavenCompatible, bootOnly""".stripMargin, 59 | Repository 60 | .Ivy("id", new URI("https://repo1.maven.org").toURL, "[orgPath]", "[orgPath]", true, true) 61 | ) 62 | 63 | repoFileContains( 64 | """|[repositories] 65 | | id: https://repo1.maven.org, [orgPath], bootOnly, mavenCompatible""".stripMargin, 66 | Repository 67 | .Ivy("id", new URI("https://repo1.maven.org").toURL, "[orgPath]", "[orgPath]", true, true) 68 | ) 69 | 70 | repoFileContains( 71 | """|[repositories] 72 | | id: https://repo1.maven.org, [orgPath], bootOnly""".stripMargin, 73 | Repository 74 | .Ivy("id", new URI("https://repo1.maven.org").toURL, "[orgPath]", "[orgPath]", false, true) 75 | ) 76 | 77 | repoFileContains( 78 | """|[repositories] 79 | | id: https://repo1.maven.org, [orgPath], [artPath]""".stripMargin, 80 | Repository 81 | .Ivy("id", new URI("https://repo1.maven.org").toURL, "[orgPath]", "[artPath]", false, false) 82 | ) 83 | 84 | repoFileContains( 85 | """|[repositories] 86 | | id: https://repo1.maven.org, [orgPath], [artPath], descriptorOptional""".stripMargin, 87 | Repository.Ivy( 88 | "id", 89 | new URI("https://repo1.maven.org").toURL, 90 | "[orgPath]", 91 | "[artPath]", 92 | false, 93 | false, 94 | true, 95 | false 96 | ) 97 | ) 98 | 99 | repoFileContains( 100 | """|[repositories] 101 | | id: https://repo1.maven.org, [orgPath], [artPath], descriptorOptional, skipConsistencyCheck""".stripMargin, 102 | Repository.Ivy( 103 | "id", 104 | new URI("https://repo1.maven.org").toURL, 105 | "[orgPath]", 106 | "[artPath]", 107 | false, 108 | false, 109 | true, 110 | true 111 | ) 112 | ) 113 | 114 | repoFileContains( 115 | """|[repositories] 116 | | id: https://repo1.maven.org, [orgPath], [artPath], skipConsistencyCheck, descriptorOptional""".stripMargin, 117 | Repository.Ivy( 118 | "id", 119 | new URI("https://repo1.maven.org").toURL, 120 | "[orgPath]", 121 | "[artPath]", 122 | false, 123 | false, 124 | true, 125 | true 126 | ) 127 | ) 128 | 129 | repoFileContains( 130 | """|[repositories] 131 | | id: https://repo1.maven.org, [orgPath], [artPath], skipConsistencyCheck, descriptorOptional, mavenCompatible, bootOnly""".stripMargin, 132 | Repository.Ivy( 133 | "id", 134 | new URI("https://repo1.maven.org").toURL, 135 | "[orgPath]", 136 | "[artPath]", 137 | true, 138 | true, 139 | true, 140 | true 141 | ) 142 | ) 143 | 144 | repoFileContains( 145 | """|[repositories] 146 | | id: https://repo1.maven.org, [orgPath], [artPath], bootOnly""".stripMargin, 147 | Repository 148 | .Ivy("id", new URI("https://repo1.maven.org").toURL, "[orgPath]", "[artPath]", false, true) 149 | ) 150 | 151 | repoFileContains( 152 | """|[repositories] 153 | | id: https://repo1.maven.org, [orgPath], [artPath], bootOnly, mavenCompatible""".stripMargin, 154 | Repository 155 | .Ivy("id", new URI("https://repo1.maven.org").toURL, "[orgPath]", "[artPath]", true, true) 156 | ) 157 | 158 | repoFileContains( 159 | """|[repositories] 160 | | id: https://repo1.maven.org, [orgPath], [artPath], mavenCompatible, bootOnly""".stripMargin, 161 | Repository 162 | .Ivy("id", new URI("https://repo1.maven.org").toURL, "[orgPath]", "[artPath]", true, true) 163 | ) 164 | 165 | repoFileContains( 166 | """|[repositories] 167 | | id: http://repo1.maven.org, [orgPath], [artPath], mavenCompatible, bootOnly, allowInsecureProtocol""".stripMargin, 168 | Repository.Ivy( 169 | "id", 170 | new URI("http://repo1.maven.org").toURL, 171 | "[orgPath]", 172 | "[artPath]", 173 | mavenCompatible = true, 174 | bootOnly = true, 175 | allowInsecureProtocol = true 176 | ) 177 | ) 178 | } 179 | 180 | def repoFileContains(file: String, repo: Repository.Repository) = 181 | assert(loadRepoFile(file).contains(repo)) 182 | 183 | def loadRepoFile(file: String) = 184 | (new ConfigurationParser).readRepositoriesConfig(file) 185 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/Configuration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import java.io.{ File, InputStreamReader } 10 | import java.net.{ MalformedURLException, URI, URL } 11 | import java.util.regex.Pattern 12 | import scala.collection.immutable.List 13 | import scala.annotation.{ nowarn, tailrec } 14 | 15 | object ConfigurationStorageState extends Enumeration: 16 | val PropertiesFile = value("properties-file") 17 | val SerializedFile = value("serialized-file") 18 | 19 | object Configuration: 20 | import ConfigurationStorageState.* 21 | final val SysPropPrefix = "-D" 22 | 23 | @nowarn 24 | def parse(file: URL, baseDirectory: File) = 25 | Using(new InputStreamReader(file.openStream, "utf8"))((new ConfigurationParser).apply) 26 | 27 | /** 28 | * Finds the configuration location. 29 | * 30 | * Note: Configuration may be previously serialized by a launcher. 31 | */ 32 | @tailrec def find( 33 | args: List[String], 34 | baseDirectory: File 35 | ): (URL, List[String], ConfigurationStorageState.Value) = 36 | args match 37 | case head :: tail if head.startsWith("@load:") => 38 | (directConfiguration(head.substring(6), baseDirectory), tail, SerializedFile) 39 | case head :: tail if head.startsWith("@") => 40 | (directConfiguration(head.substring(1), baseDirectory), tail, PropertiesFile) 41 | case head :: tail if head.startsWith(SysPropPrefix) => 42 | setProperty(head stripPrefix SysPropPrefix) 43 | find(tail, baseDirectory) 44 | case _ => 45 | val propertyConfigured = System.getProperty("sbt.boot.properties") 46 | val url = 47 | if propertyConfigured == null then configurationOnClasspath 48 | else configurationFromFile(propertyConfigured, baseDirectory) 49 | (url, args, PropertiesFile) 50 | def setProperty(head: String): Unit = 51 | head.split("=", 2) match 52 | case Array("") => Console.err.println(s"[warn] [launcher] invalid system property '$head'") 53 | case Array(key) => sys.props += key -> "" 54 | case Array(key, value) => sys.props += key -> value 55 | case _ => () 56 | () 57 | def configurationOnClasspath: URL = 58 | val paths = resourcePaths(guessSbtVersion) 59 | paths.iterator.map(getClass.getResource).find(neNull) getOrElse 60 | (multiPartError("could not find sbt launch configuration. searched classpath for:", paths)) 61 | def directConfiguration(path: String, baseDirectory: File): URL = 62 | try new URL(path) 63 | catch case _: MalformedURLException => configurationFromFile(path, baseDirectory) 64 | def configurationFromFile(path: String, baseDirectory: File): URL = 65 | val pathURI = filePathURI(path) 66 | def resolve(against: URI): Option[URL] = 67 | val resolved = 68 | against.resolve(pathURI) // variant that accepts String doesn't properly escape (#725) 69 | val exists = 70 | try (new File(resolved)).exists 71 | catch case _: IllegalArgumentException => false 72 | if exists then Some(resolved.toURL) else None 73 | val against = resolveAgainst(baseDirectory) 74 | // use Iterators so that resolution occurs lazily, for performance 75 | val resolving = against.iterator.flatMap(e => resolve(e).toList.iterator) 76 | if !resolving.hasNext then 77 | multiPartError("could not find configuration file '" + path + "'. searched:", against) 78 | resolving.next() 79 | def multiPartError[A](firstLine: String, lines: List[A]) = 80 | Pre.error((firstLine :: lines.map(_.toString())).mkString("\n\t")) 81 | 82 | def UnspecifiedVersionPart = "Unspecified" 83 | def DefaultVersionPart = "Default" 84 | def DefaultBuildProperties = "project/build.properties" 85 | def SbtVersionProperty = "sbt.version" 86 | val ConfigurationName = "sbt.boot.properties" 87 | val JarBasePath = "/sbt/" 88 | def userConfigurationPath = "/" + ConfigurationName 89 | def defaultConfigurationPath = JarBasePath + ConfigurationName 90 | val baseResourcePaths: List[String] = userConfigurationPath :: defaultConfigurationPath :: Nil 91 | def resourcePaths(sbtVersion: Option[String]): List[String] = 92 | versionParts(sbtVersion) flatMap { part => 93 | baseResourcePaths map { base => 94 | base + part 95 | } 96 | } 97 | def fallbackParts: List[String] = "" :: Nil 98 | def versionParts(version: Option[String]): List[String] = 99 | version match 100 | case None => UnspecifiedVersionPart :: fallbackParts 101 | case Some(v) => versionParts(v) 102 | def versionParts(version: String): List[String] = 103 | val pattern = Pattern.compile("""(\d+)(\.\d+)(\.\d+)(-.*)?""") 104 | val m = pattern.matcher(version) 105 | if m.matches() then 106 | subPartsIndices flatMap { is => 107 | fullMatchOnly(is.map(m.group)) 108 | } 109 | else noMatchParts 110 | def noMatchParts: List[String] = DefaultVersionPart :: fallbackParts 111 | private def fullMatchOnly(groups: List[String]): Option[String] = 112 | if groups.forall(neNull) then Some(groups.mkString) else None 113 | 114 | private def subPartsIndices = 115 | (1 :: 2 :: 3 :: 4 :: Nil) :: 116 | (1 :: 2 :: 3 :: Nil) :: 117 | (1 :: 2 :: Nil) :: 118 | (Nil) :: 119 | Nil 120 | 121 | // the location of project/build.properties and the name of the property within that file 122 | // that configures the sbt version is configured in sbt.boot.properties. 123 | // We have to hard code them here in order to use them to determine the location of sbt.boot.properties itself 124 | def guessSbtVersion: Option[String] = 125 | val props = Pre.readProperties(new File(DefaultBuildProperties)) 126 | Option(props.getProperty(SbtVersionProperty)) 127 | 128 | def resolveAgainst(baseDirectory: File): List[URI] = 129 | directoryURI(baseDirectory) :: 130 | directoryURI(new File(System.getProperty("user.home"))) :: 131 | toDirectory(classLocation(getClass).toURI) :: 132 | Nil 133 | 134 | def classLocation(cl: Class[?]): URL = 135 | val codeSource = cl.getProtectionDomain.getCodeSource 136 | if codeSource == null then Pre.error("no class location for " + cl) 137 | else codeSource.getLocation 138 | // single-arg constructor doesn't properly escape 139 | def filePathURI(path: String): URI = 140 | if path.startsWith("file:") then new URI(path) 141 | else 142 | val f = new File(path) 143 | new URI(if f.isAbsolute then "file" else null, path, null) 144 | def directoryURI(dir: File): URI = directoryURI(dir.toURI) 145 | def directoryURI(uri: URI): URI = 146 | assert(uri.isAbsolute) 147 | val str = uri.toASCIIString 148 | val dirStr = if str.endsWith("/") then str else str + "/" 149 | (new URI(dirStr)).normalize 150 | 151 | def toDirectory(uri: URI): URI = 152 | try 153 | val file = new File(uri) 154 | val newFile = if file.isFile then file.getParentFile else file 155 | directoryURI(newFile) 156 | catch case _: Exception => uri 157 | private def neNull: AnyRef => Boolean = _ ne null 158 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/ServerApplication.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt 7 | package boot 8 | 9 | import java.io.File 10 | import java.net.URI 11 | import java.io.IOException 12 | import Pre.* 13 | import scala.annotation.tailrec 14 | 15 | /** A wrapper around 'raw' static methods to meet the sbt application interface. */ 16 | class ServerApplication private (provider: xsbti.AppProvider) extends xsbti.AppMain: 17 | import ServerApplication.* 18 | 19 | override def run(configuration: xsbti.AppConfiguration): xsbti.MainResult = 20 | val serverMain = 21 | provider.entryPoint.asSubclass(ServerMainClass).getDeclaredConstructor().newInstance() 22 | val server = serverMain.start(configuration) 23 | Console.err.println(s"${SERVER_SYNCH_TEXT}${server.uri}") 24 | server.awaitTermination() 25 | 26 | /** An object that lets us detect compatible "plain" applications and launch them reflectively. */ 27 | object ServerApplication: 28 | val SERVER_SYNCH_TEXT = "[SERVER-URI]" 29 | val ServerMainClass = classOf[xsbti.ServerMain] 30 | // TODO - We should also adapt friendly static methods into servers, perhaps... 31 | // We could even structurally type things that have a uri + awaitTermination method... 32 | def isServerApplication(clazz: Class[?]): Boolean = 33 | ServerMainClass.isAssignableFrom(clazz) 34 | def apply(provider: xsbti.AppProvider): xsbti.AppMain = 35 | new ServerApplication(provider) 36 | 37 | object ServerLocator: 38 | // TODO - Probably want to drop this to reduce classfile size 39 | private def locked[U](file: File)(f: => U): U = 40 | Locks( 41 | file, 42 | new java.util.concurrent.Callable[U]: 43 | def call(): U = f 44 | ) 45 | // We use the lock file they give us to write the server info. However, 46 | // it seems we cannot both use the server info file for locking *and* 47 | // read from it successfully. Locking seems to blank the file. SO, we create 48 | // another file near the info file to lock.a 49 | def makeLockFile(f: File): File = 50 | new File(f.getParentFile, s"${f.getName}.lock") 51 | // Launch the process and read the port... 52 | def locate(currentDirectory: File, config: LaunchConfiguration): URI = 53 | config.serverConfig match 54 | case None => sys.error("no server lock file configured. cannot locate server.") 55 | case Some(sc) => 56 | locked(makeLockFile(sc.lockFile)) { 57 | readProperties(sc.lockFile) match 58 | case Some(uri) if isReachable(uri) => uri 59 | case _ => 60 | val uri = ServerLauncher.startServer(currentDirectory, config) 61 | writeProperties(sc.lockFile, uri) 62 | uri 63 | } 64 | 65 | private val SERVER_URI_PROPERTY = "server.uri" 66 | def readProperties(f: File): Option[java.net.URI] = 67 | try 68 | val props = Pre.readProperties(f) 69 | props.getProperty(SERVER_URI_PROPERTY) match 70 | case null => None 71 | case uri => Some(new java.net.URI(uri)) 72 | catch case _: IOException => None 73 | def writeProperties(f: File, uri: URI): Unit = 74 | val props = new java.util.Properties 75 | props.setProperty(SERVER_URI_PROPERTY, uri.toASCIIString) 76 | val df = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mmZ") 77 | df.setTimeZone(java.util.TimeZone.getTimeZone("UTC")) 78 | Pre.writeProperties(props, f, s"Server Startup at ${df.format(new java.util.Date)}") 79 | 80 | def isReachable(uri: java.net.URI): Boolean = 81 | try 82 | // TODO - For now we assume if we can connect, it means 83 | // that the server is working... 84 | val socket = new java.net.Socket(uri.getHost, uri.getPort) 85 | try socket.isConnected 86 | finally socket.close() 87 | catch case _: IOException => false 88 | 89 | /** A helper class that dumps incoming values into a print stream. */ 90 | class StreamDumper(in: java.io.BufferedReader, out: java.io.PrintStream) extends Thread: 91 | // Don't block the application for this thread. 92 | setDaemon(true) 93 | val endTime = new java.util.concurrent.atomic.AtomicLong(Long.MaxValue) 94 | override def run(): Unit = 95 | def read(): Unit = if endTime.get > System.currentTimeMillis then 96 | in.readLine match 97 | case null => () 98 | case line => 99 | out.println(line) 100 | out.flush() 101 | read() 102 | read() 103 | out.close() 104 | 105 | def close(waitForErrors: Boolean): Unit = 106 | // closing "in" blocks forever on Windows, so don't do it; 107 | // just wait a couple seconds to read more stuff if there is 108 | // any stuff. 109 | if waitForErrors then 110 | endTime.set(System.currentTimeMillis + 5000) 111 | // at this point we'd rather the dumper thread run 112 | // before we check whether to sleep 113 | Thread.`yield`() 114 | // let ourselves read more (thread should exit on earlier of endTime or EOF) 115 | while isAlive() && (endTime.get > System.currentTimeMillis) do Thread.sleep(50) 116 | else endTime.set(System.currentTimeMillis) 117 | object ServerLauncher: 118 | import ServerApplication.SERVER_SYNCH_TEXT 119 | def startServer(currentDirectory: File, config: LaunchConfiguration): URI = 120 | val serverConfig = config.serverConfig match 121 | case Some(c) => c 122 | case None => 123 | throw new RuntimeException( 124 | "logic failure: attempting to start a server that isn't configured to be a server. please report a bug." 125 | ) 126 | val launchConfig = java.io.File.createTempFile("sbtlaunch", "config") 127 | if System.getenv("SBT_SERVER_SAVE_TEMPS") eq null then launchConfig.deleteOnExit() 128 | LaunchConfiguration.save(config, launchConfig) 129 | val jvmArgs: List[String] = serverConfig.jvmArgs map readLines match 130 | case Some(args) => args 131 | case None => Nil 132 | val cmd: List[String] = 133 | ("java" :: jvmArgs) ++ 134 | ("-jar" :: defaultLauncherLookup.getCanonicalPath :: s"@load:${launchConfig.toURI.toURL.toString}" :: Nil) 135 | launchProcessAndGetUri(cmd, currentDirectory) 136 | 137 | // Here we try to isolate all the stupidity of dealing with Java processes. 138 | def launchProcessAndGetUri(cmd: List[String], cwd: File): URI = 139 | // TODO - Handle windows path stupidity in arguments. 140 | val pb = new java.lang.ProcessBuilder() 141 | pb.command(cmd*) 142 | pb.directory(cwd) 143 | val process = pb.start() 144 | // First we need to grab all the input streams, and close the ones we don't care about. 145 | process.getOutputStream.close() 146 | val stderr = process.getErrorStream 147 | val stdout = process.getInputStream 148 | // Now we start dumping out errors. 149 | val errorDumper = new StreamDumper( 150 | new java.io.BufferedReader(new java.io.InputStreamReader(stderr)), 151 | System.err 152 | ) 153 | errorDumper.start() 154 | // Now we look for the URI synch value, and then make sure we close the output files. 155 | try 156 | readUntilSynch(new java.io.BufferedReader(new java.io.InputStreamReader(stdout))) match 157 | case Some(uri) => uri 158 | case _ => 159 | // attempt to get rid of the server (helps prevent hanging / stuck locks, 160 | // though this is not reliable) 161 | try process.destroy() 162 | catch 163 | case _: Exception => 164 | // block a second to try to get stuff from stderr 165 | errorDumper.close(waitForErrors = true) 166 | sys.error(s"failed to start server process in ${pb.directory} command line ${pb.command}") 167 | finally 168 | errorDumper.close(waitForErrors = false) 169 | stdout.close() 170 | // Do not close stderr here because on Windows that will block, 171 | // and since the child process has no reason to exit, it may 172 | // block forever. errorDumper.close() instead owns the problem 173 | // of deciding what to do with stderr. 174 | 175 | object ServerUriLine: 176 | def unapply(in: String): Option[URI] = 177 | if in.startsWith(SERVER_SYNCH_TEXT) then Some(new URI(in.substring(SERVER_SYNCH_TEXT.size))) 178 | else None 179 | 180 | /** Reads an input steam until it hits the server synch text and server URI. */ 181 | def readUntilSynch(in: java.io.BufferedReader): Option[URI] = 182 | @tailrec 183 | def read(): Option[URI] = in.readLine match 184 | case null => None 185 | case ServerUriLine(uri) => Some(uri) 186 | case _ => read() 187 | try read() 188 | finally in.close() 189 | 190 | /** Reads all the lines in a file. If it doesn't exist, returns an empty list. Forces UTF-8 strings. */ 191 | def readLines(f: File): List[String] = 192 | if !f.exists then Nil 193 | else 194 | val reader = new java.io.BufferedReader( 195 | new java.io.InputStreamReader(new java.io.FileInputStream(f), "UTF-8") 196 | ) 197 | @tailrec 198 | def read(current: List[String]): List[String] = 199 | reader.readLine match 200 | case null => current.reverse 201 | case line => read(line :: current) 202 | try read(Nil) 203 | finally reader.close() 204 | 205 | def defaultLauncherLookup: File = 206 | try 207 | val classInLauncher = classOf[AppConfiguration] 208 | val fileOpt = for 209 | domain <- Option(classInLauncher.getProtectionDomain) 210 | source <- Option(domain.getCodeSource) 211 | location = source.getLocation 212 | yield toFile(location) 213 | fileOpt.getOrElse( 214 | throw new RuntimeException("could not inspect protection domain or code source") 215 | ) 216 | catch case e: Throwable => throw new RuntimeException("unable to find sbt-launch.jar.", e) 217 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/LaunchConfiguration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import java.io.File 10 | import java.net.URL 11 | import scala.annotation.nowarn 12 | import scala.collection.immutable.List 13 | 14 | //TODO: use copy constructor, check size change 15 | final case class LaunchConfiguration( 16 | scalaVersion: Value[String], 17 | ivyConfiguration: IvyOptions, 18 | app: Application, 19 | boot: BootSetup, 20 | logging: Logging, 21 | appProperties: List[AppProperty], 22 | serverConfig: Option[ServerConfiguration] 23 | ): 24 | def isServer: Boolean = serverConfig.isDefined 25 | def getScalaVersion = 26 | val sv = Value.get(scalaVersion) 27 | if sv == "auto" then None else Some(sv) 28 | 29 | def withScalaVersion(newScalaVersion: String) = 30 | LaunchConfiguration( 31 | Value.Explicit(newScalaVersion), 32 | ivyConfiguration, 33 | app, 34 | boot, 35 | logging, 36 | appProperties, 37 | serverConfig 38 | ) 39 | def withApp(app: Application) = 40 | LaunchConfiguration( 41 | scalaVersion, 42 | ivyConfiguration, 43 | app, 44 | boot, 45 | logging, 46 | appProperties, 47 | serverConfig 48 | ) 49 | def withAppVersion(newAppVersion: String) = 50 | LaunchConfiguration( 51 | scalaVersion, 52 | ivyConfiguration, 53 | app.withVersion(Value.Explicit(newAppVersion)), 54 | boot, 55 | logging, 56 | appProperties, 57 | serverConfig 58 | ) 59 | // TODO: withExplicit 60 | def withNameAndVersions( 61 | newScalaVersion: String, 62 | newAppVersion: String, 63 | newAppName: String, 64 | classifiers0: Classifiers 65 | ) = 66 | LaunchConfiguration( 67 | Value.Explicit(newScalaVersion), 68 | ivyConfiguration.copy(classifiers = classifiers0), 69 | app.withVersion(Value.Explicit(newAppVersion)).withName(Value.Explicit(newAppName)), 70 | boot, 71 | logging, 72 | appProperties, 73 | serverConfig 74 | ) 75 | 76 | def map(f: File => File) = 77 | LaunchConfiguration( 78 | scalaVersion, 79 | ivyConfiguration.map(f), 80 | app.map(f), 81 | boot.map(f), 82 | logging, 83 | appProperties, 84 | serverConfig.map(_.map(f)) 85 | ) 86 | object LaunchConfiguration: 87 | // Saves a launch configuration into a file. This is only safe if it is loaded by the *same* launcher version. 88 | def save(config: LaunchConfiguration, f: File): Unit = 89 | val out = new java.io.ObjectOutputStream(new java.io.FileOutputStream(f)) 90 | try out.writeObject(config) 91 | finally out.close() 92 | // Restores a launch configuration from a file. This is only safe if it is loaded by the *same* launcher version. 93 | def restore(url: URL): LaunchConfiguration = 94 | val in = new java.io.ObjectInputStream(url.openConnection.getInputStream) 95 | try in.readObject.asInstanceOf[LaunchConfiguration] 96 | finally in.close() 97 | final case class ServerConfiguration( 98 | lockFile: File, 99 | jvmArgs: Option[File], 100 | jvmPropsFile: Option[File] 101 | ): 102 | def map(f: File => File) = 103 | ServerConfiguration(f(lockFile), jvmArgs map f, jvmPropsFile map f) 104 | final case class IvyOptions( 105 | ivyHome: Option[File], 106 | classifiers: Classifiers, 107 | repositories: List[Repository.Repository], 108 | checksums: List[String], 109 | isOverrideRepositories: Boolean 110 | ): 111 | def map(f: File => File) = 112 | IvyOptions(ivyHome.map(f), classifiers, repositories, checksums, isOverrideRepositories) 113 | 114 | enum Value[A1]: 115 | case Explicit(value: A1) extends Value[A1] 116 | case Implicit(name: String, default: Option[A1]) extends Value[A1] 117 | override def toString: String = this match 118 | case Explicit(value) => value.toString() 119 | case Implicit(name, default) => 120 | name + (default match 121 | case Some(d) => "[" + d + "]" 122 | case None => "") 123 | 124 | object Value: 125 | def get[A1](v: Value[A1]): A1 = 126 | v match 127 | case e: Explicit[A1] => e.value; 128 | case _ => throw new BootException("unresolved version: " + v) 129 | def readImplied[A1](s: String, name: String, default: Option[String])(using 130 | read: String => A1 131 | ): Value[A1] = 132 | if s == "read" then Value.Implicit(name, default map read) 133 | else Pre.error("expected 'read', got '" + s + "'") 134 | end Value 135 | 136 | final case class Classifiers(forScala: Value[List[String]], app: Value[List[String]]) 137 | 138 | object Classifiers: 139 | def apply(forScala: List[String], app: List[String]): Classifiers = 140 | Classifiers(Value.Explicit(forScala), Value.Explicit(app)) 141 | 142 | object LaunchCrossVersion: 143 | def apply(s: String): xsbti.CrossValue = 144 | s match 145 | case _ if CrossVersionUtil.isFull(s) => xsbti.CrossValue.Full 146 | case _ if CrossVersionUtil.isBinary(s) => xsbti.CrossValue.Binary 147 | case _ if CrossVersionUtil.isDisabled(s) => xsbti.CrossValue.Disabled 148 | case x => Pre.error("unknown value '" + x + "' for property 'cross-versioned'") 149 | 150 | final case class Application( 151 | groupID: String, 152 | name: Value[String], 153 | version: Value[String], 154 | main: String, 155 | components: List[String], 156 | crossVersioned: xsbti.CrossValue, 157 | classpathExtra: Array[File] 158 | ): 159 | def getName = Value.get(name) 160 | def withName(newName: Value[String]) = 161 | Application(groupID, newName, version, main, components, crossVersioned, classpathExtra) 162 | def getVersion = Value.get(version) 163 | def withVersion(newVersion: Value[String]) = 164 | Application(groupID, name, newVersion, main, components, crossVersioned, classpathExtra) 165 | def toID = 166 | AppID(groupID, getName, getVersion, main, toArray(components), crossVersioned, classpathExtra) 167 | def map(f: File => File) = 168 | Application(groupID, name, version, main, components, crossVersioned, classpathExtra.map(f)) 169 | final case class AppID( 170 | groupID: String, 171 | name: String, 172 | version: String, 173 | mainClass: String, 174 | mainComponents: Array[String], 175 | crossVersionedValue: xsbti.CrossValue, 176 | classpathExtra: Array[File] 177 | ) extends xsbti.ApplicationID: 178 | def crossVersioned: Boolean = crossVersionedValue != xsbti.CrossValue.Disabled 179 | 180 | object Application: 181 | def apply(id: xsbti.ApplicationID): Application = 182 | import id.* 183 | Application( 184 | groupID, 185 | Value.Explicit(name), 186 | Value.Explicit(version), 187 | mainClass, 188 | mainComponents.toList, 189 | safeCrossVersionedValue(id), 190 | classpathExtra 191 | ) 192 | 193 | @nowarn 194 | private def safeCrossVersionedValue(id: xsbti.ApplicationID): xsbti.CrossValue = 195 | try id.crossVersionedValue 196 | catch 197 | case _: AbstractMethodError => 198 | // Before 0.13 this method did not exist on application, so we need to provide a default value 199 | // in the event we're dealing with an older Application. 200 | if id.crossVersioned then xsbti.CrossValue.Binary 201 | else xsbti.CrossValue.Disabled 202 | 203 | object Repository: 204 | trait Repository extends xsbti.Repository: 205 | def bootOnly: Boolean 206 | final case class Maven( 207 | id: String, 208 | url: URL, 209 | bootOnly: Boolean = false, 210 | allowInsecureProtocol: Boolean = false 211 | ) extends xsbti.MavenRepository 212 | with Repository 213 | final case class Ivy( 214 | id: String, 215 | url: URL, 216 | ivyPattern: String, 217 | artifactPattern: String, 218 | mavenCompatible: Boolean, 219 | bootOnly: Boolean = false, 220 | descriptorOptional: Boolean = false, 221 | skipConsistencyCheck: Boolean = false, 222 | allowInsecureProtocol: Boolean = false 223 | ) extends xsbti.IvyRepository 224 | with Repository 225 | final case class Predefined(id: xsbti.Predefined, bootOnly: Boolean = false) 226 | extends xsbti.PredefinedRepository 227 | with Repository 228 | object Predefined: 229 | def apply(s: String): Predefined = new Predefined(xsbti.Predefined.toValue(s), false) 230 | def apply(s: String, bootOnly: Boolean): Predefined = 231 | new Predefined(xsbti.Predefined.toValue(s), bootOnly) 232 | 233 | def isMavenLocal(repo: xsbti.Repository) = repo match 234 | case p: xsbti.PredefinedRepository => p.id == xsbti.Predefined.MavenLocal; 235 | case _ => false 236 | def defaults: List[xsbti.Repository] = 237 | xsbti.Predefined.values.map(x => Predefined(x, false)).toList 238 | 239 | final case class Search(tpe: Search.Value, paths: List[File]) 240 | object Search extends Enumeration: 241 | def none = Search(Current, Nil) 242 | val Only = value("only") 243 | val RootFirst = value("root-first") 244 | val Nearest = value("nearest") 245 | val Current = value("none") 246 | def apply(s: String, paths: List[File]): Search = Search(toValue(s), paths) 247 | 248 | final case class BootSetup( 249 | directory: File, 250 | lock: Boolean, 251 | properties: File, 252 | search: Search, 253 | promptCreate: String, 254 | enableQuick: Boolean, 255 | promptFill: Boolean 256 | ): 257 | def map(f: File => File) = 258 | BootSetup(f(directory), lock, f(properties), search, promptCreate, enableQuick, promptFill) 259 | final case class AppProperty(name: String)( 260 | val quick: Option[PropertyInit], 261 | val create: Option[PropertyInit], 262 | val fill: Option[PropertyInit] 263 | ) 264 | 265 | enum PropertyInit: 266 | case SetProperty(val value: String) 267 | case PromptProperty(val label: String, val default: Option[String]) 268 | 269 | final class Logging(level: LogLevel.Value) extends Serializable: 270 | def log(s: => String, at: LogLevel.Value) = 271 | if level.id <= at.id then stream(at).println("[" + at + "] " + s) 272 | def debug(s: => String) = log(s, LogLevel.Debug) 273 | private def stream(at: LogLevel.Value) = if at == LogLevel.Error then System.err else System.out 274 | object LogLevel extends Enumeration: 275 | val Debug = value("debug", 0) 276 | val Info = value("info", 1) 277 | val Warn = value("warn", 2) 278 | val Error = value("error", 3) 279 | def apply(s: String): Logging = new Logging(toValue(s)) 280 | 281 | final class AppConfiguration( 282 | val arguments: Array[String], 283 | val baseDirectory: File, 284 | val provider: xsbti.AppProvider 285 | ) extends xsbti.AppConfiguration 286 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /licenses/LICENSE_Apache: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/CoursierUpdate.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import coursier.* 10 | import coursier.cache.{ CacheDefaults, FileCache } 11 | import coursier.core.{ Publication, Repository } 12 | import coursier.credentials.DirectCredentials 13 | import coursier.ivy.IvyRepository 14 | import coursier.maven.MavenRepository 15 | import coursier.params.ResolutionParams 16 | import java.io.{ File, FileWriter, PrintWriter } 17 | import java.nio.file.{ Files, StandardCopyOption } 18 | import java.util.Properties 19 | import java.util.regex.Pattern 20 | import BootConfiguration.* 21 | import UpdateTarget.* 22 | import scala.annotation.nowarn 23 | 24 | class CousierUpdate(config: UpdateConfiguration): 25 | import config.{ bootDirectory, getScalaVersion, resolutionCacheBase, scalaVersion, scalaOrg } 26 | 27 | private def logFile = new File(bootDirectory, UpdateLogName) 28 | private val logWriter = new PrintWriter(new FileWriter(logFile)) 29 | 30 | private def defaultCacheLocation: File = 31 | def absoluteFile(path: String): File = new File(path).getAbsoluteFile() 32 | def windowsCacheDirectory: File = 33 | // Per discussion in https://github.com/dirs-dev/directories-jvm/issues/43, 34 | // LOCALAPPDATA environment variable may NOT represent the one-true 35 | // Known Folders API (https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid) 36 | // in case the user happened to have set the LOCALAPPDATA environmental variable. 37 | // Given that there's no reliable way of accessing this API from JVM, I think it's actually 38 | // better to use the LOCALAPPDATA as the first place to look. 39 | // When it is not found, it will fall back to $HOME/AppData/Local. 40 | // For the purpose of picking the Coursier cache directory, it's better to be 41 | // fast, reliable, and predictable rather than strict adherence to Microsoft. 42 | val base = 43 | sys.env 44 | .get("LOCALAPPDATA") 45 | .map(absoluteFile) 46 | .getOrElse(new File(new File(absoluteFile(sys.props("user.home")), "AppData"), "Local")) 47 | new File(new File(new File(base, "Coursier"), "Cache"), "v1") 48 | sys.props 49 | .get("sbt.coursier.home") 50 | .map(home => new File(absoluteFile(home), "cache")) 51 | .orElse(sys.env.get("COURSIER_CACHE").map(absoluteFile)) 52 | .orElse(sys.props.get("coursier.cache").map(absoluteFile)) match 53 | case Some(dir) => dir 54 | case _ => 55 | if isWindows then windowsCacheDirectory 56 | else CacheDefaults.location 57 | private lazy val coursierCache = 58 | import coursier.util.Task 59 | val credentials = bootCredentials 60 | val cache = credentials.foldLeft(FileCache(defaultCacheLocation)(using Task.sync)) { 61 | _.addCredentials(_) 62 | } 63 | cache 64 | 65 | def apply(target: UpdateTarget, reason: String): UpdateResult = 66 | try update(target, reason) 67 | catch 68 | case e: Throwable => 69 | e.printStackTrace(logWriter) 70 | log("[error] [launcher] " + e.toString) 71 | new UpdateResult(false, None, None) 72 | finally delete(resolutionCacheBase) 73 | 74 | private def update(target: UpdateTarget, reason: String): UpdateResult = 75 | val deps = target match 76 | case u: UpdateScala => 77 | val scalaVersion = getScalaVersion 78 | val scalaOrgString = if scalaOrg != ScalaOrg then scalaOrg + " " else "" 79 | Console.err.println( 80 | s"[info] [launcher] getting ${scalaOrgString}Scala $scalaVersion ${reason}..." 81 | ) 82 | scalaVersion match 83 | case sv if sv.startsWith("2.") => 84 | withPublication( 85 | Dependency( 86 | Module(Organization(scalaOrg), ModuleName(CompilerModuleName)), 87 | scalaVersion 88 | ), 89 | u.classifiers 90 | ) ::: 91 | withPublication( 92 | Dependency( 93 | Module(Organization(scalaOrg), ModuleName(LibraryModuleName)), 94 | scalaVersion 95 | ), 96 | u.classifiers 97 | ) 98 | case sv if sv.startsWith("3.") => 99 | withPublication( 100 | Dependency( 101 | Module(Organization(scalaOrg), ModuleName(Compiler3ModuleName)), 102 | scalaVersion 103 | ), 104 | u.classifiers 105 | ) ::: 106 | withPublication( 107 | Dependency( 108 | Module(Organization(scalaOrg), ModuleName(Library3ModuleName)), 109 | scalaVersion 110 | ), 111 | u.classifiers 112 | ) 113 | case _ => 114 | sys.error("unsupported Scala version " + scalaVersion) 115 | case u: UpdateApp => 116 | val app = u.id 117 | val resolvedName = (app.crossVersioned, scalaVersion) match 118 | case (xsbti.CrossValue.Full, Some(sv)) => app.getName + "_" + sv 119 | case (xsbti.CrossValue.Binary, Some(sv)) => 120 | app.getName + "_" + CrossVersionUtil.binaryScalaVersion(sv) 121 | case _ => app.getName 122 | Console.err.println( 123 | s"[info] [launcher] getting ${app.groupID} $resolvedName ${app.getVersion} $reason (this may take some time)..." 124 | ) 125 | withPublication( 126 | Dependency( 127 | Module(Organization(app.groupID), ModuleName(resolvedName)), 128 | app.getVersion 129 | ), 130 | u.classifiers 131 | ) ::: 132 | (scalaVersion match 133 | case Some(sv) if sv.startsWith("3.") => 134 | withPublication( 135 | Dependency( 136 | Module(Organization(scalaOrg), ModuleName(Library3ModuleName)), 137 | sv 138 | ), 139 | u.classifiers 140 | ) 141 | case Some(sv) if sv.startsWith("2.") => 142 | withPublication( 143 | Dependency( 144 | Module(Organization(scalaOrg), ModuleName(LibraryModuleName)), 145 | sv 146 | ), 147 | u.classifiers 148 | ) 149 | case _ => Nil) 150 | update(target, deps) 151 | 152 | private def detectScalaVersion(dependencySet: Set[Dependency]): Option[String] = 153 | def detectScalaVersion3: Option[String] = 154 | (dependencySet collect { 155 | case d: Dependency 156 | if d.module == Module(Organization(scalaOrg), ModuleName(Library3ModuleName)) => 157 | d.version 158 | }).headOption 159 | def detectScalaVersion2: Option[String] = 160 | (dependencySet collect { 161 | case d: Dependency 162 | if d.module == Module(Organization(scalaOrg), ModuleName(LibraryModuleName)) => 163 | d.version 164 | }).headOption 165 | detectScalaVersion3.orElse(detectScalaVersion2) 166 | 167 | /** Runs the resolve and retrieve for the given moduleID, which has had its dependencies added already. */ 168 | private def update( 169 | target: UpdateTarget, 170 | deps: List[Dependency] 171 | ): UpdateResult = 172 | val repos = config.repositories.flatMap(toCoursierRepository) 173 | val params = scalaVersion match 174 | case Some(sv) if sv != "auto" => 175 | ResolutionParams() 176 | .withScalaVersion(sv) 177 | .withForceScalaVersion(true) 178 | case _ => 179 | detectScalaVersion(deps.toSet) match 180 | case Some(sv) => 181 | ResolutionParams() 182 | .withScalaVersion(sv) 183 | .withForceScalaVersion(true) 184 | case _ => 185 | ResolutionParams() 186 | val r: Resolution = Resolve() 187 | .withCache(coursierCache) 188 | .addDependencies(deps*) 189 | .withRepositories(repos) 190 | .withResolutionParams(params) 191 | .run() 192 | val actualScalaVersion = detectScalaVersion(r.dependencySet.set) 193 | val retrieveDir = target match 194 | case _: UpdateScala => 195 | new File(new File(bootDirectory, baseDirectoryName(scalaOrg, scalaVersion)), "lib") 196 | case u: UpdateApp => 197 | new File( 198 | new File(bootDirectory, baseDirectoryName(scalaOrg, actualScalaVersion)), 199 | appDirectoryName(u.id.toID, File.separator) 200 | ) 201 | val isScala = target match 202 | case _: UpdateScala => true 203 | case _: UpdateApp => false 204 | val depVersion: Option[String] = target match 205 | case _: UpdateScala => scalaVersion 206 | case u: UpdateApp => Some(Value.get(u.id.version)) 207 | val isSbt = target match 208 | case _: UpdateScala => false 209 | case u: UpdateApp => Some(Value.get(u.id.name)) == Some("sbt") 210 | val isZeroDot = depVersion match 211 | case Some(v) => v.startsWith("0.") 212 | case _ => false 213 | if !retrieveDir.exists then Files.createDirectories(retrieveDir.toPath) 214 | val downloadedJars = Fetch() 215 | .withCache(coursierCache) 216 | .addDependencies(deps*) 217 | .withRepositories(repos) 218 | .withResolutionParams(params) 219 | .run() 220 | downloadedJars foreach { downloaded => 221 | val t = 222 | if isScala then 223 | val name = downloaded.getName match 224 | case n if n.startsWith("scala-compiler") => "scala-compiler.jar" 225 | case n if n.startsWith("scala-library") => "scala-library.jar" 226 | case n if n.startsWith("scala-reflect") => "scala-reflect.jar" 227 | case n => n 228 | new File(retrieveDir, name) 229 | else if isSbt && downloaded.getName == "interface.jar" && isZeroDot then 230 | val componentDir = new File(retrieveDir, "xsbti") 231 | Files.createDirectories(componentDir.toPath) 232 | new File(componentDir, downloaded.getName) 233 | else 234 | val name = downloaded.getName match 235 | // https://github.com/sbt/sbt/issues/6432 236 | // sbt expects test-interface JAR to be called test-interface-1.0.jar with 237 | // version number, but sometimes it doesn't have it. 238 | case "test-interface.jar" => 239 | if Pattern.matches("""[0-9.]+""", downloaded.getParentFile.getName) then 240 | "test-interface-" + downloaded.getParentFile.getName + ".jar" 241 | else "test-interface-0.0.jar" 242 | case n => n 243 | new File(retrieveDir, name) 244 | val isSkip = 245 | isScala && (downloaded.getName match 246 | case n if n.startsWith("compiler-interface") => true 247 | case n if n.startsWith("util-interface") => true 248 | case _ => false) 249 | if isSkip then () 250 | else 251 | Files.copy(downloaded.toPath, t.toPath, StandardCopyOption.REPLACE_EXISTING) 252 | () 253 | } 254 | new UpdateResult(true, actualScalaVersion, depVersion) 255 | 256 | def withPublication(d: Dependency, classifiers: List[String]): List[Dependency] = 257 | if classifiers.isEmpty then List(d) 258 | else classifiers.map(c => d.withPublication(Publication.empty.withClassifier(Classifier(c)))) 259 | 260 | def bootCredentials = 261 | val optionProps = 262 | Option(System.getProperty("sbt.boot.credentials")) orElse 263 | Option(System.getenv("SBT_CREDENTIALS")) map (path => 264 | Pre.readProperties(new File(substituteTilde(path))) 265 | ) 266 | def extractCredentials( 267 | keys: (String, String, String, String) 268 | )(props: Properties): Option[DirectCredentials] = 269 | val List(realm, host, user, password) = 270 | keys.productIterator.map(key => props.getProperty(key.toString)).toList 271 | if host != null && user != null && password != null then 272 | Some( 273 | DirectCredentials() 274 | .withHost(host) 275 | .withUsername(user) 276 | .withPassword(password) 277 | .withRealm(Option(realm).filter(_.nonEmpty)) 278 | .withHttpsOnly(false) 279 | .withMatchHost(true) 280 | ) 281 | else None 282 | (optionProps match 283 | case Some(props) => extractCredentials(("realm", "host", "user", "password"))(props) 284 | case None => None 285 | ).toList ::: 286 | (extractCredentials( 287 | ("sbt.boot.realm", "sbt.boot.host", "sbt.boot.user", "sbt.boot.password") 288 | )( 289 | System.getProperties 290 | )).toList 291 | 292 | @nowarn 293 | def toCoursierRepository(repo: xsbti.Repository): Seq[Repository] = 294 | import xsbti.Predefined.* 295 | repo match 296 | case m: xsbti.MavenRepository => 297 | mavenRepository(m.url.toString) :: Nil 298 | case i: xsbti.IvyRepository => 299 | ivyRepository( 300 | i.id, 301 | i.url.toString, 302 | i.ivyPattern, 303 | i.artifactPattern, 304 | i.mavenCompatible, 305 | i.descriptorOptional, 306 | i.skipConsistencyCheck, 307 | i.allowInsecureProtocol 308 | ) :: Nil 309 | case p: xsbti.PredefinedRepository => 310 | p.id match 311 | case Local => 312 | localRepository :: Nil 313 | case MavenLocal => 314 | val localDir = new File(new File(new File(sys.props("user.home")), ".m2"), "repository") 315 | mavenRepository(localDir.toPath.toUri.toString) :: Nil 316 | case MavenCentral => 317 | Repositories.central :: Nil 318 | case SonatypeOSSReleases => 319 | Repositories.sonatype("releases") :: Nil 320 | case SonatypeOSSSnapshots => 321 | Repositories.sonatype("snapshots") :: Nil 322 | case Jcenter | ScalaToolsReleases | ScalaToolsSnapshots => 323 | log( 324 | s"[warn] [launcher] ${p.id} is deprecated or no longer available; remove from repositories" 325 | ) 326 | Nil 327 | 328 | private def mavenRepository(root0: String): MavenRepository = 329 | val root = if root0.endsWith("/") then root0 else root0 + "/" 330 | MavenRepository(root) 331 | 332 | /** Uses the pattern defined in BuildConfiguration to download sbt from Google code. */ 333 | @nowarn 334 | private def ivyRepository( 335 | id: String, 336 | base: String, 337 | ivyPattern: String, 338 | artifactPattern: String, 339 | mavenCompatible: Boolean, 340 | descriptorOptional: Boolean, 341 | skipConsistencyCheck: Boolean, 342 | allowInsecureProtocol: Boolean 343 | ): IvyRepository = 344 | IvyRepository 345 | .parse( 346 | base + artifactPattern, 347 | Some(base + ivyPattern), 348 | ) 349 | .toOption 350 | .get 351 | 352 | private def localRepository: IvyRepository = 353 | val localDir = new File(new File(new File(sys.props("user.home")), ".ivy2"), "local") 354 | val root0 = localDir.toPath.toUri.toASCIIString 355 | val root = if root0.endsWith("/") then root0 else root0 + "/" 356 | IvyRepository 357 | .parse( 358 | root + LocalArtifactPattern, 359 | Some(root + LocalIvyPattern) 360 | ) 361 | .toOption 362 | .get 363 | 364 | /** Logs the given message to a file and to the console. */ 365 | private def log(msg: String) = 366 | try logWriter.println(msg) 367 | catch 368 | case e: Exception => 369 | Console.err.println("[error] [launcher] error writing to update log file: " + e.toString) 370 | Console.err.println(msg) 371 | -------------------------------------------------------------------------------- /launcher-implementation/src/main/scala/xsbt/boot/ConfigurationParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * sbt 3 | * Licensed under Apache License 2.0 (see LICENSE) 4 | */ 5 | 6 | package xsbt.boot 7 | 8 | import Pre.* 9 | import ConfigurationParser.* 10 | import java.io.{ BufferedReader, File, FileInputStream, InputStreamReader, Reader, StringReader } 11 | import java.net.{ MalformedURLException, URL } 12 | import java.util.regex.{ Matcher, Pattern } 13 | import Matcher.quoteReplacement 14 | import scala.collection.immutable.List 15 | import scala.annotation.nowarn 16 | 17 | object ConfigurationParser: 18 | def trim(s: Array[String]) = s.map(_.trim).toList 19 | def ids(value: String) = trim(substituteVariables(value).split(",")).filter(isNonEmpty) 20 | 21 | private lazy val VarPattern = Pattern.compile("""\$\{([\w.]+)(\-(.*))?\}""") 22 | def substituteVariables(s: String): String = 23 | if s.indexOf('$') >= 0 then substituteVariables0(s) else s 24 | // scala.util.Regex brought in 30kB, so we code it explicitly 25 | def substituteVariables0(s: String): String = 26 | val m = VarPattern.matcher(s) 27 | val b = new StringBuffer 28 | while m.find() do 29 | val key = m.group(1) 30 | val defined = System.getProperty(key) 31 | val value = 32 | if defined ne null then defined 33 | else 34 | val default = m.group(3) 35 | if default eq null then m.group() else substituteVariables(default) 36 | m.appendReplacement(b, quoteReplacement(value)) 37 | m.appendTail(b) 38 | b.toString 39 | 40 | implicit val readIDs: String => List[String] = ids(_) 41 | class ConfigurationParser: 42 | def apply(file: File): LaunchConfiguration = Using(newReader(file))(apply) 43 | def apply(s: String): LaunchConfiguration = Using(new StringReader(s))(apply) 44 | def apply(reader: Reader): LaunchConfiguration = Using(new BufferedReader(reader))(apply) 45 | private def apply(in: BufferedReader): LaunchConfiguration = 46 | processSections(processLines(readLine(in, Nil, 0))) 47 | private final def readLine(in: BufferedReader, accum: List[Line], index: Int): List[Line] = 48 | in.readLine match 49 | case null => accum.reverse 50 | case line => readLine(in, ParseLine(line, index) ::: accum, index + 1) 51 | private def newReader(file: File) = new InputStreamReader(new FileInputStream(file), "UTF-8") 52 | def readRepositoriesConfig(file: File): List[Repository.Repository] = 53 | Using(newReader(file))(readRepositoriesConfig) 54 | def readRepositoriesConfig(reader: Reader): List[Repository.Repository] = 55 | Using(new BufferedReader(reader))(readRepositoriesConfig) 56 | def readRepositoriesConfig(s: String): List[Repository.Repository] = 57 | Using(new StringReader(s))(readRepositoriesConfig) 58 | private def readRepositoriesConfig(in: BufferedReader): List[Repository.Repository] = 59 | processRepositoriesConfig(processLines(readLine(in, Nil, 0))) 60 | def processRepositoriesConfig(sections: SectionMap): List[Repository.Repository] = 61 | processSection(sections, "repositories", getRepositories)._1 62 | // section -> configuration instance processing 63 | def processSections(sections: SectionMap): LaunchConfiguration = 64 | val ((scalaVersion, scalaClassifiers), m1) = processSection(sections, "scala", getScala) 65 | val ((app, appClassifiers), m2) = processSection(m1, "app", getApplication) 66 | val (defaultRepositories, m3) = processSection(m2, "repositories", getRepositories) 67 | val (boot, m4) = processSection(m3, "boot", getBoot) 68 | val (logging, m5) = processSection(m4, "log", getLogging) 69 | val (properties, m6) = processSection(m5, "app-properties", getAppProperties) 70 | val ((ivyHome, checksums, isOverrideRepos, rConfigFile), m7) = processSection(m6, "ivy", getIvy) 71 | val (serverOptions, m8) = processSection(m7, "server", getServer) 72 | check(m8, "section") 73 | val classifiers = Classifiers(scalaClassifiers, appClassifiers) 74 | val repositories = rConfigFile map readRepositoriesConfig getOrElse defaultRepositories 75 | val ivyOptions = IvyOptions(ivyHome, classifiers, repositories, checksums, isOverrideRepos) 76 | 77 | // TODO - Read server properties... 78 | new LaunchConfiguration(scalaVersion, ivyOptions, app, boot, logging, properties, serverOptions) 79 | def getScala(m: LabelMap) = 80 | val (scalaVersion, m1) = getVersion(m, "Scala version", "scala.version") 81 | val (scalaClassifiers, m2) = getClassifiers(m1, "Scala classifiers") 82 | check(m2, "label") 83 | (scalaVersion, scalaClassifiers) 84 | def getClassifiers(m: LabelMap, label: String): (Value[List[String]], LabelMap) = 85 | process(m, "classifiers", processClassifiers(label)) 86 | def processClassifiers(label: String)(value: Option[String]): Value[List[String]] = 87 | value.map(readValue[List[String]](label)) getOrElse Value.Explicit(Nil) 88 | 89 | def getVersion(m: LabelMap, label: String, defaultName: String): (Value[String], LabelMap) = 90 | process(m, "version", processVersion(label, defaultName)) 91 | def processVersion(label: String, defaultName: String)(value: Option[String]): Value[String] = 92 | value.map(readValue[String](label)).getOrElse(Value.Implicit(defaultName, None)) 93 | 94 | def getName( 95 | m: LabelMap, 96 | label: String, 97 | defaultName: String, 98 | defaultValue: String 99 | ): (Value[String], LabelMap) = 100 | process(m, "name", processName(label, defaultName, defaultValue)) 101 | def processName(label: String, defaultName: String, defaultValue: String)( 102 | value: Option[String] 103 | ): Value[String] = 104 | value.map(readValue[String](label)).getOrElse(Value.Implicit(defaultName, Some(defaultValue))) 105 | 106 | def readValue[T](label: String)(implicit read: String => T): String => Value[T] = value0 => 107 | val value = substituteVariables(value0) 108 | if isEmpty(value) then 109 | Pre.error(label + " cannot be empty (omit declaration to use the default)") 110 | try parsePropertyValue(label, value)(Value.readImplied[T]) 111 | catch case _: BootException => Value.Explicit(read(value)) 112 | 113 | @nowarn 114 | def processSection[T](sections: SectionMap, name: String, f: LabelMap => T) = 115 | process[String, LabelMap, T](sections, name, m => f(m.default(x => None))) 116 | 117 | def process[K, V, T](sections: ListMap[K, V], name: K, f: V => T): (T, ListMap[K, V]) = 118 | (f(sections(name)), sections - name) 119 | def check(map: ListMap[String, ?], label: String): Unit = 120 | if map.isEmpty then () else Pre.error(map.keys.mkString("Invalid " + label + "(s): ", ",", "")) 121 | def check[T](label: String, pair: (T, ListMap[String, ?])): T = 122 | check(pair._2, label); pair._1 123 | def id(map: LabelMap, name: String, default: String): (String, LabelMap) = 124 | (substituteVariables(orElse(getOrNone(map, name), default)), map - name) 125 | def getOrNone[K, V](map: ListMap[K, Option[V]], k: K) = orElse(map.get(k), None) 126 | def ids(map: LabelMap, name: String, default: List[String]) = 127 | val result = map(name) map ConfigurationParser.ids 128 | (orElse(result, default), map - name) 129 | def bool(map: LabelMap, name: String, default: Boolean): (Boolean, LabelMap) = 130 | val (b, m) = id(map, name, default.toString) 131 | (toBoolean(b), m) 132 | 133 | def toFiles(paths: List[String]): List[File] = paths.map(toFile) 134 | def toFile(path: String): File = 135 | new File( 136 | substituteVariables(path).replace('/', File.separatorChar) 137 | ) // if the path is relative, it will be resolved by Launch later 138 | def file(map: LabelMap, name: String, default: File): (File, LabelMap) = 139 | (orElse(getOrNone(map, name).map(toFile), default), map - name) 140 | def optfile(map: LabelMap, name: String): (Option[File], LabelMap) = 141 | (getOrNone(map, name).map(toFile), map - name) 142 | def getIvy(m: LabelMap): (Option[File], List[String], Boolean, Option[File]) = 143 | val (ivyHome, m1) = optfile(m, "ivy-home") 144 | val (checksums, m2) = ids(m1, "checksums", BootConfiguration.DefaultChecksums) 145 | val (overrideRepos, m3) = bool(m2, "override-build-repos", false) 146 | val (repoConfig, m4) = optfile(m3, "repository-config") 147 | check(m4, "label") 148 | (ivyHome, checksums, overrideRepos, repoConfig filter (_.exists)) 149 | def getBoot(m: LabelMap): BootSetup = 150 | val (dir, m1) = file(m, "directory", toFile("project/boot")) 151 | val (props, m2) = file(m1, "properties", toFile("project/build.properties")) 152 | val (search, m3) = getSearch(m2, props) 153 | val (enableQuick, m4) = bool(m3, "quick-option", false) 154 | val (promptFill, m5) = bool(m4, "prompt-fill", false) 155 | val (promptCreate, m6) = id(m5, "prompt-create", "") 156 | val (lock, m7) = bool(m6, "lock", true) 157 | check(m7, "label") 158 | BootSetup(dir, lock, props, search, promptCreate, enableQuick, promptFill) 159 | def getLogging(m: LabelMap): Logging = check("label", process(m, "level", getLevel)) 160 | def getLevel(m: Option[String]) = m.map(LogLevel.apply).getOrElse(new Logging(LogLevel.Info)) 161 | def getSearch(m: LabelMap, defaultPath: File): (Search, LabelMap) = 162 | ids(m, "search", Nil) match 163 | case (Nil, newM) => (Search.none, newM) 164 | case (tpe :: Nil, newM) => (Search(tpe, List(defaultPath)), newM) 165 | case (tpe :: paths, newM) => (Search(tpe, toFiles(paths)), newM) 166 | 167 | def getApplication(m: LabelMap): (Application, Value[List[String]]) = 168 | val (org, m1) = id(m, "org", BootConfiguration.SbtOrg) 169 | val (name, m2) = id(m1, "name", "sbt") 170 | val (appName, _) = getName(m1, name + " name", name + ".name", name) 171 | val (rev, m3) = getVersion(m2, name + " version", name + ".version") 172 | val (main, m4) = id(m3, "class", "xsbt.Main") 173 | val (components, m5) = ids(m4, "components", List("default")) 174 | val (crossVersioned, m6) = id(m5, "cross-versioned", CrossVersionUtil.binaryString) 175 | val (resources, m7) = ids(m6, "resources", Nil) 176 | val (classifiers, m8) = getClassifiers(m7, "Application classifiers") 177 | check(m8, "label") 178 | val classpathExtra = toArray(toFiles(resources)) 179 | val app = new Application( 180 | org, 181 | appName, 182 | rev, 183 | main, 184 | components, 185 | LaunchCrossVersion(crossVersioned), 186 | classpathExtra 187 | ) 188 | (app, classifiers) 189 | def getServer(m: LabelMap): (Option[ServerConfiguration]) = 190 | val (lock, m1) = optfile(m, "lock") 191 | // TODO - JVM args 192 | val (args, m2) = optfile(m1, "jvmargs") 193 | val (props, _) = optfile(m2, "jvmprops") 194 | lock map { file => 195 | ServerConfiguration(file, args, props) 196 | } 197 | def getRepositories(m: LabelMap): List[Repository.Repository] = 198 | import Repository.{ Ivy, Maven, Predefined } 199 | val BootOnly = "bootOnly" 200 | val MvnComp = "mavenCompatible" 201 | val DescriptorOptional = "descriptorOptional" 202 | val DontCheckConsistency = "skipConsistencyCheck" 203 | val AllowInsecureProtocol = "allowInsecureProtocol" 204 | val OptSet = 205 | Set(BootOnly, MvnComp, DescriptorOptional, DontCheckConsistency, AllowInsecureProtocol) 206 | m.toList.map { 207 | case (key, None) => Predefined(key) 208 | case (key, Some(BootOnly)) => Predefined(key, true) 209 | case (key, Some(value)) => 210 | val r = trim(substituteVariables(value).split(",", 8)) 211 | val url = 212 | try new URL(r(0)) 213 | catch 214 | case e: MalformedURLException => 215 | Pre.error("invalid URL specified for '" + key + "': " + e.getMessage) 216 | val (optionPart, patterns) = r.tail.partition(OptSet.contains(_)) 217 | val options = ( 218 | optionPart.contains(BootOnly), 219 | optionPart.contains(MvnComp), 220 | optionPart.contains(DescriptorOptional), 221 | optionPart.contains(DontCheckConsistency), 222 | optionPart.contains(AllowInsecureProtocol) 223 | ) 224 | (patterns, options) match 225 | case (both :: Nil, (bo, mc, dso, cc, ip)) => 226 | Ivy( 227 | key, 228 | url, 229 | both, 230 | both, 231 | mavenCompatible = mc, 232 | bootOnly = bo, 233 | descriptorOptional = dso, 234 | skipConsistencyCheck = cc, 235 | allowInsecureProtocol = ip 236 | ) 237 | case (ivy :: art :: Nil, (bo, mc, dso, cc, ip)) => 238 | Ivy( 239 | key, 240 | url, 241 | ivy, 242 | art, 243 | mavenCompatible = mc, 244 | bootOnly = bo, 245 | descriptorOptional = dso, 246 | skipConsistencyCheck = cc, 247 | allowInsecureProtocol = ip 248 | ) 249 | case (Nil, (true, false, false, _, ip)) => 250 | Maven(key, url, bootOnly = true, allowInsecureProtocol = ip) 251 | case (Nil, (false, false, false, false, ip)) => 252 | Maven(key, url, allowInsecureProtocol = ip) 253 | case _ => 254 | Pre.error("could not parse %s: %s".format(key, value)) 255 | } 256 | def getAppProperties(m: LabelMap): List[AppProperty] = 257 | m.toList.flatMap: 258 | case (name, Some(value)) => 259 | val map = ListMap(trim(value.split(",")).map(parsePropertyDefinition(name))*) 260 | List(AppProperty(name)(map.get("quick"), map.get("new"), map.get("fill"))) 261 | case _ => Nil 262 | def parsePropertyDefinition(name: String)(value: String) = value.split("=", 2) match 263 | case Array(mode, value) => (mode, parsePropertyValue(name, value)(defineProperty(name))) 264 | case x => Pre.error("invalid property definition '" + x + "' for property '" + name + "'") 265 | def defineProperty( 266 | name: String 267 | )(action: String, requiredArg: String, optionalArg: Option[String]) = 268 | action match 269 | case "prompt" => PropertyInit.PromptProperty(requiredArg, optionalArg) 270 | case "set" => PropertyInit.SetProperty(requiredArg) 271 | case _ => Pre.error("unknown action '" + action + "' for property '" + name + "'") 272 | private lazy val propertyPattern = 273 | Pattern.compile("""(.+)\((.*)\)(?:\[(.*)\])?""") // examples: prompt(Version)[1.0] or set(1.0) 274 | def parsePropertyValue[T](name: String, definition: String)( 275 | f: (String, String, Option[String]) => T 276 | ): T = 277 | val m = propertyPattern.matcher(definition) 278 | if !m.matches() then 279 | Pre.error("invalid property definition '" + definition + "' for property '" + name + "'") 280 | val optionalArg = m.group(3) 281 | f(m.group(1), m.group(2), if optionalArg eq null then None else Some(optionalArg)) 282 | 283 | type LabelMap = ListMap[String, Option[String]] 284 | // section-name -> label -> value 285 | type SectionMap = ListMap[String, LabelMap] 286 | 287 | @nowarn 288 | def processLines(lines: List[Line]): SectionMap = 289 | type State = (SectionMap, Option[String]) 290 | val s: State = 291 | lines.foldLeft( 292 | (ListMap.empty.default(x => ListMap.empty[String, Option[String]]), None): State 293 | ) { 294 | case (x, Line.Comment) => x 295 | case ((map, _), s: Line.Section) => (map, Some(s.name)) 296 | case ((_, None), l: Line.Labeled) => Pre.error("label " + l.label + " is not in a section") 297 | case ((map, s @ Some(section)), l: Line.Labeled) => 298 | val sMap = map(section) 299 | if sMap.contains(l.label) then 300 | Pre.error("duplicate label '" + l.label + "' in section '" + section + "'") 301 | else (map(section) = (sMap(l.label) = l.value), s) 302 | } 303 | s._1 304 | 305 | enum Line: 306 | case Labeled(val label: String, val value: Option[String]) 307 | case Section(val name: String) 308 | case Comment 309 | 310 | class ParseException(val content: String, val line: Int, val col: Int, val msg: String) 311 | extends BootException( 312 | "[" + (line + 1) + ", " + (col + 1) + "]" + msg + "\n" + content + "\n" + List 313 | .fill(col)(" ") 314 | .mkString + "^" 315 | ) 316 | 317 | object ParseLine: 318 | def apply(content: String, line: Int) = 319 | def error(col: Int, msg: String) = throw new ParseException(content, line, col, msg) 320 | def check(condition: Boolean)(col: Int, msg: String) = if condition then () else error(col, msg) 321 | 322 | val trimmed = trimLeading(content) 323 | 324 | def section = 325 | val closing = trimmed.indexOf(']', 1) 326 | check(closing > 0)(content.length, "expected ']', found end of line") 327 | val extra = trimmed.substring(closing + 1) 328 | val trimmedExtra = trimLeading(extra) 329 | check(isEmpty(trimmedExtra))( 330 | content.length - trimmedExtra.length, 331 | "expected end of line, found '" + extra + "'" 332 | ) 333 | Line.Section(trimmed.substring(1, closing).trim) 334 | def labeled = 335 | trimmed.split(":", 2) match 336 | case Array(label, value) => 337 | val trimmedValue = value.trim 338 | check(isNonEmpty(trimmedValue))( 339 | content.indexOf(':'), 340 | "value for '" + label + "' was empty" 341 | ) 342 | Line.Labeled(label, Some(trimmedValue)) 343 | case x => Line.Labeled(x.mkString, None) 344 | 345 | if isEmpty(trimmed) then Nil 346 | else 347 | val processed = 348 | trimmed.charAt(0) match 349 | case '#' => Line.Comment 350 | case '[' => section 351 | case _ => labeled 352 | processed :: Nil 353 | --------------------------------------------------------------------------------