diff --git a/modules/build/src/main/scala/scala/build/Build.scala b/modules/build/src/main/scala/scala/build/Build.scala index 332a7da206..b2369fdf11 100644 --- a/modules/build/src/main/scala/scala/build/Build.scala +++ b/modules/build/src/main/scala/scala/build/Build.scala @@ -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 { @@ -1014,7 +1016,8 @@ object Build { resourceDirs = sources.resourceDirs, scope = scope, javaHomeOpt = Option(options.javaHomeLocation().value), - javacOptions = javacOptions + javacOptions = javacOptions, + generateSource = Option(sourceGeneratorConfig) ) project } diff --git a/modules/build/src/main/scala/scala/build/Project.scala b/modules/build/src/main/scala/scala/build/Project.scala index f6d793be82..3e3eaf975d 100644 --- a/modules/build/src/main/scala/scala/build/Project.scala +++ b/modules/build/src/main/scala/scala/build/Project.scala @@ -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, @@ -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._ @@ -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, @@ -65,7 +86,8 @@ final case class Project( platform = Some(platform), `scala` = scalaConfigOpt, java = Some(BloopConfig.Java(javacOptions)), - resolution = resolution + resolution = resolution, + sourceGenerators = sourceGenerator ) } diff --git a/modules/build/src/main/scala/scala/build/preprocessing/DirectivesPreprocessor.scala b/modules/build/src/main/scala/scala/build/preprocessing/DirectivesPreprocessor.scala index 3006196cf4..cda127e6cc 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/DirectivesPreprocessor.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/DirectivesPreprocessor.scala @@ -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[ diff --git a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala index 1201bf3b71..da5114cfa3 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala @@ -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)) diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/SourceGenerator.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/SourceGenerator.scala new file mode 100644 index 0000000000..e71dd62f21 --- /dev/null +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/SourceGenerator.scala @@ -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) + )) + } +} diff --git a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala index 695022ec04..e8d34db3ab 100644 --- a/modules/options/src/main/scala/scala/build/options/BuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/BuildOptions.scala @@ -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 diff --git a/modules/options/src/main/scala/scala/build/options/GeneratorConfig.scala b/modules/options/src/main/scala/scala/build/options/GeneratorConfig.scala new file mode 100644 index 0000000000..af87b14777 --- /dev/null +++ b/modules/options/src/main/scala/scala/build/options/GeneratorConfig.scala @@ -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 +) diff --git a/modules/options/src/main/scala/scala/build/options/SourceGeneratorOptions.scala b/modules/options/src/main/scala/scala/build/options/SourceGeneratorOptions.scala index fca0cf7967..50d8d98bc1 100644 --- a/modules/options/src/main/scala/scala/build/options/SourceGeneratorOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/SourceGeneratorOptions.scala @@ -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 {