Skip to content

Commit 18707eb

Browse files
committed
Add new types to enforce wrapping scripts before building project
1 parent 45227dc commit 18707eb

File tree

9 files changed

+251
-117
lines changed

9 files changed

+251
-117
lines changed

modules/build/src/main/scala/scala/build/CrossSources.scala

Lines changed: 104 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,94 @@ import scala.build.testrunner.DynamicTestRunner.globPattern
2828
import scala.util.Try
2929
import scala.util.chaining.*
3030

31-
final case class CrossSources(
31+
/** CrossSources with unwrapped scripts, use [[withWrappedScripts]] to wrap them and obtain an
32+
* instance of CrossSources
33+
*
34+
* See [[CrossSources]] for more information
35+
*
36+
* @param paths
37+
* paths and realtive paths to sources on disk, wrapped in their build requirements
38+
* @param inMemory
39+
* in memory sources (e.g. snippets) wrapped in their build requirements
40+
* @param defaultMainClass
41+
* @param resourceDirs
42+
* @param buildOptions
43+
* build options from sources
44+
* @param unwrappedScripts
45+
* in memory script sources, their code must be wrapped before compiling
46+
*/
47+
sealed class UnwrappedCrossSources(
3248
paths: Seq[WithBuildRequirements[(os.Path, os.RelPath)]],
3349
inMemory: Seq[WithBuildRequirements[Sources.InMemory]],
3450
defaultMainClass: Option[String],
3551
resourceDirs: Seq[WithBuildRequirements[os.Path]],
36-
buildOptions: Seq[WithBuildRequirements[BuildOptions]]
52+
buildOptions: Seq[WithBuildRequirements[BuildOptions]],
53+
unwrappedScripts: Seq[WithBuildRequirements[Sources.UnwrappedScript]]
3754
) {
3855

56+
/** For all unwrapped script sources contained in this object wrap them according to provided
57+
* BuildOptions
58+
*
59+
* @param buildOptions
60+
* options used to choose the script wrapper
61+
* @return
62+
* CrossSources with all the scripts wrapped
63+
*/
64+
def withWrappedScripts(buildOptions: BuildOptions): CrossSources = {
65+
val codeWrapper = ScriptPreprocessor.getScriptWrapper(buildOptions)
66+
67+
val wrappedScripts = unwrappedScripts.map { unwrapppedWithRequirements =>
68+
unwrapppedWithRequirements.map(_.wrap(codeWrapper))
69+
}
70+
71+
CrossSources(
72+
paths,
73+
inMemory ++ wrappedScripts,
74+
defaultMainClass,
75+
resourceDirs,
76+
this.buildOptions
77+
)
78+
}
79+
3980
def sharedOptions(baseOptions: BuildOptions): BuildOptions =
4081
buildOptions
4182
.filter(_.requirements.isEmpty)
4283
.map(_.value)
4384
.foldLeft(baseOptions)(_ orElse _)
4485

45-
private def needsScalaVersion =
86+
protected def needsScalaVersion =
4687
paths.exists(_.needsScalaVersion) ||
4788
inMemory.exists(_.needsScalaVersion) ||
4889
resourceDirs.exists(_.needsScalaVersion) ||
4990
buildOptions.exists(_.needsScalaVersion)
91+
}
5092

93+
/** Information gathered from preprocessing command inputs - sources and build options from using
94+
* directives
95+
*
96+
* @param paths
97+
* paths and realtive paths to sources on disk, wrapped in their build requirements
98+
* @param inMemory
99+
* in memory sources (e.g. snippets and wrapped scripts) wrapped in their build requirements
100+
* @param defaultMainClass
101+
* @param resourceDirs
102+
* @param buildOptions
103+
* build options from sources
104+
*/
105+
final case class CrossSources(
106+
paths: Seq[WithBuildRequirements[(os.Path, os.RelPath)]],
107+
inMemory: Seq[WithBuildRequirements[Sources.InMemory]],
108+
defaultMainClass: Option[String],
109+
resourceDirs: Seq[WithBuildRequirements[os.Path]],
110+
buildOptions: Seq[WithBuildRequirements[BuildOptions]]
111+
) extends UnwrappedCrossSources(
112+
paths,
113+
inMemory,
114+
defaultMainClass,
115+
resourceDirs,
116+
buildOptions,
117+
Nil
118+
) {
51119
def scopedSources(baseOptions: BuildOptions): Either[BuildException, ScopedSources] = either {
52120

53121
val sharedOptions0 = sharedOptions(baseOptions)
@@ -114,34 +182,6 @@ final case class CrossSources(
114182
crossSources0.buildOptions.map(_.scopedValue(defaultScope))
115183
)
116184
}
117-
118-
/** For all unwrapped script sources contained in this object wrap them according to provided
119-
* BuildOptions
120-
*
121-
* @param buildOptions
122-
* options used to choose the script wrapper
123-
* @return
124-
* this with all the script code wrapped
125-
*/
126-
def withWrappedScripts(buildOptions: BuildOptions): CrossSources =
127-
copy(
128-
inMemory = inMemory.map {
129-
case WithBuildRequirements(requirements, source) if source.wrapScriptFunOpt.isDefined =>
130-
val wrapScriptFun = source.wrapScriptFunOpt.get
131-
val codeWrapper = ScriptPreprocessor.getScriptWrapper(buildOptions)
132-
val (wrappedCode, topWrapperLen) = wrapScriptFun(codeWrapper)
133-
134-
WithBuildRequirements(
135-
requirements,
136-
source.copy(
137-
generatedContent = wrappedCode,
138-
topWrapperLen = topWrapperLen,
139-
wrapScriptFunOpt = None
140-
)
141-
)
142-
case p => p
143-
}
144-
)
145185
}
146186

147187
object CrossSources {
@@ -168,7 +208,7 @@ object CrossSources {
168208
suppressWarningOptions: SuppressWarningOptions,
169209
exclude: Seq[Positioned[String]] = Nil,
170210
maybeRecoverOnError: BuildException => Option[BuildException] = e => Some(e)
171-
)(using ScalaCliInvokeData): Either[BuildException, (CrossSources, Inputs)] = either {
211+
)(using ScalaCliInvokeData): Either[BuildException, (UnwrappedCrossSources, Inputs)] = either {
172212

173213
def preprocessSources(elems: Seq[SingleElement])
174214
: Either[BuildException, Seq[PreprocessedSource]] =
@@ -286,7 +326,17 @@ object CrossSources {
286326
val baseReqs0 = baseReqs(m.scopePath)
287327
WithBuildRequirements(
288328
m.requirements.fold(baseReqs0)(_ orElse baseReqs0),
289-
Sources.InMemory(m.originalPath, m.relPath, m.code, m.ignoreLen, m.wrapScriptFunOpt)
329+
Sources.InMemory(m.originalPath, m.relPath, m.code, m.ignoreLen)
330+
) -> m.directivesPositions
331+
}
332+
val unwrappedScriptsWithDirectivePositions
333+
: Seq[(WithBuildRequirements[Sources.UnwrappedScript], Option[DirectivesPositions])] =
334+
preprocessedSources.collect {
335+
case m: PreprocessedSource.UnwrappedScript =>
336+
val baseReqs0 = baseReqs(m.scopePath)
337+
WithBuildRequirements(
338+
m.requirements.fold(baseReqs0)(_ orElse baseReqs0),
339+
Sources.UnwrappedScript(m.originalPath, m.relPath, m.wrapScriptFun)
290340
) -> m.directivesPositions
291341
}
292342

@@ -298,14 +348,20 @@ object CrossSources {
298348
)
299349

300350
lazy val allPathsWithDirectivesByScope: Map[Scope, Seq[(os.Path, DirectivesPositions)]] =
301-
(pathsWithDirectivePositions ++ inMemoryWithDirectivePositions)
351+
(pathsWithDirectivePositions ++
352+
inMemoryWithDirectivePositions ++
353+
unwrappedScriptsWithDirectivePositions)
302354
.flatMap { (withBuildRequirements, directivesPositions) =>
303355
val scope = withBuildRequirements.scopedValue(Scope.Main).scope
304356
val path: os.Path = withBuildRequirements.value match
305357
case im: Sources.InMemory =>
306358
im.originalPath match
307359
case Right((_, p: os.Path)) => p
308360
case _ => inputs.workspace / im.generatedRelPath
361+
case us: Sources.UnwrappedScript =>
362+
us.originalPath match
363+
case Right((_, p: os.Path)) => p
364+
case _ => inputs.workspace / us.generatedRelPath
309365
case (p: os.Path, _) => p
310366
directivesPositions.map((path, scope, _))
311367
}
@@ -333,9 +389,20 @@ object CrossSources {
333389
}
334390
}
335391

336-
val paths = pathsWithDirectivePositions.map(_._1)
337-
val inMemory = inMemoryWithDirectivePositions.map(_._1)
338-
(CrossSources(paths, inMemory, defaultMainClassOpt, resourceDirs, buildOptions), allInputs)
392+
val paths = pathsWithDirectivePositions.map(_._1)
393+
val inMemory = inMemoryWithDirectivePositions.map(_._1)
394+
val unwrappedScripts = unwrappedScriptsWithDirectivePositions.map(_._1)
395+
(
396+
UnwrappedCrossSources(
397+
paths,
398+
inMemory,
399+
defaultMainClassOpt,
400+
resourceDirs,
401+
buildOptions,
402+
unwrappedScripts
403+
),
404+
allInputs
405+
)
339406
}
340407

341408
private def resolveInputsFromSources(sources: Seq[Positioned[os.Path]], enableMarkdown: Boolean) =

modules/build/src/main/scala/scala/build/Sources.scala

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,20 @@ object Sources {
7070
originalPath: Either[String, (os.SubPath, os.Path)],
7171
generatedRelPath: os.RelPath,
7272
generatedContent: String,
73-
topWrapperLen: Int,
74-
wrapScriptFunOpt: Option[CodeWrapper => (String, Int)] = None
73+
topWrapperLen: Int
7574
)
7675

76+
final case class UnwrappedScript(
77+
originalPath: Either[String, (os.SubPath, os.Path)],
78+
generatedRelPath: os.RelPath,
79+
wrapScriptFun: CodeWrapper => (String, Int)
80+
) {
81+
def wrap(wrapper: CodeWrapper): InMemory = {
82+
val (content, topWrapperLen) = wrapScriptFun(wrapper)
83+
InMemory(originalPath, generatedRelPath, content, topWrapperLen)
84+
}
85+
}
86+
7787
/** The default preprocessor list.
7888
*
7989
* @param codeWrapper

modules/build/src/main/scala/scala/build/preprocessing/PreprocessedSource.scala

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,25 @@ object PreprocessedSource {
3939
scopedRequirements: Seq[Scoped[BuildRequirements]],
4040
mainClassOpt: Option[String],
4141
scopePath: ScopePath,
42-
directivesPositions: Option[DirectivesPositions],
43-
wrapScriptFunOpt: Option[CodeWrapper => (String, Int)] = None
42+
directivesPositions: Option[DirectivesPositions]
4443
) extends PreprocessedSource {
4544
def reportingPath: Either[String, os.Path] =
4645
originalPath.map(_._2)
4746
}
47+
48+
final case class UnwrappedScript(
49+
originalPath: Either[String, (os.SubPath, os.Path)],
50+
relPath: os.RelPath,
51+
options: Option[BuildOptions],
52+
optionsWithTargetRequirements: List[WithBuildRequirements[BuildOptions]],
53+
requirements: Option[BuildRequirements],
54+
scopedRequirements: Seq[Scoped[BuildRequirements]],
55+
mainClassOpt: Option[String],
56+
scopePath: ScopePath,
57+
directivesPositions: Option[DirectivesPositions],
58+
wrapScriptFun: CodeWrapper => (String, Int)
59+
) extends PreprocessedSource
60+
4861
final case class NoSourceCode(
4962
options: Option[BuildOptions],
5063
optionsWithTargetRequirements: List[WithBuildRequirements[BuildOptions]],

modules/build/src/main/scala/scala/build/preprocessing/ScriptPreprocessor.scala

Lines changed: 56 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -76,66 +76,65 @@ case object ScriptPreprocessor extends Preprocessor {
7676
maybeRecoverOnError: BuildException => Option[BuildException],
7777
allowRestrictedFeatures: Boolean,
7878
suppressWarningOptions: SuppressWarningOptions
79-
)(using ScalaCliInvokeData): Either[BuildException, List[PreprocessedSource.InMemory]] = either {
80-
81-
val (contentIgnoredSheBangLines, _) = SheBang.ignoreSheBangLines(content)
82-
83-
val (pkg, wrapper) = AmmUtil.pathToPackageWrapper(subPath)
84-
85-
val processingOutput: ProcessingOutput =
86-
value(ScalaPreprocessor.process(
87-
contentIgnoredSheBangLines,
88-
reportingPath,
89-
scopePath / os.up,
90-
logger,
91-
maybeRecoverOnError,
92-
allowRestrictedFeatures,
93-
suppressWarningOptions
94-
))
95-
.getOrElse(ProcessingOutput.empty)
96-
97-
val scriptCode = processingOutput.updatedContent.getOrElse(contentIgnoredSheBangLines)
98-
// try to match in multiline mode, don't match comment lines starting with '//'
99-
val containsMainAnnot = "(?m)^(?!//).*@main.*".r.findFirstIn(scriptCode).isDefined
100-
101-
val wrapScriptFun = (cw: CodeWrapper) => {
102-
if (containsMainAnnot) logger.diagnostic(
103-
cw match {
104-
case _: ObjectCodeWrapper.type =>
105-
WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ true)
106-
case _ => WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ false)
107-
}
108-
)
109-
110-
val (code, topWrapperLen, _) = cw.wrapCode(
111-
pkg,
112-
wrapper,
113-
scriptCode,
114-
inputArgPath.getOrElse(subPath.last)
79+
)(using ScalaCliInvokeData): Either[BuildException, List[PreprocessedSource.UnwrappedScript]] =
80+
either {
81+
82+
val (contentIgnoredSheBangLines, _) = SheBang.ignoreSheBangLines(content)
83+
84+
val (pkg, wrapper) = AmmUtil.pathToPackageWrapper(subPath)
85+
86+
val processingOutput: ProcessingOutput =
87+
value(ScalaPreprocessor.process(
88+
contentIgnoredSheBangLines,
89+
reportingPath,
90+
scopePath / os.up,
91+
logger,
92+
maybeRecoverOnError,
93+
allowRestrictedFeatures,
94+
suppressWarningOptions
95+
))
96+
.getOrElse(ProcessingOutput.empty)
97+
98+
val scriptCode = processingOutput.updatedContent.getOrElse(contentIgnoredSheBangLines)
99+
// try to match in multiline mode, don't match comment lines starting with '//'
100+
val containsMainAnnot = "(?m)^(?!//).*@main.*".r.findFirstIn(scriptCode).isDefined
101+
102+
val wrapScriptFun = (cw: CodeWrapper) => {
103+
if (containsMainAnnot) logger.diagnostic(
104+
cw match {
105+
case _: ObjectCodeWrapper.type =>
106+
WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ true)
107+
case _ => WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ false)
108+
}
109+
)
110+
111+
val (code, topWrapperLen, _) = cw.wrapCode(
112+
pkg,
113+
wrapper,
114+
scriptCode,
115+
inputArgPath.getOrElse(subPath.last)
116+
)
117+
(code, topWrapperLen)
118+
}
119+
120+
val className = (pkg :+ wrapper).map(_.raw).mkString(".")
121+
val relPath = os.rel / (subPath / os.up) / s"${subPath.last.stripSuffix(".sc")}.scala"
122+
123+
val file = PreprocessedSource.UnwrappedScript(
124+
originalPath = reportingPath.map((subPath, _)),
125+
relPath = relPath,
126+
options = Some(processingOutput.opts),
127+
optionsWithTargetRequirements = processingOutput.optsWithReqs,
128+
requirements = Some(processingOutput.globalReqs),
129+
scopedRequirements = processingOutput.scopedReqs,
130+
mainClassOpt = Some(CodeWrapper.mainClassObject(Name(className)).backticked),
131+
scopePath = scopePath,
132+
directivesPositions = processingOutput.directivesPositions,
133+
wrapScriptFun = wrapScriptFun
115134
)
116-
(code, topWrapperLen)
135+
List(file)
117136
}
118137

119-
val className = (pkg :+ wrapper).map(_.raw).mkString(".")
120-
val relPath = os.rel / (subPath / os.up) / s"${subPath.last.stripSuffix(".sc")}.scala"
121-
122-
val file = PreprocessedSource.InMemory(
123-
originalPath = reportingPath.map((subPath, _)),
124-
relPath = relPath,
125-
code = "", // code is captured in wrapScriptFun's closure
126-
ignoreLen = 0,
127-
options = Some(processingOutput.opts),
128-
optionsWithTargetRequirements = processingOutput.optsWithReqs,
129-
requirements = Some(processingOutput.globalReqs),
130-
scopedRequirements = processingOutput.scopedReqs,
131-
mainClassOpt = Some(CodeWrapper.mainClassObject(Name(className)).backticked),
132-
scopePath = scopePath,
133-
directivesPositions = processingOutput.directivesPositions,
134-
wrapScriptFunOpt = Some(wrapScriptFun)
135-
)
136-
List(file)
137-
}
138-
139138
/** Get correct script wrapper depending on the platform and version of Scala. For Scala 2 or
140139
* Platform JS use [[ObjectCodeWrapper]]. Otherwise - for Scala 3 on JVM or Native use
141140
* [[ClassCodeWrapper]].

0 commit comments

Comments
 (0)