Skip to content
Draft
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