Skip to content

Commit

Permalink
feat: init version
Browse files Browse the repository at this point in the history
  • Loading branch information
name-snrl authored and timothyklim committed Jul 29, 2023
1 parent d5c2bbd commit 1690113
Show file tree
Hide file tree
Showing 11 changed files with 388 additions and 58 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
bazel-*
.direnv/
*.gen.scala
*.swp
*.idea
Expand All @@ -21,3 +22,4 @@ target/
/.settings
/.vscode/
/bazel.iml
/.metals/
2 changes: 2 additions & 0 deletions deps/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ scala_binary(
name = "deps",
srcs = glob(["*.scala"]),
main_class = "rules_scala3.deps.Deps",
resource_strip_prefix = package_name(),
resources = ["templates/jar_artifact_callback.bzl"],
scala = "//scala:bootstrap_3_3",
visibility = ["//visibility:public"],
# jvm_flags = ["-Djava.security.manager=allow"],
Expand Down
39 changes: 39 additions & 0 deletions deps/BazelExt.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package rules_scala3.deps

import scala.io.Source
import java.io.FileNotFoundException

object BazelExt:
private def loadResourceToString(path: String): String =
val resourceStream = getClass.getResourceAsStream(path)
if resourceStream == null then throw new FileNotFoundException(s"Resource not found: $path")
try Source.fromInputStream(resourceStream).mkString finally resourceStream.close

private lazy val jarArtifactCallback = loadResourceToString("/templates/jar_artifact_callback.bzl")

def apply(targets: Vector[Target]): String =
val dependencyLines: String = targets
.filterNot(_.replacement_label.isDefined)
.map { t =>
Vector(
s""" {"artifact": "${t.coordinates.toString}"""",
s""""url": "${t.url}"""",
s""""name": "${t.name}"""",
s""""actual": "${t.actual}"""",
s""""bind": "${t.bind}"},"""
).mkString(", ")
}
.mkString("\n")

s"""# Do not edit. rules_scala3 autogenerates this file
|$jarArtifactCallback
|
|def list_dependencies():
| return [
|$dependencyLines
| ]
|
|def maven_dependencies(callback = jar_artifact_callback):
| for hash in list_dependencies():
| callback(hash)
|""".stripMargin
29 changes: 29 additions & 0 deletions deps/Coordinates.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package rules_scala3.deps

import sbt.librarymanagement.ModuleID
import scala.util.matching.Regex.Groups

case class GroupId(grp: String):
override def toString: String = grp
def toUnixPath: String = grp.replaceAll("\\.", "/").replaceAll("-", "_")

case class Coordinates(groupId: GroupId, artifactId: String, version: Option[String]):
override def toString: String = version match
case Some(v) => s"${groupId}:${artifactId}:${v}"
case None => s"${groupId}:${artifactId}"

def withCleanName: Coordinates = copy(artifactId = this.cleanName)

def cleanName: String =
val scalaNameVersion = """^(.*)_([23](?:\.\d{1,2})?(?:[\.-].*)?)$""".r
def mkName: String = artifactId match
case scalaNameVersion(n, _) => n
case _ => artifactId
mkName.replaceAll("[.\\-]", "_")

object Coordinates:
def apply(groupId: String, artifactId: String, version: String): Coordinates =
Coordinates(GroupId(groupId), artifactId, Some(version))

def apply(groupId: String, artifactId: String): Coordinates =
Coordinates(GroupId(groupId), artifactId, None)
82 changes: 24 additions & 58 deletions deps/Deps.scala
Original file line number Diff line number Diff line change
@@ -1,67 +1,33 @@
package rules_scala3.deps

import java.io.File

import lmcoursier.*
import sbt.internal.librarymanagement.cross.CrossVersionUtil
import sbt.internal.util.ConsoleLogger
import sbt.librarymanagement.*
import sbt.librarymanagement.Configurations.Component
import sbt.librarymanagement.Resolver.{DefaultMavenRepository, JCenterRepository, JavaNet2Repository}
import sbt.librarymanagement.syntax.*

object Deps:
def main(args: Array[String]): Unit =
// https://github.com/coursier/sbt-coursier/blob/3df025313bf010d80b8b9288c76fa6a3cb13c7d0/modules/lm-coursier/src/test/scala/lmcoursier/ResolutionSpec.scala

lazy val log = ConsoleLogger()

val lmEngine = CoursierDependencyResolution(CoursierConfiguration().withResolvers(resolvers))

// val module = "commons-io" % "commons-io" % "2.5"
// lm.retrieve(module, scalaModuleInfo = None, File("/tmp/target"), log)

val stubModule = "com.example" % "foo" % "0.1.0" % "compile"
val dependencies = Vector(
"com.typesafe.scala-logging" % "scala-logging_2.12" % "3.7.2" % "compile",
"org.scalatest" % "scalatest_2.12" % "3.0.4" % "test"
).map(_.withIsTransitive(false))
val coursierModule = module(lmEngine, stubModule, dependencies, Some("2.12.4"))
val resolution =
lmEngine.update(coursierModule, UpdateConfiguration(), UnresolvedWarningConfiguration(), log)
val r = resolution.right.get
println(s"r:$r")
end main

def resolvers = Vector(
DefaultMavenRepository,
JavaNet2Repository,
JCenterRepository,
Resolver.sbtPluginRepo("releases")
@main def Deps(args: String*): Unit =
// parse args
vars = Vars(
projectRoot = new File("/home/name_snrl/work/forks/rules_scala3"),
depsDirName = "3rdparty",
bazelExtFileName = "workspace.bzl",
buildFilesDirName = "jvm",
buildFileName = "BUILD",
scalaVersion = "3.3.0",
buildFileHeader = """load("@io_bazel_rules_scala//scala:scala_import.bzl", "scala_import")"""
)

def configurations = Vector(Compile, Test, Runtime, Provided, Optional, Component)
// Replacements are not handled by `librarymanagement`. any Scala prefix in the name will be dropped.
// It also doesn't matter whether you use double `%` to get the Scala version or not.
val replacements = Map(
"org.scala-lang" % "scala-compiler" -> "@io_bazel_rules_scala_scala_compiler//:io_bazel_rules_scala_scala_compiler",
"org.scala-lang" % "scala-library" -> "@io_bazel_rules_scala_scala_library//:io_bazel_rules_scala_scala_library",
"org.scala-lang" % "scala-reflect" -> "@io_bazel_rules_scala_scala_reflect//:io_bazel_rules_scala_scala_reflect",
"org.scala-lang.modules" % "scala-parser-combinators" -> "@io_bazel_rules_scala_scala_parser_combinators//:io_bazel_rules_scala_scala_parser_combinators",
"org.scala-lang.modules" % "scala-xml" -> "@io_bazel_rules_scala_scala_xml//:io_bazel_rules_scala_scala_xml",
)

def module(
lmEngine: DependencyResolution,
moduleId: ModuleID,
deps: Vector[ModuleID],
scalaFullVersion: Option[String],
overrideScalaVersion: Boolean = true
): ModuleDescriptor =
val scalaModuleInfo = scalaFullVersion map { fv =>
ScalaModuleInfo(
scalaFullVersion = fv,
scalaBinaryVersion = CrossVersionUtil.binaryScalaVersion(fv),
configurations = configurations,
checkExplicit = true,
filterImplicit = false,
overrideScalaVersion = overrideScalaVersion
)
}
val dependencies = Vector(
"org.scala-sbt" % "librarymanagement-core_3" % "2.0.0-alpha12",
"org.scala-sbt" % "librarymanagement-coursier_3" % "2.0.0-alpha6",
)

val moduleSetting = ModuleDescriptorConfiguration(moduleId, ModuleInfo("foo"))
.withDependencies(deps)
.withConfigurations(configurations)
.withScalaModuleInfo(scalaModuleInfo)
lmEngine.moduleDescriptor(moduleSetting)
MakeTree(dependencies, replacements)
38 changes: 38 additions & 0 deletions deps/MakeTree.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package rules_scala3.deps

import sbt.librarymanagement.{DependencyBuilders, ModuleID}, DependencyBuilders.OrganizationArtifactName
import sbt.librarymanagement.syntax.*
import java.io.{File, FileWriter}

object MakeTree:
def apply(dependencies: Vector[ModuleID], replacements: Map[OrganizationArtifactName, String]): Unit =
val targets = Resolve(dependencies, replacements.map((k, v) => (k % "0.1.0").toUvCoordinates.withCleanName -> v))
val bazelExtContent = BazelExt(targets)
writeTree(targets, bazelExtContent)

private def writeToFile(path: File, content: String): Unit =
val dirname = path.getParentFile
if !dirname.exists then dirname.mkdirs
val writer = new FileWriter(path)
try writer.write(content)
finally writer.close

private def writeTree(
targets: Vector[Target],
bazelExtContent: String
): Unit =
// create bazel extension file
writeToFile(vars.bazelExtFile, bazelExtContent)

// create an empty `BUILD` file in the root directory to mark it as
// a package and call the extensions
writeToFile(vars.depsBuildFile, "")

// make tree of BUILD files
targets
.groupBy(_.coordinates.groupId)
.foreach { (group, targets) =>
val file = new File(vars.treeOfBUILDsFile, group.toUnixPath + File.separator + vars.buildFileName)
val content = vars.buildFileHeader + targets.map(_.toBzl).mkString
writeToFile(file, content)
}
63 changes: 63 additions & 0 deletions deps/Resolve.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package rules_scala3.deps

import lmcoursier.{CoursierConfiguration, CoursierDependencyResolution}
import sbt.internal.util.ConsoleLogger
import sbt.librarymanagement.syntax.*
import sbt.librarymanagement.{ModuleDescriptorConfiguration, ModuleID, ModuleInfo, UnresolvedWarningConfiguration, UpdateConfiguration}
import scala.collection.mutable.HashMap

object Resolve:
def apply(dependencies: Vector[ModuleID], replacements: Map[Coordinates, String]): Vector[Target] =
val csConfig = CoursierConfiguration()
.withScalaVersion(Some(vars.scalaVersion))
.withClasspathOrder(false) // it just gets in the way and creates duplicates.

val moduleConfig = ModuleDescriptorConfiguration("com.example" % "foo" % "0.1.0", ModuleInfo("foo"))
.withDependencies(dependencies)

val lmEngine = CoursierDependencyResolution(csConfig)

val resolution = lmEngine.update(
module = lmEngine.moduleDescriptor(moduleConfig),
configuration = UpdateConfiguration(),
uwconfig = UnresolvedWarningConfiguration(),
log = ConsoleLogger()
)
// convert librarymanagement modules to our targets
resolution match
case Left(e) => throw e.resolveException
case Right(resolution) =>
val modules = resolution.configurations.head.modules
// filter out dependencies of replacement
.map(_.filterCallers(replacements))
.filterNot { moduleReport =>
moduleReport.callers.isEmpty && !dependencies
.map(_.toUvCoordinates.withCleanName)
.contains(moduleReport.module.toUvCoordinates.withCleanName)
}
val uvCoordinates_modules = modules.map { m => m.module.toUvCoordinates -> m.module }.toMap
val modules_deps: HashMap[Coordinates, Vector[Coordinates]] = HashMap.empty
modules.foreach { moduleReport =>
moduleReport.callers.foreach { caller =>
// dependency can be resolved with a different version,
// so the comparison is done by org-name pair.
val uvCaller = caller.caller.toUvCoordinates
val coordinates = uvCoordinates_modules.get(uvCaller) match
case Some(m) => m.toCoordinates
case None => sys.error(s"""The organization-name pair was not found in the resolved modules.
|${uvCaller.toString} is missing.""".stripMargin)
if modules_deps.contains(coordinates)
then modules_deps(coordinates) = modules_deps(coordinates) :+ moduleReport.module.toCoordinates
else modules_deps += (coordinates -> Vector(moduleReport.module.toCoordinates))
}
}
modules
.map { moduleReport =>
Target(
moduleReport = moduleReport,
replacement_label = replacements.get(moduleReport.module.toUvCoordinates.withCleanName),
isDirect = dependencies.map(_.cleanName).contains(moduleReport.module.cleanName),
module_deps = modules_deps.toMap.getOrElse(moduleReport.module.toCoordinates, Vector.empty).sortBy(_.toString)
)
}
.sortBy(_.name)
106 changes: 106 additions & 0 deletions deps/Target.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package rules_scala3.deps

import sbt.librarymanagement.{Artifact, ModuleID, ModuleReport}

case class Target(
coordinates: Coordinates,
replacement_label: Option[String],
lang: Language,
name: String,
visibility: Target.Visibility,
actual: String,
bind: String,
jar: String,
url: String,
deps: Vector[Coordinates]
):
def toBzl: String =
replacement_label match
case Some(replacement_label) =>
s"""\n${lang.asString}_import(
| name = "${coordinates.cleanName}",
| exports = [
| "$replacement_label"
| ],
| visibility = [
| "${visibility.asString}"
| ]
|)\n""".stripMargin
case None =>
val runtime_deps =
val deps0 = deps
.map { c =>
if coordinates.groupId == c.groupId
then s"\":${c.cleanName}\""
else s"\"${vars.treeOfBUILDsBazelPath}/${c.groupId.toUnixPath}:${c.cleanName}\""
}
.sorted
.mkString(",\n ")
if deps0 == ""
then ""
else s"""
| runtime_deps = [
| $deps0
| ],""".stripMargin
s"""\n${lang.asString}_import(
| name = "${coordinates.cleanName}",
| jars = [
| "$jar"
| ],$runtime_deps
| visibility = [
| "${visibility.asString}"
| ]
|)\n""".stripMargin

object Target:
enum Visibility(val asString: String):
case Public extends Visibility("//visibility:public")
case SubPackages extends Visibility(s"${vars.treeOfBUILDsBazelPath}:__subpackages__")

def apply(
moduleReport: ModuleReport,
replacement_label: Option[String],
isDirect: Boolean,
module_deps: Vector[Coordinates]
): Target =
val coordinates = moduleReport.module.toCoordinates
val lang = moduleReport.module.language
val name: String = s"${coordinates.groupId}_${coordinates.artifactId}".replaceAll("[.\\-]", "_")
val visibility: Target.Visibility =
if isDirect
then Target.Visibility.Public
else Target.Visibility.SubPackages
val actual: String = s"@$name//jar"
val bind: String = s"jar/${coordinates.groupId.toUnixPath}/${coordinates.artifactId}".replaceAll("[.\\-:]", "_")
val jar: String = s"//external:$bind"
val url: String =
def e = sys.error(s"No url in artifact field of ${moduleReport.module.toString}")
moduleReport.artifacts.head.head.url.fold(e) { u => u.toString } // TODO are you sure there can only be one artifact?

replacement_label match
case Some(replacement_label) =>
Target(
coordinates = coordinates,
replacement_label = Some(replacement_label),
lang = lang,
name = name,
visibility = visibility,
actual = "",
bind = "",
jar = "",
url = "",
deps = Vector.empty
)
case None =>
Target(
coordinates = coordinates,
replacement_label = None,
lang = lang,
name = name,
visibility = visibility,
actual = actual,
bind = bind,
jar = jar,
url = url,
deps = module_deps
)
Loading

0 comments on commit 1690113

Please sign in to comment.