Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature multi project support #1875

Merged
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ workspace/
metals.sbt

.bsp/


#Workspace Tooling
.envrc
nix
shell.nix

2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Thanks goes to these wonderful people for contributing to Scala Steward:
* [Daniel Pfeiffer](https://github.com/dpfeiffer)
* [Daniel Spiewak](https://github.com/djspiewak)
* [David Francoeur](https://github.com/daddykotex)
* [Dominic Egger](https://github.com/GrafBlutwurst)
* [Don Smith III](https://github.com/cactauz)
* [Doug Roper](https://github.com/htmldoug)
* [Eldar Yusupov](https://github.com/eyusupov)
Expand Down Expand Up @@ -163,6 +164,7 @@ Consider creating PR to add your company to the list and join the community.
* [PlayQ](https://www.playq.com/)
* [Precog](https://precog.com/)
* [Rewards Network](https://www.rewardsnetwork.com/)
* [Rivero](https://rivero.tech/)
* [Septeni Original](https://www.septeni-original.co.jp)
* [Snowplow Analytics](https://snowplowanalytics.com/)
* [SoftwareMill](https://softwaremill.com)
Expand Down
5 changes: 5 additions & 0 deletions docs/repo-specific-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ commits.message = "Update ${artifactName} from ${currentVersion} to ${nextVersio
# If false, Scala Steward will not perform scalafmt, so your CI may abort when reformat needed.
# Default: true
scalafmt.runAfterUpgrading = false

# It is possible to have multiple scala projects in a single repository. In that case the folders containing the projects (build.sbt folders)
# are specified using the buildRoots property. Note that the paths used there are relative and if the repo directory itself also contains a build.sbt the dot can be used to specify it.
# Default: ["."]
buildRoots = [ ".", "subfolder/projectA" ]
```

The version information given in the patterns above can be in two formats:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ package org.scalasteward.core.buildtool
import org.scalasteward.core.data.Scope
import org.scalasteward.core.scalafix.Migration
import org.scalasteward.core.util.Nel
import org.scalasteward.core.vcs.data.Repo

trait BuildToolAlg[F[_]] {
def containsBuild(repo: Repo): F[Boolean]
trait BuildToolAlg[F[_], R] {
def containsBuild(r: R): F[Boolean]

def getDependencies(repo: Repo): F[List[Scope.Dependencies]]
def getDependencies(r: R): F[List[Scope.Dependencies]]

def runMigrations(repo: Repo, migrations: Nel[Migration]): F[Unit]
def runMigrations(r: R, migrations: Nel[Migration]): F[Unit]
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,42 +26,67 @@ import org.scalasteward.core.scalafix.Migration
import org.scalasteward.core.scalafmt.ScalafmtAlg
import org.scalasteward.core.util.Nel
import org.scalasteward.core.vcs.data.Repo
import org.scalasteward.core.repoconfig.RepoConfigAlg
import org.scalasteward.core.vcs.data.BuildRoot

trait BuildToolDispatcher[F[_]] extends BuildToolAlg[F]
trait BuildToolDispatcher[F[_]] extends BuildToolAlg[F, Repo]

object BuildToolDispatcher {
def create[F[_]](implicit
mavenAlg: MavenAlg[F],
millAlg: MillAlg[F],
sbtAlg: SbtAlg[F],
scalafmtAlg: ScalafmtAlg[F],
repoConfigAlg: RepoConfigAlg[F],
F: Monad[F]
): BuildToolDispatcher[F] = {
val allBuildTools = List(mavenAlg, millAlg, sbtAlg)
val fallbackBuildTool = sbtAlg

new BuildToolDispatcher[F] {

private def buildRootsForRepo(repo: Repo): F[List[BuildRoot]] = for {
repoConfigOpt <- repoConfigAlg.readRepoConfig(repo)
repoConfig <- repoConfigAlg.mergeWithDefault(repoConfigOpt)
buildRoots = repoConfig.buildRoots.map(config =>
BuildRoot(repo, config.relativeBuildRootPath)
)
} yield buildRoots

override def containsBuild(repo: Repo): F[Boolean] =
allBuildTools.existsM(_.containsBuild(repo))
buildRootsForRepo(repo).flatMap(buildRoots =>
buildRoots.existsM(buildRoot => allBuildTools.existsM(_.containsBuild(buildRoot)))
)

override def getDependencies(repo: Repo): F[List[Scope.Dependencies]] =
for {
dependencies <- foundBuildTools(repo).flatMap(_.flatTraverse(_.getDependencies(repo)))
additionalDependencies <- getAdditionalDependencies(repo)
} yield Scope.combineByResolvers(additionalDependencies ::: dependencies)
buildRoots <- buildRootsForRepo(repo)
result <- buildRoots.flatTraverse(buildRoot =>
for {
dependencies <- foundBuildTools(buildRoot).flatMap(
_.flatTraverse(_.getDependencies(buildRoot))
)
additionalDependencies <- getAdditionalDependencies(buildRoot)
} yield Scope.combineByResolvers(additionalDependencies ::: dependencies)
)
} yield result

override def runMigrations(repo: Repo, migrations: Nel[Migration]): F[Unit] =
foundBuildTools(repo).flatMap(_.traverse_(_.runMigrations(repo, migrations)))
buildRootsForRepo(repo).flatMap(buildRoots =>
buildRoots.traverse_(buildRoot =>
foundBuildTools(buildRoot).flatMap(_.traverse_(_.runMigrations(buildRoot, migrations)))
)
)

private def foundBuildTools(repo: Repo): F[List[BuildToolAlg[F]]] =
allBuildTools.filterA(_.containsBuild(repo)).map {
private def foundBuildTools(buildRoot: BuildRoot): F[List[BuildToolAlg[F, BuildRoot]]] =
allBuildTools.filterA(_.containsBuild(buildRoot)).map {
case Nil => List(fallbackBuildTool)
case list => list
}

def getAdditionalDependencies(repo: Repo): F[List[Scope.Dependencies]] =
def getAdditionalDependencies(buildRoot: BuildRoot): F[List[Scope.Dependencies]] =
scalafmtAlg
.getScalafmtDependency(repo)
.getScalafmtDependency(buildRoot)
.map(_.map(dep => Scope(List(dep), List(Resolver.mavenCentral))).toList)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import org.scalasteward.core.data._
import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
import org.scalasteward.core.scalafix.Migration
import org.scalasteward.core.util.Nel
import org.scalasteward.core.vcs.data.Repo
import org.scalasteward.core.vcs.data.BuildRoot

trait MavenAlg[F[_]] extends BuildToolAlg[F]
trait MavenAlg[F[_]] extends BuildToolAlg[F, BuildRoot]

object MavenAlg {
def create[F[_]](config: Config)(implicit
Expand All @@ -37,19 +37,27 @@ object MavenAlg {
F: Monad[F]
): MavenAlg[F] =
new MavenAlg[F] {
override def containsBuild(repo: Repo): F[Boolean] =
workspaceAlg.repoDir(repo).flatMap(repoDir => fileAlg.isRegularFile(repoDir / "pom.xml"))
override def containsBuild(buildRoot: BuildRoot): F[Boolean] =
workspaceAlg
.buildRootDir(buildRoot)
.flatMap(buildRootDir => fileAlg.isRegularFile(buildRootDir / "pom.xml"))

override def getDependencies(repo: Repo): F[List[Scope.Dependencies]] =
override def getDependencies(buildRoot: BuildRoot): F[List[Scope.Dependencies]] =
for {
repoDir <- workspaceAlg.repoDir(repo)
dependenciesRaw <- exec(mvnCmd(command.listDependencies), repoDir)
repositoriesRaw <- exec(mvnCmd(command.listRepositories), repoDir)
buildRootDir <- workspaceAlg.buildRootDir(buildRoot)
dependenciesRaw <- exec(
mvnCmd(command.listDependencies),
buildRootDir
)
repositoriesRaw <- exec(
mvnCmd(command.listRepositories),
buildRootDir
)
dependencies = parser.parseDependencies(dependenciesRaw).distinct
resolvers = parser.parseResolvers(repositoriesRaw).distinct
} yield List(Scope(dependencies, resolvers))

override def runMigrations(repo: Repo, migrations: Nel[Migration]): F[Unit] =
override def runMigrations(buildRoot: BuildRoot, migrations: Nel[Migration]): F[Unit] =
F.unit

def exec(command: Nel[String], repoDir: File): F[List[String]] =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ import org.scalasteward.core.data.Scope.Dependencies
import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
import org.scalasteward.core.scalafix.Migration
import org.scalasteward.core.util.Nel
import org.scalasteward.core.vcs.data.Repo
import org.scalasteward.core.vcs.data.BuildRoot

trait MillAlg[F[_]] extends BuildToolAlg[F]
trait MillAlg[F[_]] extends BuildToolAlg[F, BuildRoot]

object MillAlg {
private val content =
Expand All @@ -49,22 +49,24 @@ object MillAlg {
F: BracketThrow[F]
): MillAlg[F] =
new MillAlg[F] {
override def containsBuild(repo: Repo): F[Boolean] =
workspaceAlg.repoDir(repo).flatMap(repoDir => fileAlg.isRegularFile(repoDir / "build.sc"))
override def containsBuild(buildRoot: BuildRoot): F[Boolean] =
workspaceAlg
.buildRootDir(buildRoot)
.flatMap(buildRootDir => fileAlg.isRegularFile(buildRootDir / "build.sc"))

override def getDependencies(repo: Repo): F[List[Dependencies]] =
override def getDependencies(buildRoot: BuildRoot): F[List[Dependencies]] =
for {
repoDir <- workspaceAlg.repoDir(repo)
predef = repoDir / "scala-steward.sc"
buildRootDir <- workspaceAlg.buildRootDir(buildRoot)
predef = buildRootDir / "scala-steward.sc"
extracted <- fileAlg.createTemporarily(predef, content) {
val command = Nel("mill", List("-i", "-p", predef.toString, "show", extractDeps))
processAlg.execSandboxed(command, repoDir)
processAlg.execSandboxed(command, buildRootDir)
}
parsed <- F.fromEither(
parser.parseModules(extracted.dropWhile(!_.startsWith("{")).mkString("\n"))
)
} yield parsed.map(module => Scope(module.dependencies, module.repositories))

override def runMigrations(repo: Repo, migrations: Nel[Migration]): F[Unit] = F.unit
override def runMigrations(buildRoot: BuildRoot, migrations: Nel[Migration]): F[Unit] = F.unit
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,17 @@ import org.scalasteward.core.data.{Dependency, Resolver, Scope}
import org.scalasteward.core.io.{FileAlg, FileData, ProcessAlg, WorkspaceAlg}
import org.scalasteward.core.scalafix.Migration
import org.scalasteward.core.util.Nel
import org.scalasteward.core.vcs.data.Repo
import org.scalasteward.core.vcs.data.BuildRoot

trait SbtAlg[F[_]] extends BuildToolAlg[F] {
trait SbtAlg[F[_]] extends BuildToolAlg[F, BuildRoot] {
def addGlobalPluginTemporarily[A](plugin: FileData)(fa: F[A]): F[A]

def addGlobalPlugins[A](fa: F[A]): F[A]

def getSbtVersion(repo: Repo): F[Option[SbtVersion]]
def getSbtVersion(buildRoot: BuildRoot): F[Option[SbtVersion]]

final def getSbtDependency(repo: Repo)(implicit F: Functor[F]): F[Option[Dependency]] =
OptionT(getSbtVersion(repo)).subflatMap(sbtDependency).value
final def getSbtDependency(buildRoot: BuildRoot)(implicit F: Functor[F]): F[Option[Dependency]] =
OptionT(getSbtVersion(buildRoot)).subflatMap(sbtDependency).value
}

object SbtAlg {
Expand All @@ -66,37 +66,44 @@ object SbtAlg {
logger.info("Add global sbt plugins") >>
stewardPlugin.flatMap(addGlobalPluginTemporarily(_)(fa))

override def containsBuild(repo: Repo): F[Boolean] =
workspaceAlg.repoDir(repo).flatMap(repoDir => fileAlg.isRegularFile(repoDir / "build.sbt"))
override def containsBuild(buildRoot: BuildRoot): F[Boolean] =
workspaceAlg
.buildRootDir(buildRoot)
.flatMap(buildRootDir => fileAlg.isRegularFile(buildRootDir / "build.sbt"))

override def getSbtVersion(repo: Repo): F[Option[SbtVersion]] =
override def getSbtVersion(buildRoot: BuildRoot): F[Option[SbtVersion]] =
for {
repoDir <- workspaceAlg.repoDir(repo)
maybeProperties <- fileAlg.readFile(repoDir / "project" / "build.properties")
buildRootDir <- workspaceAlg.buildRootDir(buildRoot)
maybeProperties <- fileAlg.readFile(
buildRootDir / "project" / "build.properties"
)
version = maybeProperties.flatMap(parser.parseBuildProperties)
} yield version

override def getDependencies(repo: Repo): F[List[Scope.Dependencies]] =
override def getDependencies(buildRoot: BuildRoot): F[List[Scope.Dependencies]] =
for {
repoDir <- workspaceAlg.repoDir(repo)
buildRootDir <- workspaceAlg.buildRootDir(buildRoot)
commands = Nel.of(crossStewardDependencies, reloadPlugins, stewardDependencies)
lines <- sbt(commands, repoDir)
lines <- sbt(commands, buildRootDir)
dependencies = parser.parseDependencies(lines)
additionalDependencies <- getAdditionalDependencies(repo)
additionalDependencies <- getAdditionalDependencies(buildRoot)
} yield additionalDependencies ::: dependencies

override def runMigrations(repo: Repo, migrations: Nel[Migration]): F[Unit] =
override def runMigrations(buildRoot: BuildRoot, migrations: Nel[Migration]): F[Unit] =
addGlobalPluginTemporarily(scalaStewardScalafixSbt) {
workspaceAlg.repoDir(repo).flatMap { repoDir =>
workspaceAlg.buildRootDir(buildRoot).flatMap { buildRootDir =>
migrations.traverse_ { migration =>
val withScalacOptions =
migration.scalacOptions.fold[F[Unit] => F[Unit]](identity) { opts =>
val file = scalaStewardScalafixOptions(opts.toList)
fileAlg.createTemporarily(repoDir / file.name, file.content)(_)
fileAlg.createTemporarily(
buildRootDir / file.name,
file.content
)(_)
}

val scalafixCmds = migration.rewriteRules.map(rule => s"$scalafixAll $rule").toList
withScalacOptions(sbt(Nel(scalafixEnable, scalafixCmds), repoDir).void)
withScalacOptions(sbt(Nel(scalafixEnable, scalafixCmds), buildRootDir).void)
}
}
}
Expand Down Expand Up @@ -127,8 +134,8 @@ object SbtAlg {
}
}

def getAdditionalDependencies(repo: Repo): F[List[Scope.Dependencies]] =
getSbtDependency(repo)
def getAdditionalDependencies(buildRoot: BuildRoot): F[List[Scope.Dependencies]] =
getSbtDependency(buildRoot)
.map(_.map(dep => Scope(List(dep), List(Resolver.mavenCentral))).toList)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,16 @@ import cats.FlatMap
import cats.syntax.all._
import io.chrisdavenport.log4cats.Logger
import org.scalasteward.core.application.Config
import org.scalasteward.core.vcs.data.Repo
import org.scalasteward.core.vcs.data.{BuildRoot, Repo}

trait WorkspaceAlg[F[_]] {
def cleanWorkspace: F[Unit]

def rootDir: F[File]

def repoDir(repo: Repo): F[File]

def buildRootDir(buildRoot: BuildRoot): F[File]
}

object WorkspaceAlg {
Expand All @@ -40,6 +42,8 @@ object WorkspaceAlg {
new WorkspaceAlg[F] {
private[this] val reposDir = config.workspace / "repos"

private[this] def repoDirUnsafe(repo: Repo) = reposDir / repo.owner / repo.repo

override def cleanWorkspace: F[Unit] =
for {
_ <- logger.info(s"Clean workspace ${config.workspace}")
Expand All @@ -51,6 +55,9 @@ object WorkspaceAlg {
fileAlg.ensureExists(config.workspace)

override def repoDir(repo: Repo): F[File] =
fileAlg.ensureExists(reposDir / repo.owner / repo.repo)
fileAlg.ensureExists(repoDirUnsafe(repo))

override def buildRootDir(buildRoot: BuildRoot): F[File] =
fileAlg.ensureExists(repoDirUnsafe(buildRoot.repo) / buildRoot.relativePath)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2018-2021 Scala Steward contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.scalasteward.core.repoconfig

import cats.Eq
import io.circe.{Decoder, Encoder}

final case class BuildRootConfig(relativeBuildRootPath: String)

object BuildRootConfig {
val current = BuildRootConfig(".")

implicit val buildRootConfigDecoder: Decoder[BuildRootConfig] =
Decoder[String].map(BuildRootConfig.apply)

implicit val buildRootConfigStrategyEncoder: Encoder[BuildRootConfig] =
Encoder[String].contramap(_.relativeBuildRootPath)

implicit val buildRootConfigStrategyEq: Eq[BuildRootConfig] =
Eq.fromUniversalEquals
}
Loading