package mill.scalalib
import ammonite.runtime.SpecialClassLoader
import coursier.{Cache, CoursierPaths, Repository}
import mill.define._
import mill.eval.{Evaluator, PathRef, Result}
import mill.api.Ctx.{Home, Log}
import mill.api.Strict.Agg
import mill.api.{Loose, Strict}
import mill.{T, scalalib}
import os.Path
import scala.util.Try
object GenIdea extends ExternalModule {
def idea(ev: Evaluator) = T.command{
mill.scalalib.GenIdeaImpl(
implicitly,
ev.rootModule,
ev.rootModule.millDiscover
)
}
implicit def millScoptEvaluatorReads[T] = new mill.main.EvaluatorScopt[T]()
lazy val millDiscover = Discover[this.type]
}
object GenIdeaImpl {
def apply(ctx: Log with Home,
rootModule: BaseModule,
discover: Discover[_]): Unit = {
val pp = new scala.xml.PrettyPrinter(999, 4)
val jdkInfo = extractCurrentJdk(os.pwd / ".idea" / "misc.xml").getOrElse(("JDK_1_8", "1.8 (1)"))
os.remove.all(os.pwd/".idea"/"libraries")
os.remove.all(os.pwd/".idea"/"scala_compiler.xml")
os.remove.all(os.pwd/".idea_modules")
val evaluator = new Evaluator(ctx.home, os.pwd / 'out, os.pwd / 'out, rootModule, ctx.log)
for((relPath, xml) <- xmlFileLayout(evaluator, rootModule, jdkInfo, Some(ctx))){
os.write.over(os.pwd/relPath, pp.format(xml), createFolders = true)
}
}
def extractCurrentJdk(ideaPath: os.Path): Option[(String,String)] = {
import scala.xml.XML
Try {
val xml = XML.loadFile(ideaPath.toString)
(xml \\ "component")
.filter(x => x.attribute("project-jdk-type").map(_.text).contains("JavaSDK"))
.map { n => (n.attribute("languageLevel"), n.attribute("project-jdk-name")) }
.collectFirst{ case (Some(lang), Some(jdk)) => (lang.text, jdk.text) }
}.getOrElse(None)
}
def xmlFileLayout(evaluator: Evaluator,
rootModule: mill.Module,
jdkInfo: (String,String),
ctx: Option[Log],
fetchMillModules: Boolean = true): Seq[(os.RelPath, scala.xml.Node)] = {
val modules = rootModule.millInternal.segmentsToModules.values
.collect{ case x: scalalib.JavaModule => (x.millModuleSegments, x)}
.toSeq
val buildLibraryPaths =
if (!fetchMillModules) Nil
else sys.props.get("MILL_BUILD_LIBRARIES") match {
case Some(found) => found.split(',').map(os.Path(_)).distinct.toList
case None =>
val repos = modules.foldLeft(Set.empty[Repository]) { _ ++ _._2.repositories }
val artifactNames = Seq("main-moduledefs", "main-api", "main-core", "scalalib", "scalajslib")
val Result.Success(res) = scalalib.Lib.resolveDependencies(
repos.toList,
Lib.depToDependency(_, "2.12.4", ""),
for(name <- artifactNames)
yield ivy"com.lihaoyi::mill-$name:${sys.props("MILL_VERSION")}",
false,
None,
ctx
)
res.items.toList.map(_.path)
}
val buildDepsPaths = Try(evaluator
.rootModule
.getClass
.getClassLoader
.asInstanceOf[SpecialClassLoader]
).map {
_.allJars
.map(url => os.Path(url.getFile))
.filter(_.toIO.exists)
}.getOrElse(Seq())
case class ResolvedModule(
path: Segments,
classpath: Loose.Agg[Path],
module: JavaModule,
pluginClasspath: Loose.Agg[Path],
scalaOptions: Seq[String],
compilerClasspath: Loose.Agg[Path],
libraryClasspath: Loose.Agg[Path]
)
val resolved = for((path, mod) <- modules) yield {
val scalaLibraryIvyDeps = mod match{
case x: ScalaModule => x.scalaLibraryIvyDeps
case _ => T.task{Loose.Agg.empty[Dep]}
}
val allIvyDeps = T.task{mod.transitiveIvyDeps() ++ scalaLibraryIvyDeps() ++ mod.compileIvyDeps()}
val scalaCompilerClasspath = mod match{
case x: ScalaModule => x.scalaCompilerClasspath
case _ => T.task{Loose.Agg.empty[PathRef]}
}
val externalLibraryDependencies = T.task{
mod.resolveDeps(scalaLibraryIvyDeps)()
}
val externalDependencies = T.task{
mod.resolveDeps(allIvyDeps)() ++
Task.traverse(mod.transitiveModuleDeps)(_.unmanagedClasspath)().flatten
}
val externalSources = T.task{
mod.resolveDeps(allIvyDeps, sources = true)()
}
val (scalacPluginsIvyDeps, scalacOptions) = mod match{
case mod: ScalaModule => T.task{mod.scalacPluginIvyDeps()} -> T.task{mod.scalacOptions()}
case _ => T.task(Loose.Agg[Dep]()) -> T.task(Seq())
}
val scalacPluginDependencies = T.task{
mod.resolveDeps(scalacPluginsIvyDeps)()
}
val resolvedCp: Loose.Agg[PathRef] = evalOrElse(evaluator, externalDependencies, Loose.Agg.empty)
val resolvedSrcs: Loose.Agg[PathRef] = evalOrElse(evaluator, externalSources, Loose.Agg.empty)
val resolvedSp: Loose.Agg[PathRef] = evalOrElse(evaluator, scalacPluginDependencies, Loose.Agg.empty)
val resolvedCompilerCp: Loose.Agg[PathRef] = evalOrElse(evaluator, scalaCompilerClasspath, Loose.Agg.empty)
val resolvedLibraryCp: Loose.Agg[PathRef] = evalOrElse(evaluator, externalLibraryDependencies, Loose.Agg.empty)
val scalacOpts: Seq[String] = evalOrElse(evaluator, scalacOptions, Seq())
ResolvedModule(
path,
resolvedCp.map(_.path).filter(_.ext == "jar") ++ resolvedSrcs.map(_.path),
mod,
resolvedSp.map(_.path).filter(_.ext == "jar"),
scalacOpts,
resolvedCompilerCp.map(_.path),
resolvedLibraryCp.map(_.path)
)
}
val moduleLabels = modules.map(_.swap).toMap
val allResolved = resolved.flatMap(_.classpath) ++ buildLibraryPaths ++ buildDepsPaths
val librariesProperties = resolved.flatMap(x => x.libraryClasspath.map(_ -> x.compilerClasspath)).toMap
val commonPrefix =
if (allResolved.isEmpty) 0
else {
val minResolvedLength = allResolved.map(_.segmentCount).min
allResolved.map(_.segments.take(minResolvedLength).toList)
.transpose
.takeWhile(_.distinct.length == 1)
.length
}
// only resort to full long path names if the jar name is a duplicate
val pathShortLibNameDuplicate = allResolved
.distinct
.map{p => p.last -> p}
.groupBy(_._1)
.filter(_._2.size > 1)
.keySet
val pathToLibName = allResolved
.map{p =>
if (pathShortLibNameDuplicate(p.last))
(p, p.segments.drop(commonPrefix).mkString("_"))
else
(p, p.last)
}
.toMap
sealed trait ResolvedLibrary { def path : os.Path }
case class CoursierResolved(path : os.Path, pom : os.Path, sources : Option[os.Path])
extends ResolvedLibrary
case class OtherResolved(path : os.Path) extends ResolvedLibrary
// Tries to group jars with their poms and sources.
def toResolvedJar(path : os.Path) : Option[ResolvedLibrary] = {
val inCoursierCache = path.startsWith(os.Path(CoursierPaths.cacheDirectory()))
val isSource = path.last.endsWith("sources.jar")
val isPom = path.ext == "pom"
if (inCoursierCache && (isSource || isPom)) {
// Remove sources and pom as they'll be recovered from the jar path
None
} else if (inCoursierCache && path.ext == "jar") {
val withoutExt = path.last.dropRight(path.ext.length + 1)
val pom = path / os.up / s"$withoutExt.pom"
val sources = Some(path / os.up / s"$withoutExt-sources.jar")
.filter(_.toIO.exists())
Some(CoursierResolved(path, pom, sources))
} else Some(OtherResolved(path))
}
// Hack so that Intellij does not complain about unresolved magic
// imports in build.sc when in fact they are resolved
def sbtLibraryNameFromPom(pom : os.Path) : String = {
val xml = scala.xml.XML.loadFile(pom.toIO)
val groupId = (xml \ "groupId").text
val artifactId = (xml \ "artifactId").text
val version = (xml \ "version").text
// The scala version here is non incidental
s"SBT: $groupId:$artifactId:$version:jar"
}
def libraryName(resolvedJar: ResolvedLibrary) : String = resolvedJar match {
case CoursierResolved(path, pom, _) if buildDepsPaths.contains(path) =>
sbtLibraryNameFromPom(pom)
case CoursierResolved(path, _, _) =>
pathToLibName(path)
case OtherResolved(path) =>
pathToLibName(path)
}
def resolvedLibraries(resolved : Seq[os.Path]) : Seq[ResolvedLibrary] = resolved
.map(toResolvedJar)
.collect { case Some(r) => r}
val compilerSettings = resolved
.foldLeft(Map[(Loose.Agg[os.Path], Seq[String]), Vector[JavaModule]]()) {
(r, q) =>
val key = (q.pluginClasspath, q.scalaOptions)
r + (key -> (r.getOrElse(key, Vector()) :+ q.module))
}
val allBuildLibraries : Set[ResolvedLibrary] =
resolvedLibraries(buildLibraryPaths ++ buildDepsPaths).toSet
val fixedFiles = Seq(
Tuple2(os.rel/".idea"/"misc.xml", miscXmlTemplate(jdkInfo)),
Tuple2(os.rel/".idea"/"scala_settings.xml", scalaSettingsTemplate()),
Tuple2(
os.rel/".idea"/"modules.xml",
allModulesXmlTemplate(
modules
.filter(!_._2.skipIdea)
.map { case (path, mod) => moduleName(path) }
)
),
Tuple2(
os.rel/".idea_modules"/"mill-build.iml",
rootXmlTemplate(
for(lib <- allBuildLibraries)
yield libraryName(lib)
)
),
Tuple2(
os.rel/".idea"/"scala_compiler.xml",
scalaCompilerTemplate(compilerSettings)
)
)
val libraries = resolvedLibraries(allResolved).map{ resolved =>
import resolved.path
val name = libraryName(resolved)
val sources = resolved match {
case CoursierResolved(_, _, s) => s.map(p => "jar://" + p + "!/")
case OtherResolved(_) => None
}
Tuple2(os.rel/".idea"/'libraries/s"$name.xml", libraryXmlTemplate(name, path, sources, librariesProperties.getOrElse(path, Loose.Agg.empty)))
}
val moduleFiles = resolved.map{ case ResolvedModule(path, resolvedDeps, mod, _, _, _, _) =>
val Seq(
resourcesPathRefs: Seq[PathRef],
sourcesPathRef: Seq[PathRef],
generatedSourcePathRefs: Seq[PathRef],
allSourcesPathRefs: Seq[PathRef]
) = evaluator.evaluate(Agg(mod.resources, mod.sources, mod.generatedSources, mod.allSources)).values
val generatedSourcePaths = generatedSourcePathRefs.map(_.path)
val normalSourcePaths = (allSourcesPathRefs.map(_.path).toSet -- generatedSourcePaths.toSet).toSeq
val paths = Evaluator.resolveDestPaths(
evaluator.outPath,
mod.compile.ctx.segments
)
val scalaVersionOpt = mod match {
case x: ScalaModule => Some(evaluator.evaluate(Agg(x.scalaVersion)).values.head.asInstanceOf[String])
case _ => None
}
val generatedSourceOutPath = Evaluator.resolveDestPaths(
evaluator.outPath,
mod.generatedSources.ctx.segments
)
val isTest = mod.isInstanceOf[TestModule]
val elem = moduleXmlTemplate(
mod.intellijModulePath,
scalaVersionOpt,
Strict.Agg.from(resourcesPathRefs.map(_.path)),
Strict.Agg.from(normalSourcePaths),
Strict.Agg.from(generatedSourcePaths),
paths.out,
generatedSourceOutPath.dest,
Strict.Agg.from(resolvedDeps.map(pathToLibName)),
Strict.Agg.from(mod.moduleDeps.map{ m => moduleName(moduleLabels(m))}.distinct),
isTest
)
Tuple2(os.rel/".idea_modules"/s"${moduleName(path)}.iml", elem)
}
fixedFiles ++ libraries ++ moduleFiles
}
def evalOrElse[T](evaluator: Evaluator, e: Task[T], default: => T): T = {
evaluator.evaluate(Agg(e)).values match {
case Seq() => default
case Seq(e: T) => e
}
}
def relify(p: os.Path) = {
val r = p.relativeTo(os.pwd/".idea_modules")
(Seq.fill(r.ups)("..") ++ r.segments).mkString("/")
}
def moduleName(p: Segments) = p.value.foldLeft(StringBuilder.newBuilder) {
case (sb, Segment.Label(s)) if sb.isEmpty => sb.append(s)
case (sb, Segment.Cross(s)) if sb.isEmpty => sb.append(s.mkString("-"))
case (sb, Segment.Label(s)) => sb.append(".").append(s)
case (sb, Segment.Cross(s)) => sb.append("-").append(s.mkString("-"))
}.mkString.toLowerCase()
def scalaSettingsTemplate() = {
}
def miscXmlTemplate(jdkInfo: (String,String)) = {
}
def allModulesXmlTemplate(selectors: Seq[String]) = {
{
for(selector <- selectors)
yield {
val filepath = "$PROJECT_DIR$/.idea_modules/" + selector + ".iml"
val fileurl = "file://" + filepath
}
}
}
def rootXmlTemplate(libNames: Strict.Agg[String]) = {
{
for(name <- libNames.toSeq.sorted)
yield
}
}
def libraryXmlTemplate(name: String, path: os.Path, sources: Option[String], compilerClassPath: Loose.Agg[Path]) = {
val url = if (path.ext == "jar") "jar://" + path + "!/" else "file://" + path
val isScalaLibrary = compilerClassPath.nonEmpty
{ if(isScalaLibrary) {
{
compilerClassPath.toList.sortBy(_.wrapped).map(p => )
}
}
}
{ if (sources.isDefined) {
}
}
}
def moduleXmlTemplate(basePath: os.Path,
scalaVersionOpt: Option[String],
resourcePaths: Strict.Agg[os.Path],
normalSourcePaths: Strict.Agg[os.Path],
generatedSourcePaths: Strict.Agg[os.Path],
compileOutputPath: os.Path,
generatedSourceOutputPath: os.Path,
libNames: Strict.Agg[String],
depNames: Strict.Agg[String],
isTest: Boolean
) = {
{
val outputUrl = "file://$MODULE_DIR$/" + relify(compileOutputPath) + "/dest/classes"
if (isTest)
else
}
{
for (normalSourcePath <- normalSourcePaths.toSeq.sorted)
yield
}
{
for (generatedSourcePath <- generatedSourcePaths.toSeq.sorted)
yield
}
{
val resourceType = if (isTest) "java-test-resource" else "java-resource"
for (resourcePath <- resourcePaths.toSeq.sorted)
yield
}
{
for(scalaVersion <- scalaVersionOpt.toSeq)
yield
}
{
for(name <- libNames.toSeq.sorted)
yield
}
{
for(depName <- depNames.toSeq.sorted)
yield
}
}
def scalaCompilerTemplate(settings: Map[(Loose.Agg[os.Path], Seq[String]), Seq[JavaModule]]) = {
{
for((((plugins, params), mods), i) <- settings.toSeq.zip(1 to settings.size))
yield
moduleName(m.millModuleSegments)).mkString(",")}>
{
for(param <- params)
yield
}
{
for(plugin <- plugins.toSeq)
yield
}
}
}
}