From ed8ea584c67f1ab83c488441779356a36d8fa3bb Mon Sep 17 00:00:00 2001 From: Tobias Roeser Date: Thu, 5 Sep 2019 09:34:32 +0200 Subject: Initial module-specific extension support to GenIdea (#684) * Initial module-specific extension support to GenIdea * Contribute Facets to JavaModule * Generate additional files under .idea directory * Introduced more generel Element result type and unit tests * Moved intellijModulePath into GenIdeaModule * Added unit test for GenIdea extension (Missed that previously) --- scalalib/src/GenIdeaImpl.scala | 164 +++++++++++++++++---- scalalib/src/GenIdeaModule.scala | 51 +++++++ scalalib/src/JavaModule.scala | 9 +- .../gen-idea-extended-hello-world/build.sc | 44 ++++++ .../idea/compiler.xml | 8 + .../idea/libraries/scala-library-2.12.4.jar.xml | 18 +++ .../gen-idea-extended-hello-world/idea/misc.xml | 5 + .../gen-idea-extended-hello-world/idea/modules.xml | 9 ++ .../idea_modules/helloworld.iml | 26 ++++ .../idea_modules/helloworld.test.iml | 18 +++ .../idea_modules/mill-build.iml | 12 ++ scalalib/test/src/GenIdeaExtendedTests.scala | 41 ++++++ 12 files changed, 365 insertions(+), 40 deletions(-) create mode 100644 scalalib/src/GenIdeaModule.scala create mode 100644 scalalib/test/resources/gen-idea-extended-hello-world/build.sc create mode 100644 scalalib/test/resources/gen-idea-extended-hello-world/idea/compiler.xml create mode 100644 scalalib/test/resources/gen-idea-extended-hello-world/idea/libraries/scala-library-2.12.4.jar.xml create mode 100644 scalalib/test/resources/gen-idea-extended-hello-world/idea/misc.xml create mode 100644 scalalib/test/resources/gen-idea-extended-hello-world/idea/modules.xml create mode 100644 scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/helloworld.iml create mode 100644 scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/helloworld.test.iml create mode 100644 scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/mill-build.iml create mode 100644 scalalib/test/src/GenIdeaExtendedTests.scala (limited to 'scalalib') diff --git a/scalalib/src/GenIdeaImpl.scala b/scalalib/src/GenIdeaImpl.scala index f737d473..f91eadaf 100755 --- a/scalalib/src/GenIdeaImpl.scala +++ b/scalalib/src/GenIdeaImpl.scala @@ -10,9 +10,11 @@ import mill.api.{Loose, Result, Strict} import mill.define._ import mill.eval.{Evaluator, PathRef} import mill.{T, scalalib} -import os.Path - +import os.{Path, RelPath} import scala.util.Try +import scala.xml.{Elem, MetaData, NodeSeq, Null, UnprefixedAttribute} + +import mill.scalalib.GenIdeaModule.{IdeaConfigFile, JavaFacet} object GenIdea extends ExternalModule { @@ -34,10 +36,13 @@ case class GenIdeaImpl(evaluator: Evaluator, ctx: Log with Home, rootModule: BaseModule, discover: Discover[_]) { + import GenIdeaImpl._ + val cwd: Path = rootModule.millSourcePath - def run(): Unit = { + val ideaConfigVersion = 4 + def run(): Unit = { val pp = new scala.xml.PrettyPrinter(999, 4) val jdkInfo = extractCurrentJdk(cwd / ".idea" / "misc.xml").getOrElse(("JDK_1_8", "1.8 (1)")) @@ -68,7 +73,7 @@ case class GenIdeaImpl(evaluator: Evaluator, ctx: Option[Log], fetchMillModules: Boolean = true): Seq[(os.RelPath, scala.xml.Node)] = { - val modules = rootModule.millInternal.segmentsToModules.values + val modules: Seq[(Segments, JavaModule)] = rootModule.millInternal.segmentsToModules.values .collect{ case x: scalalib.JavaModule => x } .flatMap(_.transitiveModuleDeps) .map(x => (x.millModuleSegments, x)) @@ -112,7 +117,9 @@ case class GenIdeaImpl(evaluator: Evaluator, pluginClasspath: Loose.Agg[Path], scalaOptions: Seq[String], compilerClasspath: Loose.Agg[Path], - libraryClasspath: Loose.Agg[Path] + libraryClasspath: Loose.Agg[Path], + facets: Seq[JavaFacet], + configFileContributions: Seq[IdeaConfigFile] ) val resolved = evalOrElse(evaluator, Task.sequence(for((path, mod) <- modules) yield { @@ -149,6 +156,14 @@ case class GenIdeaImpl(evaluator: Evaluator, mod.resolveDeps(scalacPluginsIvyDeps)() } + val facets = T.task{ + mod.ideaJavaModuleFacets(ideaConfigVersion)() + } + + val configFileContributions = T.task{ + mod.ideaConfigFiles(ideaConfigVersion)() + } + T.task { val resolvedCp: Loose.Agg[PathRef] = externalDependencies() val resolvedSrcs: Loose.Agg[PathRef] = externalSources() @@ -156,6 +171,8 @@ case class GenIdeaImpl(evaluator: Evaluator, val resolvedCompilerCp: Loose.Agg[PathRef] = scalaCompilerClasspath() val resolvedLibraryCp: Loose.Agg[PathRef] = externalLibraryDependencies() val scalacOpts: Seq[String] = scalacOptions() + val resolvedFacets: Seq[JavaFacet] = facets() + val resolvedConfigFileContributions: Seq[IdeaConfigFile] = configFileContributions() ResolvedModule( path, @@ -164,7 +181,9 @@ case class GenIdeaImpl(evaluator: Evaluator, resolvedSp.map(_.path).filter(_.ext == "jar"), scalacOpts, resolvedCompilerCp.map(_.path), - resolvedLibraryCp.map(_.path) + resolvedLibraryCp.map(_.path), + resolvedFacets, + resolvedConfigFileContributions ) } }), Seq()) @@ -175,6 +194,38 @@ case class GenIdeaImpl(evaluator: Evaluator, val librariesProperties = resolved.flatMap(x => x.libraryClasspath.map(_ -> x.compilerClasspath)).toMap + val configFileContributions = resolved.flatMap(_.configFileContributions) + + type FileComponent = (String, String) + def collisionFree(confs: Seq[IdeaConfigFile]): Map[String, Seq[IdeaConfigFile]] = { + var seen: Map[FileComponent, Seq[GenIdeaModule.Element]] = Map() + var result: Map[String, Seq[IdeaConfigFile]] = Map() + confs.foreach { conf => + val key = conf.name -> conf.component + seen.get(key) match { + case None => + seen += key -> conf.config + result += conf.name -> (result.get(conf.name).getOrElse(Seq()) ++ Seq(conf)) + case Some(existing) if conf.config == existing => + // identical, ignore + case Some(existing) => + def details(elements: Seq[GenIdeaModule.Element]) = { + elements.map(ideaConfigElementTemplate(_).toString().replaceAll("\\n", "")) + } + val msg = s"Config collision in file `${conf.name}` and component `${conf.component}`: ${details(conf.config)} vs. ${details(existing)}" + ctx.map(_.log.error(msg)) + } + } + result + } + + //TODO: also check against fixed files + val fileContributions: Seq[(RelPath, Elem)] = collisionFree(configFileContributions).toSeq.map { + case (file, configs) => + val map: Map[String, Seq[GenIdeaModule.Element]] = configs.groupBy(_.component).mapValues(_.flatMap(_.config)) + (os.rel / ".idea" / file) -> ideaConfigFileTemplate(map) + } + val pathShortLibNameDuplicate = allResolved .distinct .groupBy(_.last) @@ -245,7 +296,7 @@ case class GenIdeaImpl(evaluator: Evaluator, val allBuildLibraries : Set[ResolvedLibrary] = resolvedLibraries(buildLibraryPaths ++ buildDepsPaths).toSet - val fixedFiles = Seq( + val fixedFiles: Seq[(RelPath, Elem)] = Seq( Tuple2(os.rel/".idea"/"misc.xml", miscXmlTemplate(jdkInfo)), Tuple2(os.rel/".idea"/"scala_settings.xml", scalaSettingsTemplate()), Tuple2( @@ -277,7 +328,7 @@ case class GenIdeaImpl(evaluator: Evaluator, for(name <- names) yield 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 moduleFiles = resolved.map{ case ResolvedModule(path, resolvedDeps, mod, _, _, _, _, facets, _) => val Seq( resourcesPathRefs: Seq[PathRef], sourcesPathRef: Seq[PathRef], @@ -313,19 +364,13 @@ case class GenIdeaImpl(evaluator: Evaluator, generatedSourceOutPath.dest, Strict.Agg.from(resolvedDeps.map(pathToLibName)), Strict.Agg.from(mod.moduleDeps.map{ m => moduleName(moduleLabels(m))}.distinct), - isTest + isTest, + facets ) 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 - } + fixedFiles ++ fileContributions ++ libraries ++ moduleFiles } def relify(p: os.Path) = { @@ -333,23 +378,47 @@ case class GenIdeaImpl(evaluator: Evaluator, (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 ideaConfigElementTemplate(element: GenIdeaModule.Element): Elem = { - def scalaSettingsTemplate() = { + val example = + + val attribute1: MetaData = + if (element.attributes.isEmpty) Null + else element.attributes.toSeq.reverse.foldLeft(Null.asInstanceOf[MetaData]) { + case (prevAttr, (k, v)) => + new UnprefixedAttribute(k, v, prevAttr) + } + + new Elem( + prefix = null, + label = element.name, + attributes1 = attribute1, + example.scope, + minimizeEmpty = true, + child = element.childs.map(ideaConfigElementTemplate): _* + ) + } + + def ideaConfigFileTemplate(components: Map[String, Seq[GenIdeaModule.Element]]): Elem = { + + { + components.toSeq.map { case (name, config) => + {config.map(ideaConfigElementTemplate)} + } + } + + } - + def scalaSettingsTemplate() = { +// simpleIdeaConfigFileTemplate(Map("ScalaProjectSettings" -> Map("scFileMode" -> "Ammonite"))) + } def miscXmlTemplate(jdkInfo: (String,String)) = { - + @@ -357,7 +426,7 @@ case class GenIdeaImpl(evaluator: Evaluator, } def allModulesXmlTemplate(selectors: Seq[String]) = { - + } def rootXmlTemplate(libNames: Strict.Agg[String]) = { - + @@ -430,9 +499,10 @@ case class GenIdeaImpl(evaluator: Evaluator, generatedSourceOutputPath: os.Path, libNames: Strict.Agg[String], depNames: Strict.Agg[String], - isTest: Boolean - ) = { - + isTest: Boolean, + facets: Seq[GenIdeaModule.JavaFacet] + ): Elem = { + { val outputUrl = "file://$MODULE_DIR$/" + relify(compileOutputPath) + "/dest/classes" @@ -479,11 +549,16 @@ case class GenIdeaImpl(evaluator: Evaluator, yield } + { if (facets.isEmpty) NodeSeq.Empty else { + { for (facet <- facets) yield { + { ideaConfigElementTemplate(facet.config) } + } } + } } } def scalaCompilerTemplate(settings: Map[(Loose.Agg[os.Path], Seq[String]), Seq[JavaModule]]) = { - + { for((((plugins, params), mods), i) <- settings.toSeq.zip(1 to settings.size)) @@ -507,3 +582,28 @@ case class GenIdeaImpl(evaluator: Evaluator, } } + +object GenIdeaImpl { + + /** + * Create the module name (to be used by Idea) for the module based on it segments. + * @see [[Module.millModuleSegments]] + */ + def moduleName(p: Segments): String = 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() + + /** + * Evaluate the given task `e`. In case, the task has no successful result(s), return the `default` value instead. + */ + 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 + } + } + +} diff --git a/scalalib/src/GenIdeaModule.scala b/scalalib/src/GenIdeaModule.scala new file mode 100644 index 00000000..e874da8b --- /dev/null +++ b/scalalib/src/GenIdeaModule.scala @@ -0,0 +1,51 @@ +package mill.scalalib + +import mill.define.Command +import mill.{Module, T} + +/** + * Module specific configuration of the Idea project file generator. + */ +trait GenIdeaModule extends Module { + import GenIdeaModule._ + + def intellijModulePath: os.Path = millSourcePath + + /** + * Skip Idea project file generation. + */ + def skipIdea: Boolean = false + + /** + * Contribute facets to the Java module configuration. + * @param ideaConfigVersion The IDEA configuration version in use. Probably `4`. + * @return + */ + def ideaJavaModuleFacets(ideaConfigVersion: Int): Command[Seq[JavaFacet]] = T.command { Seq[JavaFacet]() } + + /** + * Contribute components to idea config files. + */ + def ideaConfigFiles(ideaConfigVersion: Int): Command[Seq[IdeaConfigFile]] = T.command { Seq[IdeaConfigFile]() } + + } + +object GenIdeaModule { + import upickle.default._ + + case class Element(name: String, attributes: Map[String, String] = Map(), childs: Seq[Element] = Seq()) + object Element { + implicit def rw: ReadWriter[Element] = macroRW + } + + final case class JavaFacet(`type`: String, name: String, config: Element) + object JavaFacet { + implicit def rw: ReadWriter[JavaFacet] = macroRW + } + + final case class IdeaConfigFile(name: String, component: String, config: Seq[Element]) + object IdeaConfigFile { + implicit def rw: ReadWriter[IdeaConfigFile] = macroRW + } + +} diff --git a/scalalib/src/JavaModule.scala b/scalalib/src/JavaModule.scala index c7066d15..b9987ca1 100644 --- a/scalalib/src/JavaModule.scala +++ b/scalalib/src/JavaModule.scala @@ -17,7 +17,7 @@ import mill.api.Loose.Agg /** * Core configuration required to compile a single Scala compilation target */ -trait JavaModule extends mill.Module with TaskModule { outer => +trait JavaModule extends mill.Module with TaskModule with GenIdeaModule { outer => def zincWorker: ZincWorkerModule = mill.scalalib.ZincWorkerModule trait Tests extends TestModule{ @@ -554,14 +554,7 @@ trait JavaModule extends mill.Module with TaskModule { outer => */ def artifactId: T[String] = artifactName() - def intellijModulePath: os.Path = millSourcePath - def forkWorkingDir = T{ ammonite.ops.pwd } - - /** - * Skip Idea project file generation. - */ - def skipIdea: Boolean = false } trait TestModule extends JavaModule with TaskModule { diff --git a/scalalib/test/resources/gen-idea-extended-hello-world/build.sc b/scalalib/test/resources/gen-idea-extended-hello-world/build.sc new file mode 100644 index 00000000..4d1affbb --- /dev/null +++ b/scalalib/test/resources/gen-idea-extended-hello-world/build.sc @@ -0,0 +1,44 @@ +import mill.scalalib +import mill.define.Command +import mill.scalalib.GenIdeaModule._ + +trait HelloWorldModule extends scalalib.ScalaModule { + def scalaVersion = "2.12.4" + object test extends super.Tests { + def testFrameworks = Seq("utest.runner.Framework") + } + + def ideaJavaModuleFacets(ideaConfigVersion: Int): Command[Seq[JavaFacet]] = T.command { + ideaConfigVersion match { + case 4 => + Seq( + JavaFacet("AspectJ", "AspectJ", + Element("configuration", childs = Seq( + Element("projectLibrary", childs = Seq( + Element("option", Map("name" -> "name", "value" -> "/tmp")) + )) + )) + ) + ) + } + } + + override def ideaConfigFiles(ideaConfigVersion: Int): Command[Seq[IdeaConfigFile]] = T.command { + ideaConfigVersion match { + case 4 => + Seq( + IdeaConfigFile( + name = "compiler.xml", + component = "AjcSettings", + config = Seq(Element("option", Map("name" -> "ajcPath", "value" -> "/tmp/aspectjtools.jar")))), + IdeaConfigFile( + name = "compiler.xml", + component = "CompilerConfiguration", + config = Seq(Element("option", Map("name" -> "DEFAULT_COMPILER", "value" -> "ajc"))) + ) + ) + } + } +} + +object HelloWorld extends HelloWorldModule diff --git a/scalalib/test/resources/gen-idea-extended-hello-world/idea/compiler.xml b/scalalib/test/resources/gen-idea-extended-hello-world/idea/compiler.xml new file mode 100644 index 00000000..3622ff42 --- /dev/null +++ b/scalalib/test/resources/gen-idea-extended-hello-world/idea/compiler.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/scalalib/test/resources/gen-idea-extended-hello-world/idea/libraries/scala-library-2.12.4.jar.xml b/scalalib/test/resources/gen-idea-extended-hello-world/idea/libraries/scala-library-2.12.4.jar.xml new file mode 100644 index 00000000..5f7c5056 --- /dev/null +++ b/scalalib/test/resources/gen-idea-extended-hello-world/idea/libraries/scala-library-2.12.4.jar.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/scalalib/test/resources/gen-idea-extended-hello-world/idea/misc.xml b/scalalib/test/resources/gen-idea-extended-hello-world/idea/misc.xml new file mode 100644 index 00000000..f4f144ce --- /dev/null +++ b/scalalib/test/resources/gen-idea-extended-hello-world/idea/misc.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/scalalib/test/resources/gen-idea-extended-hello-world/idea/modules.xml b/scalalib/test/resources/gen-idea-extended-hello-world/idea/modules.xml new file mode 100644 index 00000000..193fa62d --- /dev/null +++ b/scalalib/test/resources/gen-idea-extended-hello-world/idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/helloworld.iml b/scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/helloworld.iml new file mode 100644 index 00000000..94810f20 --- /dev/null +++ b/scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/helloworld.iml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/helloworld.test.iml b/scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/helloworld.test.iml new file mode 100644 index 00000000..26fac21d --- /dev/null +++ b/scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/helloworld.test.iml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/mill-build.iml b/scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/mill-build.iml new file mode 100644 index 00000000..46fd8c3b --- /dev/null +++ b/scalalib/test/resources/gen-idea-extended-hello-world/idea_modules/mill-build.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/scalalib/test/src/GenIdeaExtendedTests.scala b/scalalib/test/src/GenIdeaExtendedTests.scala new file mode 100644 index 00000000..0a4de2e7 --- /dev/null +++ b/scalalib/test/src/GenIdeaExtendedTests.scala @@ -0,0 +1,41 @@ +package mill.scalalib + +import mill.util.ScriptTestSuite +import os.Path +import utest._ + +object GenIdeaExtendedTests extends ScriptTestSuite(false) { + + override def workspaceSlug: String = "gen-idea-extended-hello-world" + + override def scriptSourcePath: Path = os.pwd / 'scalalib / 'test / 'resources / workspaceSlug + + def tests: Tests = Tests { + 'genIdeaTests - { + initWorkspace() + eval("mill.scalalib.GenIdea/idea") + + Seq( + s"$workspaceSlug/idea_modules/helloworld.iml" -> workspacePath / ".idea_modules" /"helloworld.iml", + s"$workspaceSlug/idea_modules/helloworld.test.iml" -> workspacePath / ".idea_modules" /"helloworld.test.iml", + s"$workspaceSlug/idea/libraries/scala-library-2.12.4.jar.xml" -> + workspacePath / ".idea" / "libraries" / "scala-library-2.12.4.jar.xml", + + s"$workspaceSlug/idea/modules.xml" -> workspacePath / ".idea" / "modules.xml", + s"$workspaceSlug/idea/misc.xml" -> workspacePath / ".idea" / "misc.xml", + s"$workspaceSlug/idea/compiler.xml" -> workspacePath / ".idea" / "compiler.xml" + + ).foreach { case (resource, generated) => + val resourceString = scala.io.Source.fromResource(resource).getLines().mkString("\n") + val generatedString = normaliseLibraryPaths(os.read(generated)) + + assert(resourceString == generatedString) + } + } + } + + private def normaliseLibraryPaths(in: String): String = { + in.replaceAll(coursier.paths.CoursierPaths.cacheDirectory().toString, "COURSIER_HOME") + } + +} -- cgit v1.2.3