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

Add supports for using Source Generator using Directives #3033

Draft
wants to merge 14 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion modules/build/src/main/scala/scala/build/Build.scala
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,8 @@ object Build {
if (options.useBuildServer.getOrElse(true)) None
else releaseFlag(options, compilerJvmVersionOpt, logger).map(_.toString)

val sourceGeneratorConfig = options.sourceGeneratorOptions.generatorConfig

val scalaCompilerParamsOpt = artifacts.scalaOpt match {
case Some(scalaArtifacts) =>
val params = value(options.scalaParams).getOrElse {
Expand Down Expand Up @@ -1014,7 +1016,8 @@ object Build {
resourceDirs = sources.resourceDirs,
scope = scope,
javaHomeOpt = Option(options.javaHomeLocation().value),
javacOptions = javacOptions
javacOptions = javacOptions,
generateSource = Option(sourceGeneratorConfig)
)
project
}
Expand Down
30 changes: 26 additions & 4 deletions modules/build/src/main/scala/scala/build/Project.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import coursier.core.Classifier

import java.io.ByteArrayOutputStream
import java.nio.charset.StandardCharsets
import java.nio.file.Path
import java.nio.file.{Path, Paths}
import java.util.Arrays

import scala.build.options.{ScalacOpt, Scope, ShadowingSeq}
import scala.build.options.{GeneratorConfig, ScalacOpt, Scope, ShadowingSeq}

final case class Project(
workspace: os.Path,
Expand All @@ -28,7 +28,8 @@ final case class Project(
resourceDirs: Seq[os.Path],
javaHomeOpt: Option[os.Path],
scope: Scope,
javacOptions: List[String]
javacOptions: List[String],
generateSource: Option[Seq[GeneratorConfig]]
) {

import Project._
Expand All @@ -50,6 +51,26 @@ final case class Project(
bridgeJars = scalaCompiler0.bridgeJarsOpt.map(_.map(_.toNIO).toList)
)
}

val sourceGenerator: Option[List[BloopConfig.SourceGenerator]] =
generateSource.map(configs =>
configs.map { config =>
val command0 = config.commandFilePath
val sourceGlobs0 = BloopConfig.SourcesGlobs(
Paths.get(config.inputDir),
None,
config.glob,
Nil
)

BloopConfig.SourceGenerator(
List(sourceGlobs0),
(config.outputPath / "source-generator-output").toNIO,
List("/Users/kiki/Kerja/scala-cli/testing-a/scala-cli", "run", command0, "--power", "--")
)
}.toList
)

baseBloopProject(
projectName,
directory.toNIO,
Expand All @@ -65,7 +86,8 @@ final case class Project(
platform = Some(platform),
`scala` = scalaConfigOpt,
java = Some(BloopConfig.Java(javacOptions)),
resolution = resolution
resolution = resolution,
sourceGenerators = sourceGenerator
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ case class DirectivesPreprocessor(
def preprocess(extractedDirectives: ExtractedDirectives)
: Either[BuildException, PreprocessedDirectives] = either {
val ExtractedDirectives(directives, directivesPositions) = extractedDirectives

val (
buildOptionsWithoutRequirements: PartiallyProcessedDirectives[BuildOptions],
buildOptionsWithTargetRequirements: PartiallyProcessedDirectives[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ object DirectivesPreprocessingUtils {
directives.ScalaJs.handler,
directives.ScalaNative.handler,
directives.ScalaVersion.handler,
directives.SourceGenerator.handler,
directives.Sources.handler,
directives.Tests.handler
).map(_.mapE(_.buildOptions))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package scala.build.preprocessing.directives

// Virtuslab Processor
import com.virtuslab.using_directives.UsingDirectivesProcessor
import com.virtuslab.using_directives.custom.model.{
BooleanValue,
EmptyValue,
StringValue,
UsingDirectives,
Value
}
import com.virtuslab.using_directives.custom.utils.ast._
import scala.jdk.CollectionConverters.*

import scala.cli.commands.SpecificationLevel
import scala.build.directives.*
import scala.build.EitherCps.{either, value}
import scala.build.Ops.*
import scala.build.errors.{BuildException, CompositeBuildException}
import scala.build.options.{BuildOptions, SourceGeneratorOptions, GeneratorConfig}
import scala.build.options.GeneratorConfig
import scala.build.{Positioned, options}
import scala.build.directives.DirectiveValueParser.WithScopePath
import scala.util.matching.Regex
import java.nio.file.Paths
import scala.build.options.InternalOptions

@DirectiveGroupName("SourceGenerator")
@DirectivePrefix("sourceGenerator.")
@DirectiveUsage("//> using sourceGenerator", "`//> using sourceGenerator`")
@DirectiveDescription("Generate code using Source Generator")
@DirectiveLevel(SpecificationLevel.EXPERIMENTAL)
final case class SourceGenerator(
testy: DirectiveValueParser.WithScopePath[List[Positioned[String]]] =
DirectiveValueParser.WithScopePath.empty(Nil),
scripts: DirectiveValueParser.WithScopePath[List[Positioned[String]]] =
DirectiveValueParser.WithScopePath.empty(Nil),
excludeScripts: Option[Boolean] = None,
inputDirectory: DirectiveValueParser.WithScopePath[Option[Positioned[String]]] =
DirectiveValueParser.WithScopePath.empty(None),
glob: Option[Positioned[String]] = None
) extends HasBuildOptions {
def buildOptions: Either[BuildException, BuildOptions] =
SourceGenerator.buildOptions(scripts, excludeScripts)
}

object SourceGenerator {
val handler: DirectiveHandler[SourceGenerator] = DirectiveHandler.derive
def buildOptions(
scripts: DirectiveValueParser.WithScopePath[List[Positioned[String]]],
excludeScripts: Option[Boolean]
): Either[BuildException, BuildOptions] = {
val directiveProcessor = UsingDirectivesProcessor()
val parsedDirectives = scripts.value
.map(script => os.Path(script.value))
.map(os.read(_))
.map(_.toCharArray())
.map(directiveProcessor.extract(_).asScala)
.map(_.headOption)

def processDirectives(script: Option[UsingDirectives]) =
script.toSeq.flatMap { directives =>
def toStrictValue(value: UsingValue): Seq[Value[_]] = value match {
case uvs: UsingValues => uvs.values.asScala.toSeq.flatMap(toStrictValue)
case el: EmptyLiteral => Seq(EmptyValue(el))
case sl: StringLiteral => Seq(StringValue(sl.getValue(), sl))
case bl: BooleanLiteral => Seq(BooleanValue(bl.getValue(), bl))
}
def toStrictDirective(ud: UsingDef) = StrictDirective(
ud.getKey(),
toStrictValue(ud.getValue()),
ud.getPosition().getColumn()
)

directives.getAst match
case uds: UsingDefs => uds.getUsingDefs.asScala.toSeq.map(toStrictDirective)
case _ => Nil // There should be nothing else here other than UsingDefs
}

def replaceSpecialSyntax(directiveValue: String, path: os.Path): String = {
val pattern = """(((?:\$)+)(\{\.\}))""".r
pattern.replaceAllIn(
directiveValue,
(m: Regex.Match) => {
val dollarSigns = m.group(2)
val dollars = "\\$" * (dollarSigns.length / 2)
if (dollarSigns.length % 2 == 0)
s"$dollars${m.group(3)}"
else
s"$dollars${path / os.up}"
}
)
}

def checkForDuplicateDirective(listOfDirective: Seq[StrictDirective]): Unit = {
val directiveKeys = listOfDirective.map(directive => directive.key)
if (directiveKeys.length != directiveKeys.distinct.length)
throw new IllegalArgumentException(s"Duplicate directives found in generator files.")
}

val processedDirectives = parsedDirectives.map(processDirectives(_))

val sourceGeneratorKeywords = Seq("inputDirectory", "glob")
val sourceGeneratorDirectives = processedDirectives.map(directiveSeq =>
directiveSeq.filter(rawDirective =>
sourceGeneratorKeywords.exists(keyword => rawDirective.key.contains(keyword))
)
)

sourceGeneratorDirectives.foreach(components => checkForDuplicateDirective(components))

val scriptPathIterator = scripts.value.map(script =>
os.Path(script.value)
).iterator

val generatorConfigs = sourceGeneratorDirectives.collect {
case Seq(inputDir, glob) =>
val relPath = scriptPathIterator.next()
GeneratorConfig(
replaceSpecialSyntax(inputDir.values.mkString, relPath),
List(glob.values.mkString),
scripts.value(0).value,
scripts.scopePath.subPath
)
}

val excludedGeneratorPath = excludeScripts.match {
case Some(true) => scripts.value
case _ => List.empty[Positioned[String]]
}

Right(BuildOptions(
sourceGeneratorOptions = SourceGeneratorOptions(generatorConfig = generatorConfigs),
internal = InternalOptions(exclude = excludedGeneratorPath)
))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ final case class BuildOptions(
testOptions: TestOptions = TestOptions(),
notForBloopOptions: PostBuildOptions = PostBuildOptions(),
sourceGeneratorOptions: SourceGeneratorOptions = SourceGeneratorOptions(),
useBuildServer: Option[Boolean] = None
useBuildServer: Option[Boolean] = None,
) {

import BuildOptions.JavaHomeInfo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package scala.build.options

import scala.build.Positioned
import scala.build.errors.{BuildException, MalformedInputError}

final case class GeneratorConfig(
inputDir: String,
glob: List[String],
commandFilePath: String,
outputPath: os.SubPath
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ package scala.build.options
final case class SourceGeneratorOptions(
useBuildInfo: Option[Boolean] = None,
projectVersion: Option[String] = None,
computeVersion: Option[ComputeVersion] = None
computeVersion: Option[ComputeVersion] = None,
generatorConfig: Seq[GeneratorConfig] = Nil,
)

object SourceGeneratorOptions {
Expand Down