diff --git a/.gitignore b/.gitignore index dc974ae3b..495231475 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ target/ __pycache__ scripted-test/src/sbt-test/*/*/project/build.properties +scripted-test/src/sbt-test/*/*/project/plugins.sbt diff --git a/.travis.yml b/.travis.yml index 77ce91daf..be80e9798 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,7 @@ script: sbt -Dfile.encoding=UTF8 -J-XX:ReservedCodeCacheSize=256M whitesourceCheckPolicies test scriptedIvy + scriptedCoursier packagedArtifacts # ensure that all artifacts for publish package without failure env: diff --git a/build.sbt b/build.sbt index 0c50280ab..d90d2d0b4 100644 --- a/build.sbt +++ b/build.sbt @@ -12,6 +12,7 @@ def commonSettings: Seq[Setting[_]] = Def.settings( // publishArtifact in packageDoc := false, resolvers += Resolver.typesafeIvyRepo("releases"), resolvers += Resolver.sonatypeRepo("snapshots"), + resolvers += Resolver.sbtPluginRepo("releases"), resolvers += "bintray-sbt-maven-releases" at "https://dl.bintray.com/sbt/maven-releases/", // concurrentRestrictions in Global += Util.testExclusiveRestriction, testOptions += Tests.Argument(TestFrameworks.ScalaCheck, "-w", "1"), @@ -45,7 +46,7 @@ val mimaSettings = Def settings ( ) lazy val lmRoot = (project in file(".")) - .aggregate(lmCore, lmIvy) + .aggregate(lmCore, lmIvy, lmCoursier) .settings( inThisBuild( Seq( @@ -259,6 +260,26 @@ lazy val lmIvy = (project in file("ivy")) ), ) +lazy val lmCoursier = (project in file("coursier")) + .enablePlugins(ContrabandPlugin, JsonCodecPlugin) + .dependsOn(lmIvy) + .settings( + commonSettings, + crossScalaVersions := Seq(scala212, scala211), + name := "librarymanagement-coursier", + libraryDependencies ++= Seq(coursier, + coursierCache, + scalaTest % Test, + scalaCheck % Test + ), + managedSourceDirectories in Compile += + baseDirectory.value / "src" / "main" / "contraband-scala", + sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", + contrabandFormatsForType in generateContrabands in Compile := DatatypeConfig.getFormats, + scalacOptions in (Compile, console) --= + Vector("-Ywarn-unused-import", "-Ywarn-unused", "-Xlint") + ) + lazy val lmScriptedTest = (project in file("scripted-test")) .enablePlugins(SbtPlugin) .settings( @@ -276,7 +297,16 @@ addCommandAlias("scriptedIvy", Seq( "lmIvy/publishLocal", "lmScriptedTest/clean", """set ThisBuild / scriptedTestLMImpl := "ivy"""", - """set ThisBuild / scriptedLaunchOpts += "-Ddependency.resolution=set ThisBuild / dependencyResolution := sbt.librarymanagement.ivy.IvyDependencyResolution(ivyConfiguration.value)" """, + """set ThisBuild / scriptedLaunchOpts += "-Ddependency.resolution=ivy" """, + "lmScriptedTest/scripted").mkString(";",";","")) + +addCommandAlias("scriptedCoursier", Seq( + "lmCore/publishLocal", + "lmIvy/publishLocal", + "lmCoursier/publishLocal", + "lmScriptedTest/clean", + """set ThisBuild / scriptedTestLMImpl := "coursier"""", + """set ThisBuild / scriptedLaunchOpts += "-Ddependency.resolution=coursier" """, "lmScriptedTest/scripted").mkString(";",";","")) def customCommands: Seq[Setting[_]] = Seq( diff --git a/core/src/main/scala/sbt/librarymanagement/LibraryManagementInterface.scala b/core/src/main/scala/sbt/librarymanagement/LibraryManagementInterface.scala index 33c72c55f..5236a1a2a 100644 --- a/core/src/main/scala/sbt/librarymanagement/LibraryManagementInterface.scala +++ b/core/src/main/scala/sbt/librarymanagement/LibraryManagementInterface.scala @@ -66,7 +66,7 @@ trait PublisherInterface { } /** - * Decribes the representation of a module, inclding its dependencies + * Decribes the representation of a module, including its dependencies * and the version of Scala it uses, if any. */ trait ModuleDescriptor { diff --git a/coursier/src/main/contraband-scala/sbt/librarymanagement/coursier/CoursierConfiguration.scala b/coursier/src/main/contraband-scala/sbt/librarymanagement/coursier/CoursierConfiguration.scala new file mode 100644 index 000000000..1864c4440 --- /dev/null +++ b/coursier/src/main/contraband-scala/sbt/librarymanagement/coursier/CoursierConfiguration.scala @@ -0,0 +1,57 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.librarymanagement.coursier +final class CoursierConfiguration private ( + val log: Option[xsbti.Logger], + val resolvers: Vector[sbt.librarymanagement.Resolver], + val otherResolvers: Vector[sbt.librarymanagement.Resolver], + val reorderResolvers: Boolean, + val parallelDownloads: Int, + val maxIterations: Int) extends Serializable { + + private def this() = this(None, sbt.librarymanagement.Resolver.defaults, Vector.empty, true, 6, 100) + + override def equals(o: Any): Boolean = o match { + case x: CoursierConfiguration => (this.log == x.log) && (this.resolvers == x.resolvers) && (this.otherResolvers == x.otherResolvers) && (this.reorderResolvers == x.reorderResolvers) && (this.parallelDownloads == x.parallelDownloads) && (this.maxIterations == x.maxIterations) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.librarymanagement.coursier.CoursierConfiguration".##) + log.##) + resolvers.##) + otherResolvers.##) + reorderResolvers.##) + parallelDownloads.##) + maxIterations.##) + } + override def toString: String = { + "CoursierConfiguration(" + log + ", " + resolvers + ", " + otherResolvers + ", " + reorderResolvers + ", " + parallelDownloads + ", " + maxIterations + ")" + } + private[this] def copy(log: Option[xsbti.Logger] = log, resolvers: Vector[sbt.librarymanagement.Resolver] = resolvers, otherResolvers: Vector[sbt.librarymanagement.Resolver] = otherResolvers, reorderResolvers: Boolean = reorderResolvers, parallelDownloads: Int = parallelDownloads, maxIterations: Int = maxIterations): CoursierConfiguration = { + new CoursierConfiguration(log, resolvers, otherResolvers, reorderResolvers, parallelDownloads, maxIterations) + } + def withLog(log: Option[xsbti.Logger]): CoursierConfiguration = { + copy(log = log) + } + def withLog(log: xsbti.Logger): CoursierConfiguration = { + copy(log = Option(log)) + } + def withResolvers(resolvers: Vector[sbt.librarymanagement.Resolver]): CoursierConfiguration = { + copy(resolvers = resolvers) + } + def withOtherResolvers(otherResolvers: Vector[sbt.librarymanagement.Resolver]): CoursierConfiguration = { + copy(otherResolvers = otherResolvers) + } + def withReorderResolvers(reorderResolvers: Boolean): CoursierConfiguration = { + copy(reorderResolvers = reorderResolvers) + } + def withParallelDownloads(parallelDownloads: Int): CoursierConfiguration = { + copy(parallelDownloads = parallelDownloads) + } + def withMaxIterations(maxIterations: Int): CoursierConfiguration = { + copy(maxIterations = maxIterations) + } +} +object CoursierConfiguration { + + def apply(): CoursierConfiguration = new CoursierConfiguration() + def apply(log: Option[xsbti.Logger], resolvers: Vector[sbt.librarymanagement.Resolver], otherResolvers: Vector[sbt.librarymanagement.Resolver], reorderResolvers: Boolean, parallelDownloads: Int, maxIterations: Int): CoursierConfiguration = new CoursierConfiguration(log, resolvers, otherResolvers, reorderResolvers, parallelDownloads, maxIterations) + def apply(log: xsbti.Logger, resolvers: Vector[sbt.librarymanagement.Resolver], otherResolvers: Vector[sbt.librarymanagement.Resolver], reorderResolvers: Boolean, parallelDownloads: Int, maxIterations: Int): CoursierConfiguration = new CoursierConfiguration(Option(log), resolvers, otherResolvers, reorderResolvers, parallelDownloads, maxIterations) +} diff --git a/coursier/src/main/contraband-scala/sbt/librarymanagement/coursier/CoursierConfigurationFormats.scala b/coursier/src/main/contraband-scala/sbt/librarymanagement/coursier/CoursierConfigurationFormats.scala new file mode 100644 index 000000000..ff8713f87 --- /dev/null +++ b/coursier/src/main/contraband-scala/sbt/librarymanagement/coursier/CoursierConfigurationFormats.scala @@ -0,0 +1,37 @@ +/** + * This code is generated using [[http://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.librarymanagement.coursier +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait CoursierConfigurationFormats { self: sbt.internal.librarymanagement.formats.LoggerFormat with sbt.librarymanagement.ResolverFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val CoursierConfigurationFormat: JsonFormat[sbt.librarymanagement.coursier.CoursierConfiguration] = new JsonFormat[sbt.librarymanagement.coursier.CoursierConfiguration] { + override def read[J](jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.librarymanagement.coursier.CoursierConfiguration = { + jsOpt match { + case Some(js) => + unbuilder.beginObject(js) + val log = unbuilder.readField[Option[xsbti.Logger]]("log") + val resolvers = unbuilder.readField[Vector[sbt.librarymanagement.Resolver]]("resolvers") + val otherResolvers = unbuilder.readField[Vector[sbt.librarymanagement.Resolver]]("otherResolvers") + val reorderResolvers = unbuilder.readField[Boolean]("reorderResolvers") + val parallelDownloads = unbuilder.readField[Int]("parallelDownloads") + val maxIterations = unbuilder.readField[Int]("maxIterations") + unbuilder.endObject() + sbt.librarymanagement.coursier.CoursierConfiguration(log, resolvers, otherResolvers, reorderResolvers, parallelDownloads, maxIterations) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.librarymanagement.coursier.CoursierConfiguration, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("log", obj.log) + builder.addField("resolvers", obj.resolvers) + builder.addField("otherResolvers", obj.otherResolvers) + builder.addField("reorderResolvers", obj.reorderResolvers) + builder.addField("parallelDownloads", obj.parallelDownloads) + builder.addField("maxIterations", obj.maxIterations) + builder.endObject() + } +} +} diff --git a/coursier/src/main/contraband/lm-coursier.json b/coursier/src/main/contraband/lm-coursier.json new file mode 100644 index 000000000..927175607 --- /dev/null +++ b/coursier/src/main/contraband/lm-coursier.json @@ -0,0 +1,49 @@ +{ + "codecNamespace": "sbt.librarymanagement.coursier", + "types": [ + { + "name": "CoursierConfiguration", + "namespace": "sbt.librarymanagement.coursier", + "target": "Scala", + "type": "record", + "fields": [ + { + "name": "log", + "type": "xsbti.Logger?", + "default": "None", + "since": "0.0.1" + }, + { + "name": "resolvers", + "type": "sbt.librarymanagement.Resolver*", + "default": "sbt.librarymanagement.Resolver.defaults", + "since": "0.0.1" + }, + { + "name": "otherResolvers", + "type": "sbt.librarymanagement.Resolver*", + "default": "Vector.empty", + "since": "0.0.1" + }, + { + "name": "reorderResolvers", + "type": "Boolean", + "default": "true", + "since": "0.0.1" + }, + { + "name": "parallelDownloads", + "type": "Int", + "default": "6", + "since": "0.0.1" + }, + { + "name": "maxIterations", + "type": "Int", + "default": "100", + "since": "0.0.1" + } + ] + } + ] +} diff --git a/coursier/src/main/scala/coursier/FromSbt.scala b/coursier/src/main/scala/coursier/FromSbt.scala new file mode 100644 index 000000000..6539e1d97 --- /dev/null +++ b/coursier/src/main/scala/coursier/FromSbt.scala @@ -0,0 +1,283 @@ +package coursier + +import coursier.ivy.IvyRepository +import coursier.ivy.IvyXml.{ mappings => ivyXmlMappings } +import java.net.{ MalformedURLException, URL } + +import coursier.core.Authentication +import sbt.internal.librarymanagement.mavenint.SbtPomExtraProperties +import sbt.librarymanagement._ +import sbt.librarymanagement.Resolver +import sbt.util.Logger + +object FromSbt { + + def sbtModuleIdName( + moduleId: ModuleID, + scalaVersion: => String, + scalaBinaryVersion: => String + ): String = + sbtCrossVersionName(moduleId.name, moduleId.crossVersion, scalaVersion, scalaBinaryVersion) + + def sbtCrossVersionName( + name: String, + crossVersion: CrossVersion, + scalaVersion: => String, + scalaBinaryVersion: => String + ): String = + CrossVersion(crossVersion, scalaVersion, scalaBinaryVersion) + .fold(name)(_(name)) + + def attributes(attr: Map[String, String]): Map[String, String] = + attr + .map { + case (k, v) => + k.stripPrefix("e:") -> v + } + .filter { + case (k, _) => + !k.startsWith(SbtPomExtraProperties.POM_INFO_KEY_PREFIX) + } + + def moduleVersion( + module: ModuleID, + scalaVersion: String, + scalaBinaryVersion: String + ): (Module, String) = { + + val fullName = sbtModuleIdName(module, scalaVersion, scalaBinaryVersion) + + val module0 = + Module(module.organization, fullName, FromSbt.attributes(module.extraDependencyAttributes)) + val version = module.revision + + (module0, version) + } + + def dependencies( + module: ModuleID, + scalaVersion: String, + scalaBinaryVersion: String + ): Seq[(String, Dependency)] = { + + // TODO Warn about unsupported properties in `module` + + val (module0, version) = moduleVersion(module, scalaVersion, scalaBinaryVersion) + + val dep = Dependency( + module0, + version, + exclusions = module.exclusions.map { rule => + // FIXME Other `rule` fields are ignored here + (rule.organization, rule.name) + }.toSet, + transitive = module.isTransitive + ) + + val mapping = module.configurations.getOrElse("compile") + val allMappings = ivyXmlMappings(mapping) + + val attributes = + if (module.explicitArtifacts.isEmpty) + Seq(Attributes("", "")) + else + module.explicitArtifacts.map { a => + Attributes(`type` = a.`type`, classifier = a.classifier.getOrElse("")) + } + + for { + (from, to) <- allMappings + attr <- attributes + } yield from -> dep.copy(configuration = to, attributes = attr) + } + + def fallbackDependencies( + allDependencies: Seq[ModuleID], + scalaVersion: String, + scalaBinaryVersion: String + ): Seq[(Module, String, URL, Boolean)] = + for { + module <- allDependencies + artifact <- module.explicitArtifacts + url <- artifact.url.toSeq + } yield { + val (module0, version) = moduleVersion(module, scalaVersion, scalaBinaryVersion) + (module0, version, url, module.isChanging) + } + + def sbtClassifiersProject( + cm: GetClassifiersModule, + scalaVersion: String, + scalaBinaryVersion: String + ) = { + + val p = FromSbt.project( + cm.id, + cm.dependencies, + cm.configurations.map(cfg => cfg.name -> cfg.extendsConfigs.map(_.name)).toMap, + scalaVersion, + scalaBinaryVersion + ) + + // for w/e reasons, the dependencies sometimes don't land in the right config above + // this is a loose attempt at fixing that + cm.configurations match { + case Seq(cfg) => + p.copy( + dependencies = p.dependencies.map { + case (_, d) => (cfg.name, d) + } + ) + case _ => + p + } + } + + def project( + projectID: ModuleID, + allDependencies: Seq[ModuleID], + ivyConfigurations: Map[String, Seq[String]], + scalaVersion: String, + scalaBinaryVersion: String + ): Project = { + + val deps = allDependencies.flatMap(dependencies(_, scalaVersion, scalaBinaryVersion)) + + Project( + Module( + projectID.organization, + sbtModuleIdName(projectID, scalaVersion, scalaBinaryVersion), + FromSbt.attributes(projectID.extraDependencyAttributes) + ), + projectID.revision, + deps, + ivyConfigurations, + None, + Nil, + Nil, + Nil, + None, + None, + None, + None, + Nil, + Info.empty + ) + } + + private def mavenCompatibleBaseOpt(patterns: Patterns): Option[String] = + if (patterns.isMavenCompatible) { + val baseIvyPattern = patterns.ivyPatterns.head.takeWhile(c => c != '[' && c != '(') + val baseArtifactPattern = patterns.ivyPatterns.head.takeWhile(c => c != '[' && c != '(') + + if (baseIvyPattern == baseArtifactPattern) + Some(baseIvyPattern) + else + None + } else + None + + private def mavenRepositoryOpt( + root: String, + log: Logger, + authentication: Option[Authentication] + ): Option[MavenRepository] = + try { + Cache.url(root) // ensure root is a URL whose protocol can be handled here + val root0 = if (root.endsWith("/")) root else root + "/" + Some( + MavenRepository( + root0, + authentication = authentication + ) + ) + } catch { + case e: MalformedURLException => + log.warn( + "Error parsing Maven repository base " + + root + + Option(e.getMessage).fold("")(" (" + _ + ")") + + ", ignoring it" + ) + + None + } + + def repository( + resolver: Resolver, + ivyProperties: Map[String, String], + log: Logger, + authentication: Option[Authentication] + ): Option[Repository] = + resolver match { + case r: sbt.librarymanagement.MavenRepository => + mavenRepositoryOpt(r.root, log, authentication) + + case r: FileRepository + if r.patterns.ivyPatterns.lengthCompare(1) == 0 && + r.patterns.artifactPatterns.lengthCompare(1) == 0 => + val mavenCompatibleBaseOpt0 = mavenCompatibleBaseOpt(r.patterns) + + mavenCompatibleBaseOpt0 match { + case None => + val repo = IvyRepository.parse( + "file://" + r.patterns.artifactPatterns.head, + metadataPatternOpt = Some("file://" + r.patterns.ivyPatterns.head), + changing = Some(true), + properties = ivyProperties, + dropInfoAttributes = true, + authentication = authentication + ) match { + case Left(err) => + sys.error( + s"Cannot parse Ivy patterns ${r.patterns.artifactPatterns.head} and ${r.patterns.ivyPatterns.head}: $err" + ) + case Right(repo) => + repo + } + + Some(repo) + + case Some(mavenCompatibleBase) => + mavenRepositoryOpt("file://" + mavenCompatibleBase, log, authentication) + } + + case r: URLRepository + if r.patterns.ivyPatterns.lengthCompare(1) == 0 && + r.patterns.artifactPatterns.lengthCompare(1) == 0 => + val mavenCompatibleBaseOpt0 = mavenCompatibleBaseOpt(r.patterns) + + mavenCompatibleBaseOpt0 match { + case None => + val repo = IvyRepository.parse( + r.patterns.artifactPatterns.head, + metadataPatternOpt = Some(r.patterns.ivyPatterns.head), + changing = None, + properties = ivyProperties, + dropInfoAttributes = true, + authentication = authentication + ) match { + case Left(err) => + sys.error( + s"Cannot parse Ivy patterns ${r.patterns.artifactPatterns.head} and ${r.patterns.ivyPatterns.head}: $err" + ) + case Right(repo) => + repo + } + + Some(repo) + + case Some(mavenCompatibleBase) => + mavenRepositoryOpt(mavenCompatibleBase, log, authentication) + } + + case raw: RawRepository + if raw.name == "inter-project" => // sbt.RawRepository.equals just compares names anyway + None + + case other => + log.warn(s"Unrecognized repository ${other.name}, ignoring it") + None + } + +} diff --git a/coursier/src/main/scala/coursier/SbtBootJars.scala b/coursier/src/main/scala/coursier/SbtBootJars.scala new file mode 100644 index 000000000..1d9df09cd --- /dev/null +++ b/coursier/src/main/scala/coursier/SbtBootJars.scala @@ -0,0 +1,18 @@ +package coursier + +import java.io.File + +object SbtBootJars { + def apply( + scalaOrg: String, + scalaVersion: String, + jars: Seq[File] + ): Map[(Module, String), File] = + jars.collect { + case jar if jar.getName.endsWith(".jar") => + val name = jar.getName.stripSuffix(".jar") + val mod = Module(scalaOrg, name) + + (mod, scalaVersion) -> jar + }.toMap +} diff --git a/coursier/src/main/scala/coursier/ToSbt.scala b/coursier/src/main/scala/coursier/ToSbt.scala new file mode 100644 index 000000000..5af3bfc75 --- /dev/null +++ b/coursier/src/main/scala/coursier/ToSbt.scala @@ -0,0 +1,275 @@ +package coursier + +import java.io.File +import java.net.URL +import java.util.GregorianCalendar +import java.util.concurrent.ConcurrentHashMap + +import coursier.maven.MavenSource +import sbt.librarymanagement._ +import sbt.util.Logger + +object ToSbt { + + private def caching[K, V](f: K => V): K => V = { + + val cache = new ConcurrentHashMap[K, V] + + key => + val previousValueOpt = Option(cache.get(key)) + + previousValueOpt.getOrElse { + val value = f(key) + val concurrentValueOpt = Option(cache.putIfAbsent(key, value)) + concurrentValueOpt.getOrElse(value) + } + } + + val moduleId = caching[(Dependency, Map[String, String]), ModuleID] { + case (dependency, extraProperties) => + sbt.librarymanagement + .ModuleID( + dependency.module.organization, + dependency.module.name, + dependency.version + ) + .withConfigurations( + Some(dependency.configuration) + ) + .withExtraAttributes( + dependency.module.attributes ++ extraProperties + ) + .withExclusions( + dependency.exclusions.toVector + .map { + case (org, name) => + sbt.librarymanagement + .InclExclRule() + .withOrganization(org) + .withName(name) + } + ) + .withIsTransitive( + dependency.transitive + ) + } + + val artifact = caching[(Module, Map[String, String], Artifact), sbt.librarymanagement.Artifact] { + case (module, extraProperties, artifact) => + sbt.librarymanagement + .Artifact(module.name) + // FIXME Get these two from publications + .withType(artifact.attributes.`type`) + .withExtension(MavenSource.typeExtension(artifact.attributes.`type`)) + .withClassifier( + Some(artifact.attributes.classifier) + .filter(_.nonEmpty) + .orElse(MavenSource.typeDefaultClassifierOpt(artifact.attributes.`type`)) + ) + // .withConfigurations(Vector()) + .withUrl(Some(new URL(artifact.url))) + .withExtraAttributes(module.attributes ++ extraProperties) + } + + val moduleReport = + caching[(Dependency, Seq[(Dependency, Project)], Project, Seq[(Artifact, Option[File])]), + ModuleReport] { + case (dependency, dependees, project, artifacts) => + val sbtArtifacts = artifacts.collect { + case (artifact, Some(file)) => + (ToSbt.artifact((dependency.module, project.properties.toMap, artifact)), file) + } + val sbtMissingArtifacts = artifacts.collect { + case (artifact, None) => + ToSbt.artifact((dependency.module, project.properties.toMap, artifact)) + } + + val publicationDate = project.info.publication.map { dt => + new GregorianCalendar(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) + } + + val callers = dependees.map { + case (dependee, dependeeProj) => + Caller( + ToSbt.moduleId((dependee, dependeeProj.properties.toMap)), + dependeeProj.configurations.keys.toVector.map(ConfigRef(_)), + dependee.module.attributes ++ dependeeProj.properties, + // FIXME Set better values here + isForceDependency = false, + isChangingDependency = false, + isTransitiveDependency = false, + isDirectlyForceDependency = false + ) + } + + ModuleReport( + ToSbt.moduleId((dependency, project.properties.toMap)), + sbtArtifacts.toVector, + sbtMissingArtifacts.toVector + ) + // .withStatus(None) + .withPublicationDate(publicationDate) + // .withResolver(None) + // .withArtifactResolver(None) + // .withEvicted(false) + // .withEvictedData(None) + // .withEvictedReason(None) + // .withProblem(None) + .withHomepage(Some(project.info.homePage).filter(_.nonEmpty)) + .withExtraAttributes(dependency.module.attributes ++ project.properties) + // .withIsDefault(None) + // .withBranch(None) + .withConfigurations(project.configurations.keys.toVector.map(ConfigRef(_))) + .withLicenses(project.info.licenses.toVector) + .withCallers(callers.toVector) + } + + private def grouped[K, V](map: Seq[(K, V)]): Map[K, Seq[V]] = + map.groupBy { case (k, _) => k }.map { + case (k, l) => + k -> l.map { case (_, v) => v } + } + + def moduleReports( + res: Resolution, + classifiersOpt: Option[Seq[String]], + artifactFileOpt: (Module, String, Artifact) => Option[File], + log: Logger, + keepPomArtifact: Boolean = false, + includeSignatures: Boolean = false + ) = { + val depArtifacts1 = + classifiersOpt match { + case None => res.dependencyArtifacts(withOptional = true) + case Some(cl) => res.dependencyClassifiersArtifacts(cl) + } + + val depArtifacts0 = + if (keepPomArtifact) + depArtifacts1 + else + depArtifacts1.filter { + case (_, a) => a.attributes != Attributes("pom", "") + } + + val depArtifacts = + if (includeSignatures) { + + val notFound = depArtifacts0.filter(!_._2.extra.contains("sig")) + + if (notFound.isEmpty) + depArtifacts0.flatMap { + case (dep, a) => + Seq(dep -> a) ++ a.extra.get("sig").toSeq.map(dep -> _) + } else { + for ((_, a) <- notFound) + log.error(s"No signature found for ${a.url}") + sys.error(s"${notFound.length} signature(s) not found") + } + } else + depArtifacts0 + + val groupedDepArtifacts = grouped(depArtifacts) + + val versions = res.dependencies.toVector.map { dep => + dep.module -> dep.version + }.toMap + + def clean(dep: Dependency): Dependency = + dep.copy(configuration = "", exclusions = Set.empty, optional = false) + + val reverseDependencies = res.reverseDependencies.toVector + .map { + case (k, v) => + clean(k) -> v.map(clean) + } + .groupBy { case (k, _) => k } + .mapValues { v => + v.flatMap { + case (_, l) => l + } + } + .toVector + .toMap + + groupedDepArtifacts.map { + case (dep, artifacts) => + val (_, proj) = res.projectCache(dep.moduleVersion) + + // FIXME Likely flaky... + val dependees = reverseDependencies + .getOrElse(clean(dep.copy(version = "")), Vector.empty) + .map { dependee0 => + val version = versions(dependee0.module) + val dependee = dependee0.copy(version = version) + val (_, dependeeProj) = res.projectCache(dependee.moduleVersion) + (dependee, dependeeProj) + } + + ToSbt.moduleReport( + ( + dep, + dependees, + proj, + artifacts.map(a => a -> artifactFileOpt(proj.module, proj.version, a)) + )) + } + } + + def updateReport( + configDependencies: Map[String, Seq[Dependency]], + resolutions: Map[String, Resolution], + configs: Map[String, Set[String]], + classifiersOpt: Option[Seq[String]], + artifactFileOpt: (Module, String, Artifact) => Option[File], + log: Logger, + keepPomArtifact: Boolean = false, + includeSignatures: Boolean = false + ): UpdateReport = { + + val configReports = configs.map { + case (config, extends0) => + val configDeps = extends0.flatMap(configDependencies.getOrElse(_, Nil)) + val subRes = resolutions(config).subset(configDeps) + + val reports = ToSbt.moduleReports( + subRes, + classifiersOpt, + artifactFileOpt, + log, + keepPomArtifact = keepPomArtifact, + includeSignatures = includeSignatures + ) + + val reports0 = + if (subRes.rootDependencies.size == 1) { + // quick hack ensuring the module for the only root dependency + // appears first in the update report, see https://github.com/coursier/coursier/issues/650 + val dep = subRes.rootDependencies.head + val (_, proj) = subRes.projectCache(dep.moduleVersion) + val mod = ToSbt.moduleId((dep, proj.properties.toMap)) + val (main, other) = reports.partition { r => + r.module.organization == mod.organization && + r.module.name == mod.name && + r.module.crossVersion == mod.crossVersion + } + main.toVector ++ other.toVector + } else + reports.toVector + + ConfigurationReport( + ConfigRef(config), + reports0, + Vector() + ) + } + + UpdateReport( + File.createTempFile("fake-update-report", "json"), + configReports.toVector, + UpdateStats(-1L, -1L, -1L, cached = false), + Map.empty + ) + } + +} diff --git a/coursier/src/main/scala/sbt/librarymanagement/coursier/CoursierDependencyResolution.scala b/coursier/src/main/scala/sbt/librarymanagement/coursier/CoursierDependencyResolution.scala new file mode 100644 index 000000000..3734fbce8 --- /dev/null +++ b/coursier/src/main/scala/sbt/librarymanagement/coursier/CoursierDependencyResolution.scala @@ -0,0 +1,382 @@ +package sbt.librarymanagement.coursier + +import java.io.{ File, OutputStreamWriter } +import java.util.concurrent.Executors + +import scala.util.{ Failure, Success } +import scala.concurrent.ExecutionContext +import coursier.{ Artifact, Resolution, _ } +import coursier.util.{ Gather, Task } +import sbt.internal.librarymanagement.IvySbt +import sbt.librarymanagement.Configurations.{ CompilerPlugin, Component, ScalaTool } +import sbt.librarymanagement._ +import sbt.util.Logger +import sjsonnew.JsonFormat +import sjsonnew.support.murmurhash.Hasher + +case class CoursierModuleDescriptor( + directDependencies: Vector[ModuleID], + scalaModuleInfo: Option[ScalaModuleInfo], + moduleSettings: ModuleSettings, + configurations: Seq[String], + extraInputHash: Long +) extends ModuleDescriptor + +case class CoursierModuleSettings() extends ModuleSettings + +private[sbt] class CoursierDependencyResolution(coursierConfiguration: CoursierConfiguration) + extends DependencyResolutionInterface { + + // keep the pool alive while the class is loaded + private lazy val pool = + ExecutionContext.fromExecutor( + Executors.newFixedThreadPool(coursierConfiguration.parallelDownloads) + ) + + private[coursier] val reorderedResolvers = { + val resolvers0 = + coursierConfiguration.resolvers ++ coursierConfiguration.otherResolvers + + if (coursierConfiguration.reorderResolvers) { + Resolvers.reorder(resolvers0) + } else resolvers0 + } + + private[sbt] object AltLibraryManagementCodec extends CoursierLibraryManagementCodec { + type CoursierHL = ( + Vector[Resolver], + Vector[Resolver], + Boolean, + Int, + Int + ) + + def coursierToHL(c: CoursierConfiguration): CoursierHL = + ( + c.resolvers, + c.otherResolvers, + c.reorderResolvers, + c.parallelDownloads, + c.maxIterations + ) + // Redefine to use a subset of properties, that are serialisable + override implicit lazy val CoursierConfigurationFormat: JsonFormat[CoursierConfiguration] = { + def hlToCoursier(c: CoursierHL): CoursierConfiguration = { + val ( + resolvers, + otherResolvers, + reorderResolvers, + parallelDownloads, + maxIterations + ) = c + CoursierConfiguration() + .withResolvers(resolvers) + .withOtherResolvers(otherResolvers) + .withReorderResolvers(reorderResolvers) + .withParallelDownloads(parallelDownloads) + .withMaxIterations(maxIterations) + } + projectFormat[CoursierConfiguration, CoursierHL](coursierToHL, hlToCoursier) + } + } + + def extraInputHash: Long = { + import AltLibraryManagementCodec._ + Hasher.hash(coursierConfiguration) match { + case Success(keyHash) => keyHash.toLong + case Failure(_) => 0L + } + } + + /** + * Builds a ModuleDescriptor that describes a subproject with dependencies. + * + * @param moduleSetting It contains the information about the module including the dependencies. + * @return A `ModuleDescriptor` describing a subproject and its dependencies. + */ + override def moduleDescriptor( + moduleSetting: ModuleDescriptorConfiguration): CoursierModuleDescriptor = { + CoursierModuleDescriptor( + moduleSetting.dependencies, + moduleSetting.scalaModuleInfo, + CoursierModuleSettings(), + moduleSetting.configurations.map(_.name), + extraInputHash + ) + } + + val ivyHome = sys.props.getOrElse( + "ivy.home", + new File(sys.props("user.home")).toURI.getPath + ".ivy2" + ) + + val sbtIvyHome = sys.props.getOrElse( + "sbt.ivy.home", + ivyHome + ) + + val ivyProperties = Map( + "ivy.home" -> ivyHome, + "sbt.ivy.home" -> sbtIvyHome + ) ++ sys.props + + /** + * Resolves the given module's dependencies performing a retrieval. + * + * @param module The module to be resolved. + * @param configuration The update configuration. + * @param uwconfig The configuration to handle unresolved warnings. + * @param log The logger. + * @return The result, either an unresolved warning or an update report. Note that this + * update report will or will not be successful depending on the `missingOk` option. + */ + override def update(module: ModuleDescriptor, + configuration: UpdateConfiguration, + uwconfig: UnresolvedWarningConfiguration, + log: Logger): Either[UnresolvedWarning, UpdateReport] = { + + // not sure what DependencyResolutionInterface.moduleDescriptor is for, we're handled ivy stuff anyway... + val module0 = module match { + case c: CoursierModuleDescriptor => c + // This shouldn't happen at best of my understanding + case i: IvySbt#Module => + moduleDescriptor( + i.moduleSettings match { + case c: ModuleDescriptorConfiguration => c + case other => sys.error(s"unrecognized module settings: $other") + } + ) + case _ => + sys.error(s"unrecognized ModuleDescriptor type: $module") + } + + if (reorderedResolvers.isEmpty) { + log.error( + "Dependency resolution is configured with an empty list of resolvers. This is unlikely to work.") + } + + val dependencies = module.directDependencies.map(toCoursierDependency).flatten.toSet + val start = Resolution(dependencies.map(_._1)) + val authentication = None // TODO: get correct value + val ivyConfiguration = ivyProperties // TODO: is it enough? + + val repositories = + reorderedResolvers.flatMap(r => FromSbt.repository(r, ivyConfiguration, log, authentication)) ++ Seq( + Cache.ivy2Local, + Cache.ivy2Cache + ) + + implicit val ec = pool + + val coursierLogger = createLogger() + try { + val fetch = Fetch.from( + repositories, + Cache.fetch[Task](logger = Some(coursierLogger)) + ) + val resolution = start.process + .run(fetch, coursierConfiguration.maxIterations) + .unsafeRun() + + def updateReport() = { + val localArtifacts: Map[Artifact, Either[FileError, File]] = Gather[Task] + .gather( + resolution.artifacts.map { a => + Cache + .file[Task](a, logger = Some(coursierLogger)) + .run + .map((a, _)) + } + ) + .unsafeRun() + .toMap + + toUpdateReport(resolution, + (module0.configurations ++ dependencies.map(_._2)).distinct, + localArtifacts, + log) + } + + if (resolution.isDone && + resolution.errors.isEmpty && + resolution.conflicts.isEmpty) { + updateReport() + } else if (resolution.isDone && + (!resolution.errors.isEmpty && configuration.missingOk) + && resolution.conflicts.isEmpty) { + log.warn(s"""Failed to download artifacts: ${resolution.errors + .map(_._2) + .flatten + .mkString(", ")}""") + updateReport() + } else { + toSbtError(log, uwconfig, resolution) + } + } finally { + coursierLogger.stop() + } + } + + // utilities + private def createLogger() = { + val t = new TermDisplay(new OutputStreamWriter(System.out)) + t.init() + t + } + + private def toCoursierDependency(moduleID: ModuleID) = { + val attributes = + if (moduleID.explicitArtifacts.isEmpty) + Seq(Attributes("", "")) + else + moduleID.explicitArtifacts.map { a => + Attributes(`type` = a.`type`, classifier = a.classifier.getOrElse("")) + } + + val extraAttrs = FromSbt.attributes(moduleID.extraDependencyAttributes) + + val mapping = moduleID.configurations.getOrElse("compile") + + import _root_.coursier.ivy.IvyXml.{ mappings => ivyXmlMappings } + val allMappings = ivyXmlMappings(mapping) + + for { + (from, to) <- allMappings + attr <- attributes + } yield { + Dependency( + Module(moduleID.organization, moduleID.name, extraAttrs), + moduleID.revision, + configuration = to, + attributes = attr, + exclusions = moduleID.exclusions.map { rule => + (rule.organization, rule.name) + }.toSet, + transitive = moduleID.isTransitive + ) -> from + } + } + + private def toUpdateReport(resolution: Resolution, + configurations: Seq[String], + artifactFilesOrErrors0: Map[Artifact, Either[FileError, File]], + log: Logger): Either[UnresolvedWarning, UpdateReport] = { + + val artifactFiles = artifactFilesOrErrors0.collect { + case (artifact, Right(file)) => + artifact -> file + } + + val artifactErrors = artifactFilesOrErrors0.toVector + .collect { + case (a, Left(err)) if !a.isOptional || !err.notFound => + a -> err + } + + val erroredArtifacts = artifactFilesOrErrors0.collect { + case (a, Left(_)) => a + }.toSet + + val depsByConfig = { + val deps = resolution.dependencies.toVector + configurations + .map((_, deps)) + .toMap + } + + val configurations0 = extractConfigurationTree(configurations) + + val configResolutions = + (depsByConfig.keys ++ configurations0.keys).map(k => (k, resolution)).toMap + + val sbtBootJarOverrides = Map.empty[(Module, String), File] + val classifiers = None // TODO: get correct values + + if (artifactErrors.isEmpty) { + Right( + ToSbt.updateReport( + depsByConfig, + configResolutions, + configurations0, + classifiers, + artifactFileOpt( + sbtBootJarOverrides, + artifactFiles, + erroredArtifacts, + log, + _, + _, + _ + ), + log + )) + } else { + throw new RuntimeException(s"Could not save downloaded dependencies: $erroredArtifacts") + } + + } + + type ConfigurationName = String + type ConfigurationDependencyTree = Map[ConfigurationName, Set[ConfigurationName]] + + // Key is the name of the configuration (i.e. `compile`) and the values are the name itself plus the + // names of the configurations that this one depends on. + private def extractConfigurationTree(available: Seq[String]): ConfigurationDependencyTree = { + (Configurations.default ++ + Configurations.defaultInternal ++ + Seq(ScalaTool, CompilerPlugin, Component)) + .filter(c => available.contains(c.name)) + .map(c => (c.name, c.extendsConfigs.map(_.name) :+ c.name)) + .toMap + .mapValues(_.toSet) + } + + private def artifactFileOpt( + sbtBootJarOverrides: Map[(Module, String), File], + artifactFiles: Map[Artifact, File], + erroredArtifacts: Set[Artifact], + log: Logger, + module: Module, + version: String, + artifact: Artifact + ) = { + + val artifact0 = artifact + .copy(attributes = Attributes()) // temporary hack :-( + + // Under some conditions, SBT puts the scala JARs of its own classpath + // in the application classpath. Ensuring we return SBT's jars rather than + // JARs from the coursier cache, so that a same JAR doesn't land twice in the + // application classpath (once via SBT jars, once via coursier cache). + val fromBootJars = + if (artifact.classifier.isEmpty && artifact.`type` == "jar") + sbtBootJarOverrides.get((module, version)) + else + None + + val res = fromBootJars.orElse(artifactFiles.get(artifact0)) + if (res.isEmpty && !erroredArtifacts(artifact0)) + log.error(s"${artifact.url} not downloaded (should not happen)") + + res + } + + private def toSbtError(log: Logger, + uwconfig: UnresolvedWarningConfiguration, + resolution: Resolution) = { + val failedResolution = resolution.errors.map { + case ((failedModule, failedVersion), _) => + ModuleID(failedModule.organization, failedModule.name, failedVersion) + } + val msgs = resolution.errors.flatMap(_._2) + log.debug(s"Failed resolution: $msgs") + log.debug(s"Missing artifacts: $failedResolution") + val ex = new ResolveException(msgs, failedResolution) + Left(UnresolvedWarning(ex, uwconfig)) + } +} + +object CoursierDependencyResolution { + def apply(configuration: CoursierConfiguration) = + DependencyResolution(new CoursierDependencyResolution(configuration)) +} diff --git a/coursier/src/main/scala/sbt/librarymanagement/coursier/CoursierLibraryManagementCodec.scala b/coursier/src/main/scala/sbt/librarymanagement/coursier/CoursierLibraryManagementCodec.scala new file mode 100644 index 000000000..5bb0f26ba --- /dev/null +++ b/coursier/src/main/scala/sbt/librarymanagement/coursier/CoursierLibraryManagementCodec.scala @@ -0,0 +1,12 @@ +package sbt.librarymanagement +package coursier + +trait CoursierLibraryManagementCodec + extends sjsonnew.BasicJsonProtocol + with LibraryManagementCodec + // with sbt.internal.librarymanagement.formats.GlobalLockFormat + with sbt.internal.librarymanagement.formats.LoggerFormat + with sbt.librarymanagement.ResolverFormats + with CoursierConfigurationFormats + +object CoursierLibraryManagementCodec extends CoursierLibraryManagementCodec diff --git a/coursier/src/main/scala/sbt/librarymanagement/coursier/Resolvers.scala b/coursier/src/main/scala/sbt/librarymanagement/coursier/Resolvers.scala new file mode 100644 index 000000000..ac656ac8a --- /dev/null +++ b/coursier/src/main/scala/sbt/librarymanagement/coursier/Resolvers.scala @@ -0,0 +1,51 @@ +package sbt.librarymanagement.coursier + +import sbt.librarymanagement.{ MavenRepository, Resolver, URLRepository } + +object Resolvers { + + private val slowReposBase = Seq( + "https://repo.typesafe.com/", + "https://repo.scala-sbt.org/", + "http://repo.typesafe.com/", + "http://repo.scala-sbt.org/" + ) + + private val fastReposBase = Seq( + "http://repo1.maven.org/", + "https://repo1.maven.org/" + ) + + private def url(res: Resolver): Option[String] = + res match { + case m: MavenRepository => + Some(m.root) + case u: URLRepository => + u.patterns.artifactPatterns.headOption + .orElse(u.patterns.ivyPatterns.headOption) + case _ => + None + } + + private def filterResolvers(bases: Seq[String], + resolvers: Seq[(Resolver, Option[String])]): Seq[Resolver] = + resolvers + .filter(tuple => tuple._2.exists(url => bases.exists(base => url.startsWith(base)))) + .map(_._1) + + def reorder(resolvers: Seq[Resolver]): Seq[Resolver] = { + + val byUrl = resolvers.map(r => (r, url(r))) + + val fast = filterResolvers(fastReposBase, byUrl) + val slow = filterResolvers(slowReposBase, byUrl) + val rest = resolvers.diff(fast).diff(slow) + + val reordered = fast ++ rest ++ slow + assert(reordered.size == resolvers.size, + "Reordered resolvers should be the same size as the unordered ones.") + + reordered + } + +} diff --git a/coursier/src/test/scala/sbt/librarymanagement/coursier/BaseCoursierSpecification.scala b/coursier/src/test/scala/sbt/librarymanagement/coursier/BaseCoursierSpecification.scala new file mode 100644 index 000000000..20545bcf3 --- /dev/null +++ b/coursier/src/test/scala/sbt/librarymanagement/coursier/BaseCoursierSpecification.scala @@ -0,0 +1,40 @@ +package sbt.librarymanagement.coursier + +import sbt.internal.librarymanagement.cross.CrossVersionUtil +import sbt.internal.util.ConsoleLogger +import sbt.librarymanagement.Configurations._ +import sbt.librarymanagement._ + +trait BaseCoursierSpecification extends UnitSpec { + lazy val log = ConsoleLogger() + val lmEngine: CoursierDependencyResolution + + def configurations = Vector(Compile, Test, Runtime) + def module(moduleId: ModuleID, + deps: Vector[ModuleID], + scalaFullVersion: Option[String], + overrideScalaVersion: Boolean = true): ModuleDescriptor = { + val scalaModuleInfo = scalaFullVersion map { fv => + ScalaModuleInfo( + scalaFullVersion = fv, + scalaBinaryVersion = CrossVersionUtil.binaryScalaVersion(fv), + configurations = configurations, + checkExplicit = true, + filterImplicit = false, + overrideScalaVersion = overrideScalaVersion + ) + } + + val moduleSetting = ModuleDescriptorConfiguration(moduleId, ModuleInfo("foo")) + .withDependencies(deps) + .withConfigurations(configurations) + .withScalaModuleInfo(scalaModuleInfo) + lmEngine.moduleDescriptor(moduleSetting) + } + + def resolvers: Vector[Resolver] + + def configuration(res: Vector[Resolver] = resolvers) = + CoursierConfiguration().withResolvers(res) + +} diff --git a/coursier/src/test/scala/sbt/librarymanagement/coursier/ResolutionSpec.scala b/coursier/src/test/scala/sbt/librarymanagement/coursier/ResolutionSpec.scala new file mode 100644 index 000000000..a118ccc10 --- /dev/null +++ b/coursier/src/test/scala/sbt/librarymanagement/coursier/ResolutionSpec.scala @@ -0,0 +1,144 @@ +package sbt.librarymanagement.coursier + +import sbt.librarymanagement.Configurations.Component +import sbt.librarymanagement.Resolver.{ + DefaultMavenRepository, + JCenterRepository, + JavaNet2Repository +} +import sbt.librarymanagement.syntax._ +import sbt.librarymanagement.{ Resolver, UnresolvedWarningConfiguration, UpdateConfiguration } + +class ResolutionSpec extends BaseCoursierSpecification { + override def resolvers = Vector( + DefaultMavenRepository, + JavaNet2Repository, + JCenterRepository, + Resolver.sbtPluginRepo("releases") + ) + + val lmEngine = new CoursierDependencyResolution(configuration()) + + private final val stubModule = "com.example" % "foo" % "0.1.0" % "compile" + + "Coursier dependency resolution" should "resolve very simple module" in { + val dependencies = Vector( + "com.typesafe.scala-logging" % "scala-logging_2.12" % "3.7.2" % "compile", + "org.scalatest" % "scalatest_2.12" % "3.0.4" % "test" + ).map(_.withIsTransitive(false)) + + val coursierModule = module(stubModule, dependencies, Some("2.12.4")) + val resolution = + lmEngine.update(coursierModule, UpdateConfiguration(), UnresolvedWarningConfiguration(), log) + + resolution should be('right) + val r = resolution.right.get + r.configurations.map(_.configuration) should have size 3 + + val compileConfig = r.configurations.find(_.configuration == Compile.toConfigRef).get + compileConfig.modules should have size 2 + + val runtimeConfig = r.configurations.find(_.configuration == Runtime.toConfigRef).get + runtimeConfig.modules should have size 2 + + val testConfig = r.configurations.find(_.configuration == Test.toConfigRef).get + testConfig.modules should have size 2 + } + + it should "resolve compiler bridge" in { + val dependencies = + Vector(("org.scala-sbt" % "compiler-interface" % "1.0.4" % "component").sources()) + val coursierModule = module(stubModule, dependencies, Some("2.12.4")) + val resolution = + lmEngine.update(coursierModule, UpdateConfiguration(), UnresolvedWarningConfiguration(), log) + + val r = resolution.right.get + + val componentConfig = r.configurations.find(_.configuration == Component.toConfigRef).get + componentConfig.modules should have size 2 + componentConfig.modules.head.artifacts should have size 1 + componentConfig.modules.head.artifacts.head._1.classifier should contain("sources") + } + + it should "resolve sbt jars" in { + val dependencies = + Vector(("org.scala-sbt" % "sbt" % "1.1.0" % "provided")) + val coursierModule = module(stubModule, dependencies, Some("2.12.4")) + val resolution = + lmEngine.update(coursierModule, UpdateConfiguration(), UnresolvedWarningConfiguration(), log) + + val r = resolution.right.get + + val modules = r.configurations.flatMap(_.modules) + modules.map(_.module.name) should contain("main_2.12") + } + + it should "resolve with default resolvers" in { + val dependencies = + Vector(("org.scala-sbt" % "compiler-interface" % "1.0.4" % "component").sources()) + val lmEngine = + CoursierDependencyResolution.apply( + configuration(Resolver.combineDefaultResolvers(Vector.empty)) + ) + val coursierModule = module(stubModule, dependencies, Some("2.12.4")) + val resolution = + lmEngine.update(coursierModule, UpdateConfiguration(), UnresolvedWarningConfiguration(), log) + + resolution should be('right) + } + + it should "resolve plugin" in { + val pluginAttributes = Map("scalaVersion" -> "2.12", "sbtVersion" -> "1.0") + val dependencies = + Vector(("org.xerial.sbt" % "sbt-sonatype" % "2.0").withExtraAttributes(pluginAttributes)) + val coursierModule = module(stubModule, dependencies, Some("2.12.4")) + val resolution = + lmEngine.update(coursierModule, UpdateConfiguration(), UnresolvedWarningConfiguration(), log) + + val r = resolution.right.get + + val componentConfig = r.configurations.find(_.configuration == Compile.toConfigRef).get + componentConfig.modules.map(_.module.name) should have size 5 + } + + it should "strip e: prefix from plugin attributes" in { + val pluginAttributes = Map("e:scalaVersion" -> "2.12", "e:sbtVersion" -> "1.0") + val dependencies = + Vector(("org.xerial.sbt" % "sbt-sonatype" % "2.0").withExtraAttributes(pluginAttributes)) + val coursierModule = module(stubModule, dependencies, Some("2.12.4")) + val resolution = + lmEngine.update(coursierModule, UpdateConfiguration(), UnresolvedWarningConfiguration(), log) + + resolution should be('right) + } + + it should "resolve plugins hosted on repo.typesafe.com" in { + val pluginAttributes = Map("e:scalaVersion" -> "2.12", "e:sbtVersion" -> "1.0") + val dependencies = + Vector(("com.typesafe.sbt" % "sbt-git" % "0.9.3").withExtraAttributes(pluginAttributes)) + val coursierModule = module(stubModule, dependencies, Some("2.12.4")) + val resolution = + lmEngine.update(coursierModule, UpdateConfiguration(), UnresolvedWarningConfiguration(), log) + + resolution should be('right) + } + + it should "reorder fast and slow resolvers" in { + val resolvers = Vector( + JavaNet2Repository, + Resolver.sbtPluginRepo("releases"), + DefaultMavenRepository + ) + val engine = new CoursierDependencyResolution(configuration(resolvers)) + engine.reorderedResolvers.head.name should be("public") + engine.reorderedResolvers.last.name should be("sbt-plugin-releases") + engine.reorderedResolvers should have size 3 + } + + it should "reorder default resolvers" in { + val resolvers = Resolver.combineDefaultResolvers(Vector.empty) + val engine = new CoursierDependencyResolution(configuration(resolvers)) + engine.reorderedResolvers should not be 'empty + engine.reorderedResolvers.head.name should be("public") + } +} diff --git a/coursier/src/test/scala/sbt/librarymanagement/coursier/UnitSpec.scala b/coursier/src/test/scala/sbt/librarymanagement/coursier/UnitSpec.scala new file mode 100644 index 000000000..e6d7f8e75 --- /dev/null +++ b/coursier/src/test/scala/sbt/librarymanagement/coursier/UnitSpec.scala @@ -0,0 +1,5 @@ +package sbt.librarymanagement.coursier + +import org.scalatest.{ FlatSpec, Matchers } + +abstract class UnitSpec extends FlatSpec with Matchers diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 21ade5fc5..bc9a8f31c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -9,6 +9,8 @@ object Dependencies { private val ioVersion = "1.2.1" private val utilVersion = "1.2.2" + private val coursierVersion = "1.1.0-M7" + private val sbtIO = "org.scala-sbt" %% "io" % ioVersion private val utilPosition = "org.scala-sbt" %% "util-position" % utilVersion @@ -41,7 +43,13 @@ object Dependencies { val launcherInterface = "org.scala-sbt" % "launcher-interface" % "1.0.0" val ivy = "org.scala-sbt.ivy" % "ivy" % "2.3.0-sbt-b18f59ea3bc914a297bb6f1a4f7fb0ace399e310" - val jsch = "com.jcraft" % "jsch" % "0.1.54" + val coursier = "io.get-coursier" %% "coursier" % coursierVersion + val coursierCache = "io.get-coursier" %% "coursier-cache" % coursierVersion + + val sbtV = "1.0" + val scalaV = "2.12" + + val jsch = "com.jcraft" % "jsch" % "0.1.54" intransitive () val scalaReflect = Def.setting { "org.scala-lang" % "scala-reflect" % scalaVersion.value } val scalaCompiler = Def.setting { "org.scala-lang" % "scala-compiler" % scalaVersion.value } val scalaXml = scala211Module("scala-xml", "1.0.5") diff --git a/project/SbtScriptedIT.scala b/project/SbtScriptedIT.scala index 925875479..7094191ab 100644 --- a/project/SbtScriptedIT.scala +++ b/project/SbtScriptedIT.scala @@ -85,15 +85,17 @@ object SbtScriptedIT extends AutoPlugin { scriptedTests := { val targetDir = target.value / "sbt" - cloneSbt(targetDir, scriptedTestSbtRepo.value, scriptedTestSbtRef.value) + if (!targetDir.exists) { + cloneSbt(targetDir, scriptedTestSbtRepo.value, scriptedTestSbtRef.value) - publishLocalSbt( - targetDir, - version.value, - organization.value, - s"librarymanagement-${scriptedTestLMImpl.value}", - scriptedSbtVersion.value - ) + publishLocalSbt( + targetDir, + version.value, + organization.value, + s"librarymanagement-${scriptedTestLMImpl.value}", + scriptedSbtVersion.value + ) + } setScriptedTestsSbtVersion( sbtTestDirectory.value / thisProject.value.id, diff --git a/scripted-test/src/sbt-test/lmScriptedTest/multi-project/build.sbt b/scripted-test/src/sbt-test/lmScriptedTest/multi-project/build.sbt new file mode 100644 index 000000000..ca1db4e98 --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/multi-project/build.sbt @@ -0,0 +1,3 @@ +lazy val root = (project in file(".")).aggregate(parent, child) +lazy val parent = project +lazy val child = project.dependsOn(parent) diff --git a/scripted-test/src/sbt-test/lmScriptedTest/multi-project/child/src/main/scala/Bar.scala b/scripted-test/src/sbt-test/lmScriptedTest/multi-project/child/src/main/scala/Bar.scala new file mode 100644 index 000000000..458d393cf --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/multi-project/child/src/main/scala/Bar.scala @@ -0,0 +1 @@ +class Bar extends Foo diff --git a/scripted-test/src/sbt-test/lmScriptedTest/multi-project/parent/src/main/scala/Foo.scala b/scripted-test/src/sbt-test/lmScriptedTest/multi-project/parent/src/main/scala/Foo.scala new file mode 100644 index 000000000..c389887ee --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/multi-project/parent/src/main/scala/Foo.scala @@ -0,0 +1 @@ +class Foo diff --git a/scripted-test/src/sbt-test/lmScriptedTest/multi-project/test b/scripted-test/src/sbt-test/lmScriptedTest/multi-project/test new file mode 100755 index 000000000..5df2af1f3 --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/multi-project/test @@ -0,0 +1 @@ +> compile diff --git a/scripted-test/src/sbt-test/lmScriptedTest/sbt-plugins/Main.scala b/scripted-test/src/sbt-test/lmScriptedTest/sbt-plugins/Main.scala new file mode 100644 index 000000000..b548d6a7b --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/sbt-plugins/Main.scala @@ -0,0 +1,6 @@ + +object Main { + + println("hello, world!") + +} diff --git a/scripted-test/src/sbt-test/lmScriptedTest/sbt-plugins/build.sbt b/scripted-test/src/sbt-test/lmScriptedTest/sbt-plugins/build.sbt new file mode 100644 index 000000000..7c0d20685 --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/sbt-plugins/build.sbt @@ -0,0 +1,27 @@ + +{ + def writePluginsSbt(str: String) = { + val pluginsSbt = file(".") / "project" / "plugins.sbt" + if (!pluginsSbt.exists) + IO.write( + pluginsSbt, + s"""$str + |addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.8") + |""".stripMargin + ) + } + val dr = sys.props.get("dependency.resolution") match { + case Some("ivy") => + """dependencyResolution := sbt.librarymanagement.ivy.IvyDependencyResolution(ivyConfiguration.value)""" + case Some("coursier") => + """dependencyResolution := sbt.librarymanagement.coursier.CoursierDependencyResolution(sbt.librarymanagement.coursier.CoursierConfiguration())""" + case _ => sys.error("""|The system property 'dependency.resolution' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) + } + + writePluginsSbt(dr) + addCommandAlias( + "setDependencyResolution", + s"""set $dr""" + ) +} diff --git a/scripted-test/src/sbt-test/lmScriptedTest/sbt-plugins/test b/scripted-test/src/sbt-test/lmScriptedTest/sbt-plugins/test new file mode 100755 index 000000000..15ad081ac --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/sbt-plugins/test @@ -0,0 +1,2 @@ +> reload +> assembly diff --git a/scripted-test/src/sbt-test/lmScriptedTest/simple/build.sbt b/scripted-test/src/sbt-test/lmScriptedTest/simple/build.sbt index 6fc1423d0..7b0e47a38 100644 --- a/scripted-test/src/sbt-test/lmScriptedTest/simple/build.sbt +++ b/scripted-test/src/sbt-test/lmScriptedTest/simple/build.sbt @@ -1,8 +1,19 @@ -addCommandAlias("setDependencyResolution", sys.props.get("dependency.resolution") match { - case Some(x) => x +sys.props.get("dependency.resolution") match { + case Some("ivy") => + addCommandAlias( + "setDependencyResolution", + """set dependencyResolution := sbt.librarymanagement.ivy.IvyDependencyResolution(ivyConfiguration.value)""" + ) + case Some("coursier") => + addCommandAlias( + "setDependencyResolution", + """set dependencyResolution := sbt.librarymanagement.coursier.CoursierDependencyResolution(sbt.librarymanagement.coursier.CoursierConfiguration())""" + ) case _ => sys.error("""|The system property 'dependency.resolution' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) -}) +} -libraryDependencies += "com.typesafe" % "config" % "1.3.2" +libraryDependencies ++= Seq( + "com.typesafe" % "config" % "1.3.3" +) diff --git a/scripted-test/src/sbt-test/lmScriptedTest/simple/test b/scripted-test/src/sbt-test/lmScriptedTest/simple/test index d0005883e..5df2af1f3 100755 --- a/scripted-test/src/sbt-test/lmScriptedTest/simple/test +++ b/scripted-test/src/sbt-test/lmScriptedTest/simple/test @@ -1,5 +1 @@ -> sbtVersion -> setDependencyResolution -> show ThisBuild/dependencyResolution -> clean > compile diff --git a/scripted-test/src/sbt-test/lmScriptedTest/use-sbt-in-meta/build.sbt b/scripted-test/src/sbt-test/lmScriptedTest/use-sbt-in-meta/build.sbt new file mode 100644 index 000000000..d200f6743 --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/use-sbt-in-meta/build.sbt @@ -0,0 +1,25 @@ + +{ + def writePluginsSbt(str: String) = { + val pluginsSbt = file(".") / "project" / "plugins.sbt" + if (!pluginsSbt.exists) + IO.write( + pluginsSbt, + str + ) + } + val dr = sys.props.get("dependency.resolution") match { + case Some("ivy") => + """dependencyResolution := sbt.librarymanagement.ivy.IvyDependencyResolution(ivyConfiguration.value)""" + case Some("coursier") => + """dependencyResolution := sbt.librarymanagement.coursier.CoursierDependencyResolution(sbt.librarymanagement.coursier.CoursierConfiguration())""" + case _ => sys.error("""|The system property 'dependency.resolution' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) + } + + writePluginsSbt(dr) + addCommandAlias( + "setDependencyResolution", + s"""set $dr""" + ) +} diff --git a/scripted-test/src/sbt-test/lmScriptedTest/use-sbt-in-meta/project/Dependencies.scala b/scripted-test/src/sbt-test/lmScriptedTest/use-sbt-in-meta/project/Dependencies.scala new file mode 100644 index 000000000..587f8ac76 --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/use-sbt-in-meta/project/Dependencies.scala @@ -0,0 +1,6 @@ +import sbt._ +import Keys._ + +object Dependencies { + +} diff --git a/scripted-test/src/sbt-test/lmScriptedTest/use-sbt-in-meta/test b/scripted-test/src/sbt-test/lmScriptedTest/use-sbt-in-meta/test new file mode 100755 index 000000000..a0f164c64 --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/use-sbt-in-meta/test @@ -0,0 +1 @@ +> reload diff --git a/scripted-test/src/sbt-test/lmScriptedTest/with-trasnsitive/Main.scala b/scripted-test/src/sbt-test/lmScriptedTest/with-trasnsitive/Main.scala new file mode 100644 index 000000000..5398da2af --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/with-trasnsitive/Main.scala @@ -0,0 +1,14 @@ + +object Main { + + import akka.actor._ + + val system = ActorSystem() + + system.terminate() + + import com.typesafe.config.ConfigFactory + + val x = ConfigFactory.load() + +} diff --git a/scripted-test/src/sbt-test/lmScriptedTest/with-trasnsitive/build.sbt b/scripted-test/src/sbt-test/lmScriptedTest/with-trasnsitive/build.sbt new file mode 100644 index 000000000..460935447 --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/with-trasnsitive/build.sbt @@ -0,0 +1,19 @@ + +sys.props.get("dependency.resolution") match { + case Some("ivy") => + addCommandAlias( + "setDependencyResolution", + """set dependencyResolution := sbt.librarymanagement.ivy.IvyDependencyResolution(ivyConfiguration.value)""" + ) + case Some("coursier") => + addCommandAlias( + "setDependencyResolution", + """set dependencyResolution := sbt.librarymanagement.coursier.CoursierDependencyResolution(sbt.librarymanagement.coursier.CoursierConfiguration())""" + ) + case _ => sys.error("""|The system property 'dependency.resolution' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +} + +libraryDependencies ++= Seq( + "com.typesafe.akka" %% "akka-actor" % "2.5.17" +) diff --git a/scripted-test/src/sbt-test/lmScriptedTest/with-trasnsitive/test b/scripted-test/src/sbt-test/lmScriptedTest/with-trasnsitive/test new file mode 100755 index 000000000..5df2af1f3 --- /dev/null +++ b/scripted-test/src/sbt-test/lmScriptedTest/with-trasnsitive/test @@ -0,0 +1 @@ +> compile