Skip to content

Commit

Permalink
sbt-crossproject support
Browse files Browse the repository at this point in the history
  • Loading branch information
Roman Janusz committed Mar 19, 2023
1 parent a8a4569 commit f4b45b0
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 72 deletions.
91 changes: 60 additions & 31 deletions build.sbt
Expand Up @@ -19,42 +19,71 @@ inThisBuild(Seq(
)),
))

lazy val root = project.in(file("."))
val commonSettings: Seq[Def.Setting[_]] = Seq(
ideBasePackages := Seq(s"${organization.value}.sbt.nosbt"),

Compile / scalacOptions ++= Seq(
"-encoding", "utf-8",
"-explaintypes",
"-feature",
"-deprecation",
"-unchecked",
"-language:implicitConversions",
"-language:existentials",
"-language:dynamics",
"-language:experimental.macros",
"-language:higherKinds",
"-Werror",
"-Xlint:-missing-interpolator,-adapted-args,-unused,_",
),

publishMavenStyle := true,
pomIncludeRepository := { _ => false },
publishTo := sonatypePublishToBundle.value,

projectInfo := ModuleInfo(
nameFormal = "sbt-nosbt",
description = "SBT plugin for organizing your build into plain Scala files",
homepage = Some(url("https://github.com/ghik/sbt-nosbt")),
startYear = Some(2023),
licenses = Vector(
"Apache License, Version 2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0")
),
organizationName = "ghik",
organizationHomepage = Some(url("https://github.com/ghik")),
scmInfo = Some(ScmInfo(
browseUrl = url("https://github.com/ghik/sbt-nosbt.git"),
connection = "scm:git:git@github.com:ghik/sbt-nosbt.git",
devConnection = Some("scm:git:git@github.com:ghik/sbt-nosbt.git")
)),
developers = Vector(
Developer("ghik", "Roman Janusz", "romeqjanoosh@gmail.com", url("https://github.com/ghik"))
),
),

pluginCrossBuild / sbtVersion := {
scalaBinaryVersion.value match {
case "2.12" => "1.8.0"
}
},
)

lazy val root: Project = project.in(file("."))
.aggregate(crossproject)
.enablePlugins(SbtPlugin)
.settings(
commonSettings,
name := "sbt-nosbt",
pluginCrossBuild / sbtVersion := {
scalaBinaryVersion.value match {
case "2.12" => "1.8.0"
}
},

ideBasePackages := Seq(s"${organization.value}.${name.value}"),
libraryDependencies ++= Seq(
"com.avsystem.commons" %% "commons-core" % "2.9.0",
),
)

publishMavenStyle := true,
pomIncludeRepository := { _ => false },
publishTo := sonatypePublishToBundle.value,

projectInfo := ModuleInfo(
nameFormal = "sbt-nosbt",
description = "SBT plugin for organizing your build into plain Scala files",
homepage = Some(url("https://github.com/ghik/sbt-nosbt")),
startYear = Some(2023),
licenses = Vector(
"Apache License, Version 2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0")
),
organizationName = "ghik",
organizationHomepage = Some(url("https://github.com/ghik")),
scmInfo = Some(ScmInfo(
browseUrl = url("https://github.com/ghik/sbt-nosbt.git"),
connection = "scm:git:git@github.com:ghik/sbt-nosbt.git",
devConnection = Some("scm:git:git@github.com:ghik/sbt-nosbt.git")
)),
developers = Vector(
Developer("ghik", "Roman Janusz", "romeqjanoosh@gmail.com", url("https://github.com/ghik"))
),
),
lazy val crossproject: Project = project
.enablePlugins(SbtPlugin)
.dependsOn(LocalRootProject) // avoid recursion
.settings(
commonSettings,
name := "sbt-nosbt-crossproject",
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "1.2.0"),
)
@@ -0,0 +1,39 @@
package com.github.ghik.sbt.nosbt
package crossproject

import com.avsystem.commons.misc.OptArg
import sbtcrossproject.{CrossProject, CrossType, Platform}

trait CrossProjectSupport { this: ProjectGroup =>
/**
* A cross-project version of `mkSubProject`.
* Use this method in place of `sbtcrossproject.CrossPlugin.autoImport.crossProject`.
*/
protected def mkCrossSubProject(platforms: Platform*): CrossProjectBuilder = macro Macros.mkCrossSubProjectImpl

protected def mkCrossSubProject(name: String)(platforms: Platform*): CrossProjectBuilder =
new CrossProjectBuilder(CrossProject(subProjectId(name), subProjectDir(name))(platforms *))

/** Imitates `CrossProject.Builder` */
protected final class CrossProjectBuilder(wrapped: CrossProject.Builder) {
def withoutSuffixFor(platform: Platform): CrossProjectBuilder =
new CrossProjectBuilder(wrapped.withoutSuffixFor(platform))

def crossType(crossType: CrossType): CrossProjectBuilder =
new CrossProjectBuilder(wrapped.crossType(crossType))

def build(): CrossProject =
wrapped.build().settings(allSubProjectSettings)
}
protected object CrossProjectBuilder {
implicit def crossProjectFromBuilder(builder: CrossProjectBuilder): CrossProject =
builder.build()
}
}

