diff --git a/extractor/src/main/scala/org/jetbrains/sbt/Options.scala b/extractor/src/main/scala/org/jetbrains/sbt/Options.scala index fa2eafd..4a4f00f 100644 --- a/extractor/src/main/scala/org/jetbrains/sbt/Options.scala +++ b/extractor/src/main/scala/org/jetbrains/sbt/Options.scala @@ -5,7 +5,8 @@ final case class Options(download: Boolean = false, resolveJavadocClassifiers: Boolean = false, resolveSbtClassifiers: Boolean = false, prettyPrint: Boolean = false, - insertProjectTransitiveDependencies: Boolean = true) + insertProjectTransitiveDependencies: Boolean = true, + separateProdAndTestSources: Boolean = false) object Options { @@ -26,7 +27,8 @@ object Options { resolveJavadocClassifiers = options.contains(Keys.ResolveJavadocClassifiers), resolveSbtClassifiers = options.contains(Keys.ResolveSbtClassifiers), prettyPrint = options.contains(Keys.PrettyPrint), - insertProjectTransitiveDependencies = options.contains(Keys.InsertProjectTransitiveDependencies) + insertProjectTransitiveDependencies = options.contains(Keys.InsertProjectTransitiveDependencies), + separateProdAndTestSources = options.contains(Keys.SeparateProdAndTestSources) ) object Keys { @@ -36,6 +38,7 @@ object Options { val ResolveSbtClassifiers = "resolveSbtClassifiers" val PrettyPrint = "prettyPrint" val InsertProjectTransitiveDependencies = "insertProjectTransitiveDependencies" + val SeparateProdAndTestSources = "separateProdAndTestSources" } } diff --git a/extractor/src/main/scala/org/jetbrains/sbt/extractors/DependenciesExtractor.scala b/extractor/src/main/scala/org/jetbrains/sbt/extractors/DependenciesExtractor.scala index f122f87..eb59156 100644 --- a/extractor/src/main/scala/org/jetbrains/sbt/extractors/DependenciesExtractor.scala +++ b/extractor/src/main/scala/org/jetbrains/sbt/extractors/DependenciesExtractor.scala @@ -1,11 +1,13 @@ package org.jetbrains.sbt package extractors +import org.jetbrains.sbt.extractors.DependenciesExtractor.{ProductionType, ProjectType, TestType} import org.jetbrains.sbt.structure._ import sbt.{Configuration => SbtConfiguration, _} import sbt.jetbrains.apiAdapter._ -import scala.collection.Seq +import scala.collection.{Seq, mutable} +import scala.language.postfixOps /** * @author Nikolay Obedin @@ -20,7 +22,8 @@ class DependenciesExtractor(projectRef: ProjectRef, testConfigurations: Seq[SbtConfiguration], sourceConfigurations: Seq[SbtConfiguration], insertProjectTransitiveDependencies: Boolean, - projectToConfigurations: Map[ProjectRef, Seq[Configuration]]) + separateProdTestSources: Boolean, + projectToConfigurations: Map[ProjectType, Seq[Configuration]]) extends ModulesOps { private lazy val testConfigurationNames = testConfigurations.map(_.name) @@ -28,35 +31,72 @@ class DependenciesExtractor(projectRef: ProjectRef, private lazy val sourceConfigurationsNames = sourceConfigurations.map(_.name) private[extractors] def extract: DependencyData = { - val projectDependencies = - if (insertProjectTransitiveDependencies) transitiveProjectDependencies - else nonTransitiveProjectDependencies + val projectDependencies = (separateProdTestSources, insertProjectTransitiveDependencies) match { + case (true, _) => separatedSourcesProjectDependencies + case (_, true) => transitiveProjectDependencies + case _ => nonTransitiveProjectDependencies + } DependencyData(projectDependencies, moduleDependencies, jarDependencies) } - private def transitiveProjectDependencies: Seq[ProjectDependencyData] = { - val dependencies = projectToConfigurations.map { case (project, configurations) => + private def transitiveProjectDependencies: Dependencies[ProjectDependencyData] = { + val dependencies = projectToConfigurations.map { case(ProjectType(project), configurations) => val transformedConfigurations = mapConfigurations(configurations).map(mapCustomSourceConfigurationToCompileIfApplicable) ProjectDependencyData(project.id, Some(project.build), transformedConfigurations) }.toSeq - dependencies + Dependencies(Seq.empty, dependencies) + } + + private def mapToProjectNameWithSourceTypeAppended(projectType: ProjectType): String = { + val projectName = projectType.project.project + projectType match { + case ProductionType(_) => s"$projectName:main" + case TestType(_) => s"$projectName:test" + } + } + + private def separatedSourcesProjectDependencies: Dependencies[ProjectDependencyData] = { + processDependencies(projectToConfigurations.toSeq) { case (projectType @ ProjectType(project), configs) => + val projectName = mapToProjectNameWithSourceTypeAppended(projectType) + ProjectDependencyData(projectName, Option(project.build), configs) + } } - private def nonTransitiveProjectDependencies: Seq[ProjectDependencyData] = - buildDependencies.classpath.getOrElse(projectRef, Seq.empty).map { it => + private def nonTransitiveProjectDependencies: Dependencies[ProjectDependencyData] = { + val dependencies = buildDependencies.classpath.getOrElse(projectRef, Seq.empty).map { it => val configurations = it.configuration.map(Configuration.fromString).getOrElse(Seq.empty) ProjectDependencyData(it.project.id, Some(it.project.build), configurations) } + Dependencies(Seq.empty, dependencies) + } - private def moduleDependencies: Seq[ModuleDependencyData] = - forAllConfigurations(modulesIn).map { case (moduleId, configurations) => - ModuleDependencyData(moduleId, mapConfigurations(configurations)) + private def moduleDependencies: Dependencies[ModuleDependencyData] = { + val allModuleDependencies = forAllConfigurations(modulesIn) + if (separateProdTestSources) { + processDependencies(allModuleDependencies) { case(moduleId, configs) => + ModuleDependencyData.apply(moduleId, configs) + } + } else { + val dependencies = allModuleDependencies.map { case(moduleId, configs) => + ModuleDependencyData(moduleId, mapConfigurations(configs)) + } + Dependencies(Seq.empty, dependencies) } + } - private def jarDependencies: Seq[JarDependencyData] = - forAllConfigurations(jarsIn).map { case (file, configurations) => - JarDependencyData(file, mapConfigurations(configurations)) + private def jarDependencies: Dependencies[JarDependencyData] = { + val allJarDependencies = forAllConfigurations(jarsIn) + if (separateProdTestSources) { + processDependencies(allJarDependencies) { case (file, configs) => + JarDependencyData(file, configs) + } + } else { + val dependencies = allJarDependencies.map { case (file, configs) => + JarDependencyData(file, mapConfigurations(configs)) + } + Dependencies(Seq.empty, dependencies) } + } private def jarsIn(configuration: SbtConfiguration): Seq[File] = unmanagedClasspath(configuration).map(_.data) @@ -80,6 +120,57 @@ class DependenciesExtractor(projectRef: ProjectRef, .toSeq } + /** + * Configurations passed in a parameter indicate in what configurations some dependency (project, module, jar) is present. Based on that + * we can infer where (prod/test modules) and in what scope this dependency should be added. + */ + private def splitConfigurationsToDifferentSourceSets(configurations: Seq[Configuration]): Dependencies[Configuration] = { + val cs = mergeAllTestConfigurations(configurations) + val resultConfigurations: (Seq[Configuration], Seq[Configuration]) = { + if (Seq(Configuration.Compile, Configuration.Test, Configuration.Runtime).forall(cs.contains)) { // compile configuration + (Seq(Configuration.Compile), Seq(Configuration.Compile)) + } else { + Seq( + // note: + // provided -> prod Provided + test Compile + // runtime -> prod Runtime + test Compile + // these 3 conditions are also suitable for -internal configurations because when e.g. compile-internal configuration + // is used cs contains only compile configuration and this will cause the dependency to be added to the production module + // with the provided scope + (cs.contains(Configuration.Test), Nil, Seq(Configuration.Compile)), + (cs.contains(Configuration.Compile), Seq(Configuration.Provided), Nil), + (cs.contains(Configuration.Runtime), Seq(Configuration.Runtime), Nil) + ).foldLeft((Seq.empty[Configuration], Seq.empty[Configuration])) { case((productionSoFar, testSoFar), (condition, productionUpdate, testUpdate)) => + if (condition) (productionSoFar ++ productionUpdate, testSoFar ++ testUpdate) + else (productionSoFar, testSoFar) + } + } + } + Dependencies(resultConfigurations._2, resultConfigurations._1) + } + + private def processDependencies[D, F]( + dependencies: Seq[(D, Seq[Configuration])] + )(mapToTargetType: ((D, Seq[Configuration])) => F): Dependencies[F] = { + val productionDependencies = mutable.Map.empty[D, Seq[Configuration]] + val testDependencies = mutable.Map.empty[D, Seq[Configuration]] + + def updateDependenciesInProductionAndTest(project: D, configsForProduction: Seq[Configuration], configsForTest: Seq[Configuration]): Unit = { + Seq((productionDependencies, configsForProduction), (testDependencies, configsForTest)) + .filterNot(_._2.isEmpty) + .foreach { case(dependencies, configs) => + val existingConfigurations = dependencies.getOrElse(project, Seq.empty) + dependencies.update(project, existingConfigurations ++ configs) + } + } + + dependencies.foreach { case(dependency, configurations) => + val cs = splitConfigurationsToDifferentSourceSets(configurations) + updateDependenciesInProductionAndTest(dependency, cs.forProduction, cs.forTest) + } + Dependencies(testDependencies.toSeq.map(mapToTargetType), productionDependencies.toSeq.map(mapToTargetType)) + } + // We have to perform this configurations mapping because we're using externalDependencyClasspath // rather than libraryDependencies (to acquire transitive dependencies), so we detect // module presence (in external classpath) instead of explicitly declared configurations. @@ -134,6 +225,7 @@ object DependenciesExtractor extends SbtStateOps with TaskOps { def taskDef: Def.Initialize[Task[DependencyData]] = Def.taskDyn { val state = Keys.state.value + val settings = Keys.settingsData.value val projectRef = Keys.thisProjectRef.value val options = StructureKeys.sbtStructureOpts.value //example: Seq(compile, runtime, test, provided, optional) @@ -159,13 +251,16 @@ object DependenciesExtractor extends SbtStateOps with TaskOps { .forAllConfigurations(state, dependencyConfigurations) val allAcceptedProjects = StructureKeys.acceptedProjects.value - val allConfigurationsWithSourceOfAllProjects = (StructureKeys.allConfigurationsWithSource - .forAllProjects(state, allAcceptedProjects) - // From what I checked adding a Compile configuration explicitly is not needed here but this is how it is done in - // org.jetbrains.sbt.extractors.UtilityTasks.sourceConfigurations so probably for some cases it is required - .flatMap(_._2) ++ Seq(sbt.Compile)).distinct - val settings = Keys.settingsData.value + def getProjectToConfigurations(key: SettingKey[Seq[SbtConfiguration]]) = + key.forAllProjects(state, allAcceptedProjects).toMap.mapValues(_.map(_.name)).withDefaultValue(Seq.empty) + + val projectToSourceConfigurations = getProjectToConfigurations(StructureKeys.sourceConfigurations) + val projectToTestConfigurations = getProjectToConfigurations(StructureKeys.testConfigurations) + + val projectToConfigurations = allAcceptedProjects.map { proj => + proj -> ProjectConfigurations(projectToSourceConfigurations(proj), projectToTestConfigurations(proj)) + }.toMap Def.task { (for { @@ -174,16 +269,14 @@ object DependenciesExtractor extends SbtStateOps with TaskOps { classpathConfiguration <- classpathConfigurationTask } yield { - val projectToTransitiveDependencies = if (options.insertProjectTransitiveDependencies) { - getTransitiveDependenciesForProject( - projectRef, - allConfigurationsWithSourceOfAllProjects, - classpathConfiguration, - settings, - buildDependencies - ) - } else - Map.empty[ProjectRef, Seq[Configuration]] + val arguments = (projectRef, projectToConfigurations, classpathConfiguration, settings, buildDependencies) + val projectToTransitiveDependencies = + if (options.separateProdAndTestSources) { + (getTransitiveDependenciesForProjectProdTestSources _ tupled)(arguments) + } else if (options.insertProjectTransitiveDependencies) { + (getTransitiveDependenciesForProject _ tupled)(arguments) + } else + Map.empty[ProjectType, Seq[Configuration]] val extractor = new DependenciesExtractor( projectRef, @@ -194,6 +287,7 @@ object DependenciesExtractor extends SbtStateOps with TaskOps { testConfigurations, sourceConfigurations, options.insertProjectTransitiveDependencies, + options.separateProdAndTestSources, projectToTransitiveDependencies ) extractor.extract @@ -227,23 +321,78 @@ object DependenciesExtractor extends SbtStateOps with TaskOps { private def getTransitiveDependenciesForProject( projectRef: ProjectRef, - allConfigurationsWithSourceOfAllProjects: Seq[SbtConfiguration], + projectToConfigurations: Map[ProjectRef, ProjectConfigurations], classPathConfiguration: Map[SbtConfiguration, SbtConfiguration], settings: Settings[Scope], buildDependencies: BuildDependencies - ): Map[ProjectRef, Seq[Configuration]] = { - val configToDependencies: Map[Configuration, Seq[ProjectDependency]] = - classPathConfiguration.map { case (selfConfig, config) => - val projectDependencies = retrieveTransitiveProjectDependencies(projectRef, config, settings, buildDependencies) + ): Map[ProjectType, Seq[Configuration]] = { + val dependencyToConfigurations = retrieveTransitiveProjectToConfigsDependencies( + projectRef, + classPathConfiguration, + settings, + buildDependencies, + projectToConfigurations + ) + mapDependenciesToProjectType(dependencyToConfigurations) { projectDependency => ProductionType(projectDependency.project) } + } + + private def mapDependenciesToProjectType( + dependencyToConfigurations: Map[ProjectDependency, Seq[Configuration]] + )(projectDependencyMapping: ProjectDependency => ProjectType): Map[ProjectType, Seq[Configuration]] = + dependencyToConfigurations.foldLeft(Map.empty[ProjectType, Seq[Configuration]]) { case (acc, (projectDependency, configs)) => + val projectType = projectDependencyMapping(projectDependency) + val existingConfigurations = acc.getOrElse(projectType, Seq.empty) + acc.updated(projectType, (existingConfigurations ++ configs).distinct) + } + + private case class ProjectConfigurations(source: Seq[String], test: Seq[String]) + + sealed abstract class ProjectType(val project: ProjectRef) + case class ProductionType(override val project: ProjectRef) extends ProjectType(project) + case class TestType(override val project: ProjectRef) extends ProjectType(project) + + object ProjectType { + def unapply(projectType: ProjectType): Option[ProjectRef] = Some(projectType.project) + } + + private def retrieveTransitiveProjectToConfigsDependencies( + projectRef: ProjectRef, + classPathConfiguration: Map[SbtConfiguration, SbtConfiguration], + settings: Settings[Scope], + buildDependencies: BuildDependencies, + projectToConfigurations: Map[ProjectRef, ProjectConfigurations] + ): Map[ProjectDependency, Seq[Configuration]] = { + val configToDependencies = classPathConfiguration.map { case (selfConfig, config) => + val projectDependencies = retrieveTransitiveProjectDependencies(projectRef, config, settings, buildDependencies, projectToConfigurations) (Configuration(selfConfig.name), projectDependencies) } + invert(configToDependencies) + } - val allConfigurationsWithSourceOfAllProjectsNames = allConfigurationsWithSourceOfAllProjects.map(_.name) - val configToProjects: Map[Configuration, Seq[ProjectRef]] = configToDependencies.mapValues { - keepProjectsWithAtLeastOneSourceConfig(_, allConfigurationsWithSourceOfAllProjectsNames) + private def getTransitiveDependenciesForProjectProdTestSources( + projectRef: ProjectRef, + projectToConfigurations: Map[ProjectRef, ProjectConfigurations], + classPathConfiguration: Map[SbtConfiguration, SbtConfiguration], + settings: Settings[Scope], + buildDependencies: BuildDependencies + ): Map[ProjectType, Seq[Configuration]] = { + val dependencyToConfigurations = retrieveTransitiveProjectToConfigsDependencies( + projectRef, + classPathConfiguration, + settings, + buildDependencies, + projectToConfigurations + ) + val keysMappedToProjectType = mapDependenciesToProjectType(dependencyToConfigurations) { case ProjectDependency(project, configuration) => + projectToConfigurations.get(project) match { + case Some(projectConfigurations) if projectConfigurations.test.contains(configuration) => + TestType(project) + case _ => + ProductionType(project) } + } - invert(configToProjects) + keysMappedToProjectType + (ProductionType(projectRef) -> Seq(Configuration.Test)) } /** @@ -265,12 +414,15 @@ object DependenciesExtractor extends SbtStateOps with TaskOps { projectRef: ProjectRef, config: sbt.Configuration, settings: Settings[Scope], - buildDependencies: BuildDependencies + buildDependencies: BuildDependencies, + projectToConfigurations: Map[ProjectRef, ProjectConfigurations] ): Seq[ProjectDependency] = { val allDependencies = Classpaths.interSort(projectRef, config, settings, buildDependencies) - // note: when production and test sources will be separated removing all dependencies - // with origin project itself, should be done more carefully because it will be required to put projectRef production sources in test sources - val dependenciesWithoutProjectItself = allDependencies.filter(_._1 != projectRef) + val dependenciesWithoutProjectItself = allDependencies + // note: removing dependencies to the origin project itself (when prod/test sources are separated prod part is always added to the test part in #getTransitiveDependenciesForProjectProdTestSources) + // and projects with configurations that do not have sources e.g. provided + .filter { case(project, config) => project != projectRef && isProjectDependencyInSourceConfiguration(project, config, projectToConfigurations) } + dependenciesWithoutProjectItself.map(ProjectDependency.apply) } @@ -289,27 +441,14 @@ object DependenciesExtractor extends SbtStateOps with TaskOps { * }}} * which in practice means that we only have to add `proj2` as a dependency to `root` and `proj1` dependency shouldn't be taken into account. */ - private def keepProjectsWithAtLeastOneSourceConfig( - dependencies: Seq[ProjectDependency], - allConfigurationsWithSourceOfAllProjectsNames: Seq[String] - ): Seq[ProjectRef] = { - val projectToConfigs = dependencies - .groupBy(_.project) - .mapValues(_.map(_.configuration)) - - // note: when production and test sources will be separated we shouldn't just check - // whether there is at least one source configuration per project (it is a very big simplification but sufficient for now). - // For separating production and test sources an analysis should be done to determine what exactly are the configurations of the dependent project and - // from this we should conclude whether we should add production or test part of dependent project to the owner of the dependency. - // There is still a question of where (in production or test sources of the owner of the dependency) to put production/test part of dependent project, - // but it should probably be done in a different place. - val projectToConfigsWithAtLeastOneSourceConfig = - projectToConfigs.filter { case (_, dependencies) => - dependencies.exists(allConfigurationsWithSourceOfAllProjectsNames.contains) - } - - projectToConfigsWithAtLeastOneSourceConfig.keys.toSeq - } + private def isProjectDependencyInSourceConfiguration( + project: ProjectRef, + configuration: String, + projectToConfigurations: Map[ProjectRef, ProjectConfigurations] + ): Boolean = + projectToConfigurations.get(project) + .fold(Seq.empty[String])(t => t.source ++ t.test) + .contains(configuration) private def throwExceptionIfUpdateFailed(result: Result[Map[sbt.Configuration,Keys.Classpath]]): Map[sbt.Configuration, Keys.Classpath] = result match { diff --git a/extractor/src/main/scala/org/jetbrains/sbt/extractors/ProjectExtractor.scala b/extractor/src/main/scala/org/jetbrains/sbt/extractors/ProjectExtractor.scala index b1fcf89..5f5b31f 100644 --- a/extractor/src/main/scala/org/jetbrains/sbt/extractors/ProjectExtractor.scala +++ b/extractor/src/main/scala/org/jetbrains/sbt/extractors/ProjectExtractor.scala @@ -4,7 +4,7 @@ package extractors import org.jetbrains.sbt.structure._ import sbt.Def.Initialize import sbt.jetbrains.keysAdapterEx -import sbt.{Def, File, Configuration => _, _} +import sbt.{Def, File, Configuration => SbtConfiguration, _} import scala.reflect.ClassTag import scala.util.{Failure, Success, Try} @@ -35,9 +35,9 @@ class ProjectExtractor( scalaOrganization: String, scalaInstance: Option[ScalaInstance], scalaCompilerBridgeBinaryJar: Option[File], - scalacOptions: Seq[String], + scalacOptions: Map[Configuration, Seq[String]], javaHome: Option[File], - javacOptions: Seq[String], + javacOptions: Map[Configuration, Seq[String]], compileOrder: CompileOrder, sourceConfigurations: Seq[sbt.Configuration], testConfigurations: Seq[sbt.Configuration], @@ -45,7 +45,9 @@ class ProjectExtractor( play2: Option[Play2Data], settingData: Seq[SettingData], taskData: Seq[TaskData], - commandData: Seq[CommandData] + commandData: Seq[CommandData], + mainSourceDirectories: Seq[File], + testSourceDirectories: Seq[File] ) { private[extractors] def extract: ProjectData = { @@ -78,7 +80,9 @@ class ProjectExtractor( play2, settingData, taskData, - commandData + commandData, + testSourceDirectories, + mainSourceDirectories ) } @@ -253,6 +257,11 @@ object ProjectExtractor extends SbtStateOps with TaskOps { state: State) = key.in(projectRef, Compile).get(state) + private def taskInConfig[T](key: TaskKey[T], config: SbtConfiguration) + (implicit projectRef: ProjectRef, state: State) = + key.in(projectRef, config).get(state) + + def taskDef: Initialize[Task[ProjectData]] = Def.taskDyn { implicit val state: State = Keys.state.value @@ -306,10 +315,16 @@ object ProjectExtractor extends SbtStateOps with TaskOps { taskInCompile(Keys.scalaInstance).onlyIf(options.download).value val scalaCompilerBridgeBinaryJar = keysAdapterEx.myScalaCompilerBridgeBinaryJar.value - val scalacOptions = - taskInCompile(Keys.scalacOptions).onlyIf(options.download).value - val javacOptions = - taskInCompile(Keys.javacOptions).onlyIf(options.download).value + + val configurationToScalacOptions = Map( + Configuration.Compile -> taskInConfig(Keys.scalacOptions, Compile).onlyIf(options.download).value.getOrElse(Seq.empty), + Configuration.Test -> taskInConfig(Keys.scalacOptions, Test).onlyIf(options.download).value.getOrElse(Seq.empty) + ) + val configurationToJavacOptions = Map( + Configuration.Compile -> taskInConfig(Keys.javacOptions, Compile).onlyIf(options.download).value.getOrElse(Seq.empty), + Configuration.Test -> taskInConfig(Keys.javacOptions, Test).onlyIf(options.download).value.getOrElse(Seq.empty) + ) + val name = Keys.name.in(projectRef, Compile).value val organization = Keys.organization.in(projectRef, Compile).value @@ -319,6 +334,18 @@ object ProjectExtractor extends SbtStateOps with TaskOps { val javaHome = Keys.javaHome.in(projectRef, Compile).value val compileOrder = Keys.compileOrder.in(projectRef, Compile).value + val sourceConfigurations = StructureKeys.sourceConfigurations.value + val testConfigurations = StructureKeys.testConfigurations.value + + // note: because we are extracting ConfigurationData with all sourceConfigurations and testConfigurations we also have to take sourceDirectories + // in all configurations + val mainSourceDirectories = Keys.sourceDirectory.in(projectRef) + .forAllConfigurations(state, sourceConfigurations) + .map(_._2) + val testSourceDirectories = Keys.sourceDirectory.in(projectRef) + .forAllConfigurations(state, testConfigurations) + .map(_._2) + new ProjectExtractor( projectRef, name, @@ -339,9 +366,9 @@ object ProjectExtractor extends SbtStateOps with TaskOps { scalaOrganization, scalaInstance, scalaCompilerBridgeBinaryJar, - scalacOptions.getOrElse(Seq.empty), + configurationToScalacOptions, javaHome, - javacOptions.getOrElse(Seq.empty), + configurationToJavacOptions, compileOrder, StructureKeys.sourceConfigurations.value, StructureKeys.testConfigurations.value, @@ -349,7 +376,9 @@ object ProjectExtractor extends SbtStateOps with TaskOps { StructureKeys.extractPlay2.value, StructureKeys.settingData.value, StructureKeys.taskData.value, - StructureKeys.commandData.value.distinct + StructureKeys.commandData.value.distinct, + mainSourceDirectories, + testSourceDirectories ).extract } } diff --git a/extractor/src/main/scala/org/jetbrains/sbt/extractors/UtilityTasks.scala b/extractor/src/main/scala/org/jetbrains/sbt/extractors/UtilityTasks.scala index 7f463f6..b9032e8 100644 --- a/extractor/src/main/scala/org/jetbrains/sbt/extractors/UtilityTasks.scala +++ b/extractor/src/main/scala/org/jetbrains/sbt/extractors/UtilityTasks.scala @@ -140,9 +140,12 @@ object UtilityTasks extends SbtStateOps { val transitiveTest = cs.filter(c => transitiveExtends(c.extendsConfigs) .toSet - .intersect(predefinedTest).nonEmpty) ++ - predefinedTest - transitiveTest.distinct + .intersect(predefinedTest).nonEmpty + ) + // note: IntegrationTest is not a predefined configuration in each sbt project. It has to be manually enabled. + // So returning it from testConfigurations is not necessary and it causes incorrect values to be returned from the sourceDirectory key. + val predefinedAvailableTest = predefinedTest.filter(cs.contains).toSeq + (predefinedAvailableTest ++ transitiveTest).distinct } def sourceConfigurations: Def.Initialize[Seq[Configuration]] = Def.setting { diff --git a/extractor/src/main/scala/org/jetbrains/sbt/operations.scala b/extractor/src/main/scala/org/jetbrains/sbt/operations.scala index 4cb960a..e37709f 100644 --- a/extractor/src/main/scala/org/jetbrains/sbt/operations.scala +++ b/extractor/src/main/scala/org/jetbrains/sbt/operations.scala @@ -25,6 +25,10 @@ trait SbtStateOps { def forAllProjects(state: State, projects: Seq[ProjectRef]): Seq[(ProjectRef, T)] = projects.flatMap(p => key.in(p).find(state).map(it => (p, it))) + def forAllConfigurations(state: State, configurations: Seq[sbt.Configuration]): Seq[(sbt.Configuration, T)] = { + configurations.flatMap(c => key.in(c).get(structure(state).data).map(it => (c, it))) + } + } implicit class `enrich TaskKey`[T](key: TaskKey[T]) { diff --git a/shared/src/main/scala/org/jetbrains/sbt/structure/data.scala b/shared/src/main/scala/org/jetbrains/sbt/structure/data.scala index 4eb4884..ea3705e 100644 --- a/shared/src/main/scala/org/jetbrains/sbt/structure/data.scala +++ b/shared/src/main/scala/org/jetbrains/sbt/structure/data.scala @@ -42,26 +42,29 @@ case class StructureData(sbtVersion: String, * @param basePackages List of packages to use as base prefixes in chaining * @param target Compiler output directory (value of `target` key) */ -case class ProjectData(id: String, - buildURI: URI, - name: String, - organization: String, - version: String, - base: File, - packagePrefix: Option[String], - basePackages: Seq[String], - target: File, - configurations: Seq[ConfigurationData], - java: Option[JavaData], - scala: Option[ScalaData], - compileOrder: String, - dependencies: DependencyData, - resolvers: Set[ResolverData], - play2: Option[Play2Data], - settings: Seq[SettingData], - tasks: Seq[TaskData], - commands: Seq[CommandData] - ) +case class ProjectData( + id: String, + buildURI: URI, + name: String, + organization: String, + version: String, + base: File, + packagePrefix: Option[String], + basePackages: Seq[String], + target: File, + configurations: Seq[ConfigurationData], + java: Option[JavaData], + scala: Option[ScalaData], + compileOrder: String, + dependencies: DependencyData, + resolvers: Set[ResolverData], + play2: Option[Play2Data], + settings: Seq[SettingData], + tasks: Seq[TaskData], + commands: Seq[CommandData], + testSourceDirectories: Seq[File], + mainSourceDirectories: Seq[File] +) case class SettingData(label: String, description: Option[String], rank: Int, stringValue: Option[String]) case class TaskData(label: String, description: Option[String], rank: Int) @@ -108,7 +111,7 @@ case class ConfigurationData(id: String, case class DirectoryData(file: File, managed: Boolean) -case class JavaData(home: Option[File], options: Seq[String]) +case class JavaData(home: Option[File], options: Map[Configuration, Seq[String]]) /** * Analog of `sbt.internal.inc.ScalaInstance` @@ -126,15 +129,22 @@ case class ScalaData( compilerJars: Seq[File], extraJars: Seq[File], compilerBridgeBinaryJar: Option[File], - options: Seq[String] + options: Map[Configuration, Seq[String]] ) { def allJars: Seq[File] = libraryJars ++ compilerJars ++ extraJars def allCompilerJars: Seq[File] = libraryJars ++ compilerJars } -case class DependencyData(projects: Seq[ProjectDependencyData], - modules: Seq[ModuleDependencyData], - jars: Seq[JarDependencyData]) +case class DependencyData(projects: Dependencies[ProjectDependencyData], + modules: Dependencies[ModuleDependencyData], + jars: Dependencies[JarDependencyData]) + +/** + * When the project is imported without prod/test sources feature enabled, all dependencies are put in forProduction parameter. + * @param forTest dependencies that should go to the test module + * @param forProduction dependencies that should go to the main module + */ +case class Dependencies[T](forTest: Seq[T], forProduction: Seq[T]) /** * Inter-project dependency diff --git a/shared/src/main/scala/org/jetbrains/sbt/structure/dataSerializers.scala b/shared/src/main/scala/org/jetbrains/sbt/structure/dataSerializers.scala index 0a49376..bdb3e6a 100644 --- a/shared/src/main/scala/org/jetbrains/sbt/structure/dataSerializers.scala +++ b/shared/src/main/scala/org/jetbrains/sbt/structure/dataSerializers.scala @@ -138,14 +138,21 @@ trait DataSerializers { {what.home.toSeq.map { file => {file.path} }} - {what.options.map { option => - + {what.options.map { case (k, v) => + }} override def deserialize(what: Node): Either[Throwable,JavaData] = { val home = (what \ "home").headOption.map(e => e.text.file) - val options = (what \ "option").map(o => o.text.canonIfFile) + val options = (what \ "option").map { option => + val key = (option \ "key").text + val values = (option \ "value").map(_.text) + Configuration(key) -> values + }.toMap Right(JavaData(home, options)) } } @@ -164,7 +171,12 @@ trait DataSerializers { { what.compilerBridgeBinaryJar.toSeq.map { jar => {jar.path}} } - { what.options.map { option => }} + {what.options.map { case (k, v) => + + }} override def deserialize(what: Node): Either[Throwable,ScalaData] = { @@ -176,7 +188,11 @@ trait DataSerializers { val extraJars = (what \ "extraJars"\ "jar").map(_.text.file) val compilerBridgeBinaryJar = (what \ "compilerBridgeBinaryJar").headOption.map(_.text.file) - val options = (what \ "option").map(o => o.text.canonIfFile) + val options = (what \ "option").map { option => + val key = (option \ "key").text + val values = (option \ "value").map(_.text) + Configuration(key) -> values + }.toMap Right(ScalaData( organization, version, @@ -189,6 +205,24 @@ trait DataSerializers { } } + implicit val projectDependenciesSerializer: XmlSerializer[Dependencies[ProjectDependencyData]] = new XmlSerializer[Dependencies[ProjectDependencyData]] { + override def serialize(what: Dependencies[ProjectDependencyData]): Elem = + + + {what.forTest.map(_.serialize)} + + + {what.forProduction.map(_.serialize)} + + + + override def deserialize(what: Node): Either[Throwable, Dependencies[ProjectDependencyData]] = { + val testDependencies = (what \ "forTest" \ "project").deserialize[ProjectDependencyData] + val compileDependencies = (what \ "forProduction" \ "project").deserialize[ProjectDependencyData] + Right(Dependencies(testDependencies, compileDependencies)) + } + } + implicit val projectDependencySerializer: XmlSerializer[ProjectDependencyData] = new XmlSerializer[ProjectDependencyData] { override def serialize(what: ProjectDependencyData): Elem = { val configurations = what.configurations.mkString(";") @@ -225,6 +259,24 @@ trait DataSerializers { } } + implicit val moduleDependenciesSerializer: XmlSerializer[Dependencies[ModuleDependencyData]] = new XmlSerializer[Dependencies[ModuleDependencyData]] { + override def serialize(what: Dependencies[ModuleDependencyData]): Elem = + + + {what.forTest.map(_.serialize)} + + + {what.forProduction.map(_.serialize)} + + + + override def deserialize(what: Node): Either[Throwable, Dependencies[ModuleDependencyData]] = { + val testDependencies = (what \ "forTest" \ "module").deserialize[ModuleDependencyData] + val compileDependencies = (what \ "forProduction" \ "module").deserialize[ModuleDependencyData] + Right(Dependencies(testDependencies, compileDependencies)) + } + } + implicit val moduleDependencyDataSerializer: XmlSerializer[ModuleDependencyData] = new XmlSerializer[ModuleDependencyData] { override def serialize(what: ModuleDependencyData): Elem = { val elem = what.id.serialize @@ -239,6 +291,24 @@ trait DataSerializers { } } + implicit val jarDependenciesSerializer: XmlSerializer[Dependencies[JarDependencyData]] = new XmlSerializer[Dependencies[JarDependencyData]] { + override def serialize(what: Dependencies[JarDependencyData]): Elem = + + + {what.forTest.map(_.serialize)} + + + {what.forProduction.map(_.serialize)} + + + + override def deserialize(what: Node): Either[Throwable, Dependencies[JarDependencyData]] = { + val testDependencies = (what \ "forTest" \ "jar").deserialize[JarDependencyData] + val compileDependencies = (what \ "forProduction" \ "jar").deserialize[JarDependencyData] + Right(Dependencies(testDependencies, compileDependencies)) + } + } + implicit val jarDependencyDataSerializer: XmlSerializer[JarDependencyData] = new XmlSerializer[JarDependencyData] { override def serialize(what: JarDependencyData): Elem = {what.file.path} @@ -253,16 +323,19 @@ trait DataSerializers { implicit val dependencyDataSerializer: XmlSerializer[DependencyData] = new XmlSerializer[DependencyData] { override def serialize(what: DependencyData): Elem = - {what.projects.sortBy(_.project).map(_.serialize)} - {what.modules.sortBy(_.id.key).map(_.serialize)} - {what.jars.sortBy(_.file).map(_.serialize)} + {what.projects.serialize} + {what.modules.serialize} + {what.jars.serialize} override def deserialize(what: Node): Either[Throwable,DependencyData] = { - val projects = (what \ "project").deserialize[ProjectDependencyData] - val modules = (what \ "module").deserialize[ModuleDependencyData] - val jars = (what \ "jar").deserialize[JarDependencyData] - Right(DependencyData(projects, modules, jars)) + for { + projects <- (what \ "projects").deserializeOne[Dependencies[ProjectDependencyData]].right + modules <- (what \ "modules").deserializeOne[Dependencies[ModuleDependencyData]].right + jars <- (what \ "jars").deserializeOne[Dependencies[JarDependencyData]].right + } yield { + DependencyData(projects, modules, jars) + } } } @@ -420,6 +493,8 @@ trait DataSerializers { {what.settings.map(_.serialize)} {what.tasks.map(_.serialize)} {what.commands.map(_.serialize)} + {what.testSourceDirectories.map(dir => {dir.path})} + {what.mainSourceDirectories.map(dir => {dir.path})} override def deserialize(what: Node): Either[Throwable,ProjectData] = { @@ -429,6 +504,8 @@ trait DataSerializers { val organization = (what \ "organization").text val version = (what \ "version").text val base = (what \ "base").text.file + val testSourceDirectories = (what \ "testSourceDir").map(_.text.file) + val mainSourceDirectories = (what \ "mainSourceDir").map(_.text.file) val packagePrefix = (what \ "packagePrefix").headOption.map(_.text) val basePackages = (what \ "basePackage").map(_.text) val target = (what \ "target").text.file @@ -448,7 +525,7 @@ trait DataSerializers { tryDeps.right.map { dependencies => ProjectData(id, buildURI, name, organization, version, base, packagePrefix, basePackages, target, configurations, java, scala, compileOrder, - dependencies, resolvers, play2, settings, tasks, commands) + dependencies, resolvers, play2, settings, tasks, commands, testSourceDirectories, mainSourceDirectories) } }