From 842de76ca6570855dc7e8380eedcfced438796c0 Mon Sep 17 00:00:00 2001 From: Alexandre Archambault Date: Wed, 30 Dec 2015 01:34:34 +0100 Subject: [PATCH] Add SBT plugin --- build.sbt | 17 +- .../main/scala/coursier/cli/TermDisplay.scala | 9 +- .../coursier/core/compatibility/package.scala | 2 + .../scala/coursier/core/Definitions.scala | 4 + .../src/main/scala/coursier/core/Orders.scala | 49 ++-- .../main/scala/coursier/core/Resolution.scala | 7 +- .../scala/coursier/ivy/IvyRepository.scala | 40 ++- .../src/main/scala/coursier/ivy/IvyXml.scala | 8 +- .../main/scala/coursier/CoursierPlugin.scala | 257 ++++++++++++++++++ plugin/src/main/scala/coursier/FromSbt.scala | 132 +++++++++ .../coursier/InterProjectRepository.scala | 50 ++++ plugin/src/main/scala/coursier/Keys.scala | 18 ++ .../src/main/scala/coursier/Structure.scala | 40 +++ plugin/src/main/scala/coursier/Tasks.scala | 69 +++++ plugin/src/main/scala/coursier/ToSbt.scala | 18 ++ 15 files changed, 672 insertions(+), 48 deletions(-) create mode 100644 plugin/src/main/scala/coursier/CoursierPlugin.scala create mode 100644 plugin/src/main/scala/coursier/FromSbt.scala create mode 100644 plugin/src/main/scala/coursier/InterProjectRepository.scala create mode 100644 plugin/src/main/scala/coursier/Keys.scala create mode 100644 plugin/src/main/scala/coursier/Structure.scala create mode 100644 plugin/src/main/scala/coursier/Tasks.scala create mode 100644 plugin/src/main/scala/coursier/ToSbt.scala diff --git a/build.sbt b/build.sbt index c1a5a3f99..5cdc80b07 100644 --- a/build.sbt +++ b/build.sbt @@ -72,7 +72,6 @@ lazy val commonSettings = baseCommonSettings ++ Seq( } ) - lazy val core = crossProject .settings(commonSettings: _*) .settings(publishingSettings: _*) @@ -163,7 +162,7 @@ lazy val bootstrap = project lazy val cli = project .dependsOn(coreJvm, files) .settings(commonSettings) - .settings(noPublishSettings) + .settings(publishingSettings) .settings(packAutoSettings) .settings( name := "coursier-cli", @@ -171,10 +170,7 @@ lazy val cli = project "com.github.alexarchambault" %% "case-app" % "1.0.0-SNAPSHOT", "com.lihaoyi" %% "ammonite-terminal" % "0.5.0", "ch.qos.logback" % "logback-classic" % "1.1.3" - ), - resourceGenerators in Compile += packageBin.in(bootstrap).in(Compile).map { jar => - Seq(jar) - }.taskValue + ) ) lazy val web = project @@ -208,6 +204,15 @@ lazy val web = project ) ) +// Don't try to compile that if you're not in 2.10 +lazy val plugin = project + .dependsOn(coreJvm, files, cli) + .settings(baseCommonSettings) + .settings( + name := "coursier-sbt-plugin", + sbtPlugin := true + ) + lazy val `coursier` = project.in(file(".")) .aggregate(coreJvm, coreJs, `fetch-js`, testsJvm, testsJs, files, bootstrap, cli, web) .settings(commonSettings) diff --git a/cli/src/main/scala/coursier/cli/TermDisplay.scala b/cli/src/main/scala/coursier/cli/TermDisplay.scala index d973da446..dee09ea6c 100644 --- a/cli/src/main/scala/coursier/cli/TermDisplay.scala +++ b/cli/src/main/scala/coursier/cli/TermDisplay.scala @@ -26,11 +26,6 @@ class TermDisplay(out: Writer) extends Logger { case Some(Right(())) => // update display - for (_ <- 0 until lineCount) { - ansi.up(1) - ansi.clearLine(2) - } - val downloads0 = downloads.synchronized { downloads .toVector @@ -71,9 +66,13 @@ class TermDisplay(out: Writer) extends Logger { } else (url, extra) + ansi.clearLine(2) out.write(s"$url0 $extra0\n") } + for (_ <- downloads0.indices) + ansi.up(1) + out.flush() Thread.sleep(refreshInterval) helper(downloads0.length) diff --git a/core/jvm/src/main/scala/coursier/core/compatibility/package.scala b/core/jvm/src/main/scala/coursier/core/compatibility/package.scala index b4874ba94..1b398bcab 100644 --- a/core/jvm/src/main/scala/coursier/core/compatibility/package.scala +++ b/core/jvm/src/main/scala/coursier/core/compatibility/package.scala @@ -36,6 +36,8 @@ package object compatibility { def isText = node match { case _: scala.xml.Text => true; case _ => false } def textContent = node.text def isElement = node match { case _: scala.xml.Elem => true; case _ => false } + + override def toString = node.toString } parse.right diff --git a/core/shared/src/main/scala/coursier/core/Definitions.scala b/core/shared/src/main/scala/coursier/core/Definitions.scala index 82db9ae31..8576d835c 100644 --- a/core/shared/src/main/scala/coursier/core/Definitions.scala +++ b/core/shared/src/main/scala/coursier/core/Definitions.scala @@ -70,6 +70,10 @@ case class Project( publications: Seq[(String, Publication)] ) { def moduleVersion = (module, version) + + /** All configurations that each configuration extends, including the ones it extends transitively */ + lazy val allConfigurations: Map[String, Set[String]] = + Orders.allConfigurations(configurations) } // Maven-specific diff --git a/core/shared/src/main/scala/coursier/core/Orders.scala b/core/shared/src/main/scala/coursier/core/Orders.scala index 2952604df..d2bd8cdb7 100644 --- a/core/shared/src/main/scala/coursier/core/Orders.scala +++ b/core/shared/src/main/scala/coursier/core/Orders.scala @@ -8,34 +8,39 @@ object Orders { .exists(_ <= 0) } + /** All configurations that each configuration extends, including the ones it extends transitively */ + def allConfigurations(configurations: Map[String, Seq[String]]): Map[String, Set[String]] = { + def allParents(config: String): Set[String] = { + def helper(configs: Set[String], acc: Set[String]): Set[String] = + if (configs.isEmpty) + acc + else if (configs.exists(acc)) + helper(configs -- acc, acc) + else if (configs.exists(!configurations.contains(_))) { + val (remaining, notFound) = configs.partition(configurations.contains) + helper(remaining, acc ++ notFound) + } else { + val extraConfigs = configs.flatMap(configurations) + helper(extraConfigs, acc ++ configs) + } + + helper(Set(config), Set.empty) + } + + configurations + .keys + .toList + .map(config => config -> (allParents(config) - config)) + .toMap + } + /** * Only relations: * Compile < Runtime < Test */ def configurationPartialOrder(configurations: Map[String, Seq[String]]): PartialOrdering[String] = new PartialOrdering[String] { - def allParents(config: String): Set[String] = { - def helper(configs: Set[String], acc: Set[String]): Set[String] = - if (configs.isEmpty) - acc - else if (configs.exists(acc)) - helper(configs -- acc, acc) - else if (configs.exists(!configurations.contains(_))) { - val (remaining, notFound) = configs.partition(configurations.contains) - helper(remaining, acc ++ notFound) - } else { - val extraConfigs = configs.flatMap(configurations) - helper(extraConfigs, acc ++ configs) - } - - helper(Set(config), Set.empty) - } - - val allParentsMap = configurations - .keys - .toList - .map(config => config -> (allParents(config) - config)) - .toMap + val allParentsMap = allConfigurations(configurations) def tryCompare(x: String, y: String) = if (x == y) diff --git a/core/shared/src/main/scala/coursier/core/Resolution.scala b/core/shared/src/main/scala/coursier/core/Resolution.scala index f9eb25759..f2e240bfd 100644 --- a/core/shared/src/main/scala/coursier/core/Resolution.scala +++ b/core/shared/src/main/scala/coursier/core/Resolution.scala @@ -777,10 +777,13 @@ case class Resolution( def part(dependencies: Set[Dependency]): Resolution = { val (_, _, finalVersions) = nextDependenciesAndConflicts + def updateVersion(dep: Dependency): Dependency = + dep.copy(version = finalVersions.getOrElse(dep.module, dep.version)) + @tailrec def helper(current: Set[Dependency]): Set[Dependency] = { val newDeps = current ++ current .flatMap(finalDependencies0) - .map(dep => dep.copy(version = finalVersions.getOrElse(dep.module, dep.version))) + .map(updateVersion) val anyNewDep = (newDeps -- current).nonEmpty @@ -792,7 +795,7 @@ case class Resolution( copy( rootDependencies = dependencies, - dependencies = helper(dependencies) + dependencies = helper(dependencies.map(updateVersion)) // don't know if something should be done about conflicts ) } diff --git a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala index 0b496a5c0..ac24d534a 100644 --- a/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala +++ b/core/shared/src/main/scala/coursier/ivy/IvyRepository.scala @@ -9,8 +9,9 @@ import java.util.regex.Pattern.quote object IvyRepository { - val optionalPartRegex = (quote("(") + "[^" + quote("()") + "]*" + quote(")")).r - val variableRegex = (quote("[") + "[^" + quote("[()]") + "]*" + quote("]")).r + val optionalPartRegex = (quote("(") + "[^" + quote("{()}") + "]*" + quote(")")).r + val variableRegex = (quote("[") + "[^" + quote("{[()]}") + "]*" + quote("]")).r + val propertyRegex = (quote("${") + "[^" + quote("{[()]}") + "]*" + quote("}")).r sealed abstract class PatternPart(val effectiveStart: Int, val effectiveEnd: Int) extends Product with Serializable { require(effectiveStart <= effectiveEnd) @@ -67,19 +68,32 @@ object IvyRepository { } } + def substituteProperties(s: String, properties: Map[String, String]): String = + propertyRegex.findAllMatchIn(s).toVector.foldRight(s) { case (m, s0) => + val key = s0.substring(m.start + "${".length, m.end - "}".length) + val value = properties.getOrElse(key, "") + s0.take(m.start) + value + s0.drop(m.end) + } + } -case class IvyRepository(pattern: String, changing: Option[Boolean] = None) extends Repository { +case class IvyRepository( + pattern: String, + changing: Option[Boolean] = None, + properties: Map[String, String] = Map.empty +) extends Repository { import Repository._ import IvyRepository._ + private val pattern0 = substituteProperties(pattern, properties) + val parts = { - val optionalParts = optionalPartRegex.findAllMatchIn(pattern).toList.map { m => + val optionalParts = optionalPartRegex.findAllMatchIn(pattern0).toList.map { m => PatternPart.Optional(m.start, m.end) } - val len = pattern.length + val len = pattern0.length @tailrec def helper( @@ -105,16 +119,16 @@ case class IvyRepository(pattern: String, changing: Option[Boolean] = None) exte helper(0, optionalParts, Nil) } - assert(pattern.isEmpty == parts.isEmpty) - if (pattern.nonEmpty) { + assert(pattern0.isEmpty == parts.isEmpty) + if (pattern0.nonEmpty) { for ((a, b) <- parts.zip(parts.tail)) assert(a.end == b.start) assert(parts.head.start == 0) - assert(parts.last.end == pattern.length) + assert(parts.last.end == pattern0.length) } private val substituteHelpers = parts.map { part => - part(pattern.substring(part.effectiveStart, part.effectiveEnd)) + part(pattern0.substring(part.effectiveStart, part.effectiveEnd)) } def substitute(variables: Map[String, String]): String \/ String = @@ -154,7 +168,13 @@ case class IvyRepository(pattern: String, changing: Option[Boolean] = None) exte def artifacts(dependency: Dependency, project: Project) = project .publications - .collect { case (conf, p) if conf == "*" || conf == dependency.configuration => p } + .collect { + case (conf, p) + if conf == "*" || + conf == dependency.configuration || + project.allConfigurations.getOrElse(dependency.configuration, Set.empty).contains(conf) => + p + } .flatMap { p => substitute(variables( dependency.module.organization, diff --git a/core/shared/src/main/scala/coursier/ivy/IvyXml.scala b/core/shared/src/main/scala/coursier/ivy/IvyXml.scala index 617c7f796..bc3d52ebb 100644 --- a/core/shared/src/main/scala/coursier/ivy/IvyXml.scala +++ b/core/shared/src/main/scala/coursier/ivy/IvyXml.scala @@ -66,7 +66,7 @@ object IvyXml { private def publications(node: Node): Map[String, Seq[Publication]] = node.children .filter(_.label == "artifact") - .flatMap { node0 => + .flatMap { node => val name = node.attribute("name").getOrElse("") val type0 = node.attribute("type").getOrElse("jar") val ext = node.attribute("ext").getOrElse(type0) @@ -116,12 +116,14 @@ object IvyXml { if (publicationsOpt.isEmpty) // no publications node -> default JAR artifact Seq("*" -> Publication(module.name, "jar", "jar")) - else + else { // publications node is there -> only its content (if it is empty, no artifacts, // as per the Ivy manual) + val inAllConfs = publicationsOpt.flatMap(_.get("*")).getOrElse(Nil) configurations0.flatMap { case (conf, _) => - publicationsOpt.flatMap(_.get(conf)).getOrElse(Nil).map(conf -> _) + (publicationsOpt.flatMap(_.get(conf)).getOrElse(Nil) ++ inAllConfs).map(conf -> _) } + } ) } \ No newline at end of file diff --git a/plugin/src/main/scala/coursier/CoursierPlugin.scala b/plugin/src/main/scala/coursier/CoursierPlugin.scala new file mode 100644 index 000000000..c55f64ace --- /dev/null +++ b/plugin/src/main/scala/coursier/CoursierPlugin.scala @@ -0,0 +1,257 @@ +package coursier + +import java.io.{ File, OutputStreamWriter } + +import coursier.cli.TermDisplay +import sbt.{ MavenRepository => _, _ } +import sbt.Keys._ + +import scalaz.{ -\/, \/- } +import scalaz.concurrent.Task + +object CoursierPlugin extends AutoPlugin { + + override def trigger = allRequirements + + override def requires = sbt.plugins.IvyPlugin + + private def errPrintln(s: String): Unit = scala.Console.err.println(s) + + object autoImport { + val coursierParallelDownloads = Keys.coursierParallelDownloads + val coursierMaxIterations = Keys.coursierMaxIterations + val coursierChecksums = Keys.coursierChecksums + val coursierCachePolicy = Keys.coursierCachePolicy + val coursierResolvers = Keys.coursierResolvers + val coursierCache = Keys.coursierCache + val coursierProject = Keys.coursierProject + val coursierProjects = Keys.coursierProjects + } + + import autoImport._ + + + private val ivyProperties = Map( + "ivy.home" -> s"${sys.props("user.home")}/.ivy2" + ) ++ sys.props + + private def createLogger() = Some { + if (sys.env.get("COURSIER_NO_TERM").nonEmpty) + new coursier.Files.Logger { + override def downloadingArtifact(url: String, file: File): Unit = { + println(s"$url\n -> $file") + } + override def downloadedArtifact(url: String, success: Boolean): Unit = { + println(s"$url: ${if (success) "Success" else "Failed"}") + } + def init() = {} + } + else + new TermDisplay(new OutputStreamWriter(System.err)) + } + + + private def task = Def.task { + // let's update only one module at once, for a better output + // Downloads are already parallel, no need to parallelize further anyway + synchronized { + + val (currentProject, _) = coursierProject.value + val projects = coursierProjects.value + + val parallelDownloads = coursierParallelDownloads.value + val checksums = coursierChecksums.value + val maxIterations = coursierMaxIterations.value + val cachePolicy = coursierCachePolicy.value + val cacheDir = coursierCache.value + + val resolvers = coursierResolvers.value + + + val startRes = Resolution( + currentProject.dependencies.map { case (_, dep) => dep }.toSet, + filter = Some(dep => !dep.optional), + forceVersions = projects.map { case (proj, _) => proj.moduleVersion }.toMap + ) + + val interProjectRepo = InterProjectRepository(projects) + val repositories = interProjectRepo +: resolvers.flatMap(FromSbt.repository(_, ivyProperties)) + + val files = Files( + Seq("http://" -> new File(cacheDir, "http"), "https://" -> new File(cacheDir, "https")), + () => ???, + concurrentDownloadCount = parallelDownloads + ) + + val logger = createLogger() + logger.foreach(_.init()) + val fetch = coursier.Fetch( + repositories, + files.fetch(checksums = checksums, logger = logger)(cachePolicy = CachePolicy.LocalOnly), + files.fetch(checksums = checksums, logger = logger)(cachePolicy = cachePolicy) + ) + + def depsRepr = currentProject.dependencies.map { case (config, dep) => + s"${dep.module}:${dep.version}:$config->${dep.configuration}" + }.sorted + + errPrintln(s"Resolving ${currentProject.module.organization}:${currentProject.module.name}:${currentProject.version}") + for (depRepr <- depsRepr) + errPrintln(s" $depRepr") + + val res = startRes + .process + .run(fetch, maxIterations) + .attemptRun + .leftMap(ex => throw new Exception(s"Exception during resolution", ex)) + .merge + + if (!res.isDone) + throw new Exception(s"Maximum number of iteration reached!") + + errPrintln("Resolution done") + + def repr(dep: Dependency) = { + // dep.version can be an interval, whereas the one from project can't + val version = res + .projectCache + .get(dep.moduleVersion) + .map(_._2.version) + .getOrElse(dep.version) + val extra = + if (version == dep.version) "" + else s" ($version for ${dep.version})" + + ( + Seq( + dep.module.organization, + dep.module.name, + dep.attributes.`type` + ) ++ + Some(dep.attributes.classifier) + .filter(_.nonEmpty) + .toSeq ++ + Seq( + version + ) + ).mkString(":") + extra + } + + if (res.conflicts.nonEmpty) { + // Needs test + println(s"${res.conflicts.size} conflict(s):\n ${res.conflicts.toList.map(repr).sorted.mkString(" \n")}") + } + + val errors = res.errors + if (errors.nonEmpty) { + println(s"\n${errors.size} error(s):") + for ((dep, errs) <- errors) { + println(s" ${dep.module}:${dep.version}:\n${errs.map(" " + _.replace("\n", " \n")).mkString("\n")}") + } + } + + val trDepsWithArtifactsTasks = res.artifacts + .toVector + .map { a => + files.file(a, checksums = checksums, logger = logger)(cachePolicy = cachePolicy).run.map((a, _)) + } + + errPrintln(s"Fetching artifacts") + // rename + val trDepsWithArtifacts = Task.gatherUnordered(trDepsWithArtifactsTasks).attemptRun match { + case -\/(ex) => + throw new Exception(s"Error while downloading / verifying artifacts", ex) + case \/-(l) => l.toMap + } + errPrintln(s"Fetching artifacts: done") + + val configs = ivyConfigurations.value.map(c => c.name -> c.extendsConfigs.map(_.name)).toMap + def allExtends(c: String) = { + // possibly bad complexity + def helper(current: Set[String]): Set[String] = { + val newSet = current ++ current.flatMap(configs.getOrElse(_, Nil)) + if ((newSet -- current).nonEmpty) + helper(newSet) + else + newSet + } + + helper(Set(c)) + } + + val depsByConfig = currentProject + .dependencies + .groupBy { case (c, _) => c } + .map { case (c, l) => + c -> l.map { case (_, d) => d } + } + + val sbtModuleReportsPerScope = configs.map { case (c, _) => c -> { + val a = allExtends(c).flatMap(depsByConfig.getOrElse(_, Nil)) + res.part(a) + .dependencyArtifacts + .groupBy { case (dep, _) => dep } + .map { case (dep, l) => dep -> l.map { case (_, a) => a } } + .map { case (dep, artifacts) => + val fe = artifacts.map { a => + a -> trDepsWithArtifacts.getOrElse(a, -\/("Not downloaded")) + } + new ModuleReport( + ModuleID(dep.module.organization, dep.module.name, dep.version, configurations = Some(dep.configuration)), + fe.collect { case (artifact, \/-(file)) => + if (file.toString.contains("file:/")) + throw new Exception(s"Wrong path: $file") + ToSbt.artifact(dep.module, artifact) -> file + }, + fe.collect { case (artifact, -\/(e)) => + errPrintln(s"${artifact.url}: $e") + ToSbt.artifact(dep.module, artifact) + }, + None, + None, + None, + None, + false, + None, + None, + None, + None, + Map.empty, + None, + None, + Nil, + Nil, + Nil + ) + } + }} + + new UpdateReport( + null, + sbtModuleReportsPerScope.toVector.map { case (c, r) => + new ConfigurationReport( + c, + r.toVector, + Nil, + Nil + ) + }, + new UpdateStats(-1L, -1L, -1L, cached = false), + Map.empty + ) + } + } + + override lazy val projectSettings = Seq( + coursierParallelDownloads := 6, + coursierMaxIterations := 50, + coursierChecksums := Seq(Some("SHA-1"), Some("MD5")), + coursierCachePolicy := CachePolicy.FetchMissing, + coursierResolvers <<= Tasks.coursierResolversTask, + coursierCache := new File(sys.props("user.home") + "/.coursier/sbt"), + update <<= task, + coursierProject <<= Tasks.coursierProjectTask, + coursierProjects <<= Tasks.coursierProjectsTask + ) + +} diff --git a/plugin/src/main/scala/coursier/FromSbt.scala b/plugin/src/main/scala/coursier/FromSbt.scala new file mode 100644 index 000000000..b7ea73e44 --- /dev/null +++ b/plugin/src/main/scala/coursier/FromSbt.scala @@ -0,0 +1,132 @@ +package coursier + +import coursier.ivy.IvyRepository +import sbt.{ Resolver, CrossVersion, ModuleID } + +object FromSbt { + + def sbtModuleIdName( + moduleId: ModuleID, + scalaVersion: => String, + scalaBinaryVersion: => String + ): String = moduleId.crossVersion match { + case CrossVersion.Disabled => moduleId.name + case f: CrossVersion.Full => moduleId.name + "_" + f.remapVersion(scalaVersion) + case f: CrossVersion.Binary => moduleId.name + "_" + f.remapVersion(scalaBinaryVersion) + } + + def mappings(mapping: String): Seq[(String, String)] = + mapping.split(';').flatMap { m => + val (froms, tos) = m.split("->", 2) match { + case Array(from) => (from, "default(compile)") + case Array(from, to) => (from, to) + } + + for { + from <- froms.split(',') + to <- tos.split(',') + } yield (from, to) + } + + def dependencies( + module: ModuleID, + scalaVersion: String, + scalaBinaryVersion: String + ): Seq[(String, Dependency)] = { + + // TODO Warn about unsupported properties in `module` + + val fullName = sbtModuleIdName(module, scalaVersion, scalaBinaryVersion) + + val dep = Dependency( + Module(module.organization, fullName), + module.revision, + exclusions = module.exclusions.map { rule => + // FIXME Other `rule` fields are ignored here + (rule.organization, rule.name) + }.toSet + ) + + val mapping = module.configurations.getOrElse("compile") + val allMappings = mappings(mapping) + + val attributes = + if (module.explicitArtifacts.isEmpty) + Seq(Attributes()) + else + module.explicitArtifacts.map { a => + Attributes(`type` = a.extension, classifier = a.classifier.getOrElse("")) + } + + for { + (from, to) <- allMappings.toSeq + attr <- attributes + } yield from -> dep.copy(configuration = to, attributes = attr) + } + + def project( + projectID: ModuleID, + allDependencies: Seq[ModuleID], + ivyConfigurations: Map[String, Seq[String]], + scalaVersion: String, + scalaBinaryVersion: String + ): Project = { + + // FIXME Ignored for now + // val sbtDepOverrides = dependencyOverrides.value + // val sbtExclusions = excludeDependencies.value + + val deps = allDependencies.flatMap(dependencies(_, scalaVersion, scalaBinaryVersion)) + + Project( + Module(projectID.organization, sbtModuleIdName(projectID, scalaVersion, scalaBinaryVersion)), + projectID.revision, + deps, + ivyConfigurations, + None, + Nil, + Map.empty, + Nil, + None, + None, + Nil + ) + } + + def repository(resolver: Resolver, ivyProperties: Map[String, String]): Option[Repository] = + resolver match { + case sbt.MavenRepository(_, root) => + if (root.startsWith("http://") || root.startsWith("https://")) { + val root0 = if (root.endsWith("/")) root else root + "/" + Some(MavenRepository(root0)) + } else { + Console.err.println(s"Warning: unrecognized Maven repository protocol in $root, ignoring it") + None + } + + case sbt.FileRepository(_, _, patterns) + if patterns.ivyPatterns.lengthCompare(1) == 0 && + patterns.ivyPatterns == patterns.artifactPatterns => + + Some(IvyRepository( + "file://" + patterns.ivyPatterns.head, + changing = Some(true), + properties = ivyProperties + )) + + case sbt.URLRepository(_, patterns) + if patterns.ivyPatterns.lengthCompare(1) == 0 && + patterns.ivyPatterns == patterns.artifactPatterns => + + Some(IvyRepository( + patterns.ivyPatterns.head, + changing = None, + properties = ivyProperties + )) + + case other => + Console.err.println(s"Warning: unrecognized repository ${other.name}, ignoring it") + None + } + +} diff --git a/plugin/src/main/scala/coursier/InterProjectRepository.scala b/plugin/src/main/scala/coursier/InterProjectRepository.scala new file mode 100644 index 000000000..18233c8fe --- /dev/null +++ b/plugin/src/main/scala/coursier/InterProjectRepository.scala @@ -0,0 +1,50 @@ +package coursier + +import scalaz.{ -\/, \/-, Monad, EitherT } + +case class InterProjectSource(artifacts: Map[(Module, String), Map[String, Seq[Artifact]]]) extends Artifact.Source { + def artifacts(dependency: Dependency, project: Project): Seq[Artifact] = + artifacts + .get(dependency.moduleVersion) + .toSeq + .flatMap(_.get(dependency.configuration)) + .flatten +} + +case class InterProjectRepository(projects: Seq[(Project, Seq[(String, Seq[Artifact])])]) extends Repository { + + Console.err.println("InterProjectRepository") + for ((p, _) <- projects) + Console.err.println(s" ${p.module}:${p.version}") + + private val map = projects + .map { case (proj, a) => proj.moduleVersion -> proj } + .toMap + + val source = InterProjectSource( + projects.map { case (proj, a) => + val artifacts = a.toMap + val allArtifacts = proj.allConfigurations.map { case (c, ext) => + c -> ext.toSeq.flatMap(artifacts.getOrElse(_, Nil)) + } + proj.moduleVersion -> allArtifacts + }.toMap + ) + + def find[F[_]]( + module: Module, + version: String, + fetch: Fetch.Content[F] + )(implicit + F: Monad[F] + ): EitherT[F, String, (Artifact.Source, Project)] = { + val res = map.get((module, version)) match { + case Some(proj) => + \/-((source, proj)) + case None => + -\/(s"Project not found: $module:$version") + } + + EitherT(F.point(res)) + } +} \ No newline at end of file diff --git a/plugin/src/main/scala/coursier/Keys.scala b/plugin/src/main/scala/coursier/Keys.scala new file mode 100644 index 000000000..cf6c56990 --- /dev/null +++ b/plugin/src/main/scala/coursier/Keys.scala @@ -0,0 +1,18 @@ +package coursier + +import java.io.File +import sbt.{ Resolver, SettingKey, TaskKey } + +object Keys { + val coursierParallelDownloads = SettingKey[Int]("coursier-parallel-downloads", "") // 6 + val coursierMaxIterations = SettingKey[Int]("coursier-max-iterations", "") // 50 + val coursierChecksums = SettingKey[Seq[Option[String]]]("coursier-checksums", "") //Seq(Some("SHA-1"), Some("MD5")) + val coursierCachePolicy = SettingKey[CachePolicy]("coursier-cache-policy", "") // = CachePolicy.FetchMissing + + val coursierResolvers = TaskKey[Seq[Resolver]]("coursier-resolvers", "") + + val coursierCache = SettingKey[File]("coursier-cache", "") + + val coursierProject = TaskKey[(Project, Seq[(String, Seq[Artifact])])]("coursier-project", "") + val coursierProjects = TaskKey[Seq[(Project, Seq[(String, Seq[Artifact])])]]("coursier-projects", "") +} diff --git a/plugin/src/main/scala/coursier/Structure.scala b/plugin/src/main/scala/coursier/Structure.scala new file mode 100644 index 000000000..c9e5ea7e3 --- /dev/null +++ b/plugin/src/main/scala/coursier/Structure.scala @@ -0,0 +1,40 @@ +package coursier + +import sbt._ + +// things from sbt-structure +object Structure { + import Def.Initialize._ + + def structure(state: State): Load.BuildStructure = + sbt.Project.structure(state) + + implicit def `enrich SettingKey`[T](key: SettingKey[T]) = new { + def find(state: State): Option[T] = + key.get(structure(state).data) + + def get(state: State): T = + find(state).get + + def getOrElse(state: State, default: => T): T = + find(state).getOrElse(default) + } + + implicit def `enrich TaskKey`[T](key: TaskKey[T]) = new { + def find(state: State): Option[sbt.Task[T]] = + key.get(structure(state).data) + + def get(state: State): sbt.Task[T] = + find(state).get + + def forAllProjects(state: State, projects: Seq[ProjectRef]): sbt.Task[Map[ProjectRef, T]] = { + val tasks = projects.flatMap(p => key.in(p).get(structure(state).data).map(_.map(it => (p, it)))) + std.TaskExtra.joinTasks(tasks).join.map(_.toMap) + } + + def forAllConfigurations(state: State, configurations: Seq[sbt.Configuration]): sbt.Task[Map[sbt.Configuration, T]] = { + val tasks = configurations.flatMap(c => key.in(c).get(structure(state).data).map(_.map(it => (c, it)))) + std.TaskExtra.joinTasks(tasks).join.map(_.toMap) + } + } +} diff --git a/plugin/src/main/scala/coursier/Tasks.scala b/plugin/src/main/scala/coursier/Tasks.scala new file mode 100644 index 000000000..130af086b --- /dev/null +++ b/plugin/src/main/scala/coursier/Tasks.scala @@ -0,0 +1,69 @@ +package coursier + +import sbt.{Classpaths, Resolver, Def} +import Structure._ +import Keys._ +import sbt.Keys._ + +object Tasks { + + def coursierResolversTask: Def.Initialize[sbt.Task[Seq[Resolver]]] = Def.task { + var l = externalResolvers.value + if (sbtPlugin.value) + l = Seq( + sbtResolver.value, + Classpaths.sbtPluginReleases + ) ++ l + l + } + + def coursierProjectTask: Def.Initialize[sbt.Task[(Project, Seq[(String, Seq[Artifact])])]] = + ( + sbt.Keys.state, + sbt.Keys.thisProjectRef + ).flatMap { (state, projectRef) => + + // should projectID.configurations be used instead? + val configurations = ivyConfigurations.in(projectRef).get(state) + + // exportedProducts looks like what we want, but depends on the update task, which + // make the whole thing run into cycles... + val artifacts = configurations.map { cfg => + cfg.name -> Option(classDirectory.in(projectRef).in(cfg).getOrElse(state, null)) + }.collect { case (name, Some(classDir)) => + name -> Seq( + Artifact( + classDir.toURI.toString, + Map.empty, + Map.empty, + Attributes(), + changing = true + ) + ) + } + + val allDependenciesTask = allDependencies.in(projectRef).get(state) + + for { + allDependencies <- allDependenciesTask + } yield { + + val proj = FromSbt.project( + projectID.in(projectRef).get(state), + allDependencies, + configurations.map { cfg => cfg.name -> cfg.extendsConfigs.map(_.name) }.toMap, + scalaVersion.in(projectRef).get(state), + scalaBinaryVersion.in(projectRef).get(state) + ) + + (proj, artifacts) + } + } + + def coursierProjectsTask: Def.Initialize[sbt.Task[Seq[(Project, Seq[(String, Seq[Artifact])])]]] = + sbt.Keys.state.flatMap { state => + val projects = structure(state).allProjectRefs + coursierProject.forAllProjects(state, projects).map(_.values.toVector) + } + +} diff --git a/plugin/src/main/scala/coursier/ToSbt.scala b/plugin/src/main/scala/coursier/ToSbt.scala new file mode 100644 index 000000000..6a83eaa78 --- /dev/null +++ b/plugin/src/main/scala/coursier/ToSbt.scala @@ -0,0 +1,18 @@ +package coursier + +import sbt._ + +object ToSbt { + + def artifact(module: Module, artifact: Artifact): sbt.Artifact = + sbt.Artifact( + s"${module.organization}:${module.name}", + artifact.attributes.`type`, + "jar", + Some(artifact.attributes.classifier), + Nil, + Some(url(artifact.url)), + Map.empty + ) + +}