abstract class CrossProjectGroup(
groupName: String,
parent: OptArg[ProjectGroup] = OptArg.Empty,
)(implicit
discoverProject: DiscoveredProjects
) extends ProjectGroup(groupName, parent) with CrossProjectSupport
12 changes: 8 additions & 4 deletions src/main/scala/com/github/ghik/sbt/nosbt/Macros.scala
Expand Up @@ -6,8 +6,6 @@ class Macros(val c: blackbox.Context) {

import c.universe.*

private def NosbtPkg = q"_root_.com.github.ghik.sbt.nosbt"

private def classBeingConstructed: ClassSymbol = {
val ownerConstr = c.internal.enclosingOwner
if (!ownerConstr.isConstructor) {
Expand Down Expand Up @@ -41,6 +39,12 @@ class Macros(val c: blackbox.Context) {
q"($arg: $projectGroupTpe) => _root_.scala.Seq(..$projectRefs)"
}

def mkFreshProject: Tree =
q"$NosbtPkg.FreshProject(_root_.sbt.project)"
private def enclosingValName: String =
SbtMacroUtils.definingValName(c, methodName => s"$methodName must be assigned directly to a lazy val")

def mkSubProjectImpl: Tree =
q"${c.prefix}.mkSubProject($enclosingValName)"

def mkCrossSubProjectImpl(platforms: c.Tree*): Tree =
q"${c.prefix}.mkCrossSubProject($enclosingValName)(..$platforms)"
}
68 changes: 31 additions & 37 deletions src/main/scala/com/github/ghik/sbt/nosbt/ProjectGroup.scala
Expand Up @@ -17,40 +17,41 @@ abstract class ProjectGroup(
final def parentGroup: Option[ProjectGroup] = parent.toOption

def baseDir: File = parent.fold(file("."))(p => p.baseDir / groupName)
protected def subProjectDir(name: String): File = baseDir / name

/**
* Settings shared by all the projects defined in this [[ProjectGroup]] and its child [[ProjectGroup]]s
* (i.e. those that declare this group as their [[parent]]).
* (i.e. those that declare this group as their [[parentGroup]]).
*/
def commonSettings: Seq[Def.Setting[?]] = Seq.empty

/**
* Settings shared by all the projects defined in this [[ProjectGroup]] and its child [[ProjectGroup]]s
* (i.e. those that declare this group as their [[parent]]), excluding the root project of this group.
* (i.e. those that declare this group as their [[parentGroup]]), excluding the root project of this group.
*/
def subProjectSettings: Seq[Def.Setting[?]] = Seq.empty

/**
* Settings shared by all subprojects defined via [[mkSubProject]] in this [[ProjectGroup]] and all its
* Settings shared by all subprojects defined via `mkSubProject` in this [[ProjectGroup]] and all its
* child [[ProjectGroup]]s. This is like [[commonSettings]] but excludes all the intermediate aggregating projects,
* i.e. the root projects of each [[ProjectGroup]].
*/
def leafProjectSettings: Seq[Def.Setting[?]] = Seq.empty

/**
* Settings shared by all the projects defined in this [[ProjectGroup]], including its root project
* (via [[mkRootProject]]) and directly defined subprojects (via [[mkSubProject]]).
* (via `mkRootProject` and directly defined subprojects (via `mkSubProject`).
*/
def directCommonSettings: Seq[Def.Setting[?]] = Seq.empty

/**
* Settings shared by all the subprojects defined in this [[ProjectGroup]] via [[mkSubProject]].
* Settings shared by all the subprojects defined in this [[ProjectGroup]] via `mkSubProject`.
* Like [[directCommonSettings]] but excludes the root project of this group.
*/
def directSubProjectSettings: Seq[Def.Setting[?]] = Seq.empty

/**
* A [[ProjectReference]] to the root project of this group. Use this if referring directly
* A `ProjectReference` to the root project of this group. Use this if referring directly
* to the root project would create a cycle during project resolution.
*/
final def rootRef: ProjectReference = LocalProject(rootProjectId)
Expand All @@ -69,18 +70,16 @@ abstract class ProjectGroup(
* - current directory if this project group is the toplevel group
* - `<parentGroupDirectory>/<groupName>` otherwise
*/
protected final def mkRootProject(implicit freshProject: FreshProject): Project =
mkRootProject(freshProject.project)

protected def mkRootProject(freshProject: Project): Project =
freshProject
.withId(rootProjectId)
.in(baseDir)
protected def mkRootProject: Project =
Project(rootProjectId, baseDir)
.enablePlugins(this)
.settings(commonSettings)
.settings(directCommonSettings)
.settings(parent.mapOr(Nil, _.commonSettings))
.settings(parent.mapOr(Nil, _.subProjectSettings))
.settings(allRootProjectSettings)

protected final def allRootProjectSettings: Seq[Def.Setting[?]] =
commonSettings ++
directCommonSettings ++
parent.mapOr(Nil, _.commonSettings) ++
parent.mapOr(Nil, _.subProjectSettings)

/**
* Creates a subproject in this project group. This method should be used in a similar way that regular
Expand All @@ -90,21 +89,21 @@ abstract class ProjectGroup(
* Name of this `lazy val` will be used as the subdirectory name for this project and as a suffix in the
* ID of the project.
*/
protected final def mkSubProject(implicit freshProject: FreshProject): Project =
mkSubProject(freshProject.project)

protected def mkSubProject(freshProject: Project): Project =
freshProject
.in(baseDir / freshProject.id)
.withId(subProjectId(freshProject.id))
.settings(commonSettings)
.settings(subProjectSettings)
.settings(leafProjectSettings)
.settings(directCommonSettings)
.settings(directSubProjectSettings)
.settings(parent.mapOr(Nil, _.commonSettings))
.settings(parent.mapOr(Nil, _.subProjectSettings))
.settings(parent.mapOr(Nil, _.leafProjectSettings))
protected final def mkSubProject: Project = macro Macros.mkSubProjectImpl

protected def mkSubProject(name: String): Project =
Project(subProjectId(name), subProjectDir(name))
.settings(allSubProjectSettings)

protected final def allSubProjectSettings: Seq[Def.Setting[?]] =
commonSettings ++
subProjectSettings ++
leafProjectSettings ++
directCommonSettings ++
directSubProjectSettings ++
parent.mapOr(Nil, _.commonSettings) ++
parent.mapOr(Nil, _.subProjectSettings) ++
parent.mapOr(Nil, _.leafProjectSettings)

final def subprojects: Seq[Project] = discoveredProjects.get(this)

Expand All @@ -117,11 +116,6 @@ abstract class ProjectGroup(
override final def projectSettings: Seq[Def.Setting[?]] = Nil
}

final case class FreshProject(project: Project)
object FreshProject {
implicit def materialize: FreshProject = macro Macros.mkFreshProject
}

trait DiscoveredProjects {
def get(group: ProjectGroup): Seq[Project]
}
Expand Down
34 changes: 34 additions & 0 deletions src/main/scala/com/github/ghik/sbt/nosbt/SbtMacroUtils.scala
@@ -0,0 +1,34 @@
package com.github.ghik.sbt.nosbt

import scala.annotation.tailrec
import scala.reflect.macros.blackbox

// copied from sbt.std.KeyMacro
private[nosbt] object SbtMacroUtils {
def definingValName(c: blackbox.Context, invalidEnclosingTree: String => String): String = {
import c.universe.{Apply as ApplyTree, *}
val methodName = c.macroApplication.symbol.name
def processName(n: Name): String =
n.decodedName.toString.trim // trim is not strictly correct, but macros don't expose the API necessary
@tailrec def enclosingVal(trees: List[c.Tree]): String = {
trees match {
case ValDef(_, name, _, _) :: _ => processName(name)
case (_: ApplyTree | _: Select | _: TypeApply) :: xs => enclosingVal(xs)
// lazy val x: X = <methodName> has this form for some reason (only when the explicit type is present, though)
case Block(_, _) :: DefDef(mods, name, _, _, _, _) :: _ if mods.hasFlag(Flag.LAZY) =>
processName(name)
case _ =>
c.error(c.enclosingPosition, invalidEnclosingTree(methodName.decodedName.toString))
"<error>"
}
}
enclosingVal(enclosingTrees(c).toList)
}

private def enclosingTrees(c: blackbox.Context): Seq[c.Tree] =
c.asInstanceOf[reflect.macros.runtime.Context]
.callsiteTyper
.context
.enclosingContextChain
.map(_.tree.asInstanceOf[c.Tree])
}

0 comments on commit f4b45b0

Please sign in to comment.