Skip to content

Add object wrapper flag and directive #2136

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,14 @@ case object ScriptPreprocessor extends Preprocessor {
// try to match in multiline mode, don't match comment lines starting with '//'
val containsMainAnnot = "(?m)^(?!//).*@main.*".r.findFirstIn(scriptCode).isDefined

val wrapScriptFun = (cw: CodeWrapper) => {
if (containsMainAnnot) logger.diagnostic(
cw match {
case _: ObjectCodeWrapper.type =>
WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ true)
case _ => WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ false)
}
)

val (code, topWrapperLineCount, _) = cw.wrapCode(
pkg,
wrapper,
scriptCode,
inputArgPath.getOrElse(subPath.last)
)
(code, topWrapperLineCount)
}
val wrapScriptFun = getScriptWrappingFunction(
logger,
containsMainAnnot,
pkg,
wrapper,
scriptCode,
inputArgPath.getOrElse(subPath.toString)
)

val className = (pkg :+ wrapper).map(_.raw).mkString(".")
val relPath = os.rel / (subPath / os.up) / s"${subPath.last.stripSuffix(".sc")}.scala"
Expand All @@ -135,6 +126,32 @@ case object ScriptPreprocessor extends Preprocessor {
List(file)
}

def getScriptWrappingFunction(
logger: Logger,
containsMainAnnot: Boolean,
packageStrings: Seq[Name],
wrapperName: Name,
scriptCode: String,
scriptPath: String
): CodeWrapper => (String, Int) = {
(codeWrapper: CodeWrapper) =>
if (containsMainAnnot) logger.diagnostic(
codeWrapper match {
case _: ObjectCodeWrapper.type =>
WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ true)
case _ => WarningMessages.mainAnnotationNotSupported( /* annotationIgnored */ false)
}
)

val (code, topWrapperLineCount, _) = codeWrapper.wrapCode(
packageStrings,
wrapperName,
scriptCode,
scriptPath
)
(code, topWrapperLineCount)
}

/** Get correct script wrapper depending on the platform and version of Scala. For Scala 2 or
* Platform JS use [[ObjectCodeWrapper]]. Otherwise - for Scala 3 on JVM or Native use
* [[ClassCodeWrapper]].
Expand All @@ -143,16 +160,18 @@ case object ScriptPreprocessor extends Preprocessor {
* @return
* code wrapper compatible with provided BuildOptions
*/
def getScriptWrapper(buildOptions: BuildOptions): CodeWrapper = {
val scalaVersionOpt = for {
maybeScalaVersion <- buildOptions.scalaOptions.scalaVersion
scalaVersion <- maybeScalaVersion.versionOpt
} yield scalaVersion
buildOptions.scalaOptions.platform.map(_.value) match {
case Some(_: Platform.JS.type) => ObjectCodeWrapper
case _ if scalaVersionOpt.exists(_.startsWith("2")) => ObjectCodeWrapper
case _ => ClassCodeWrapper
def getScriptWrapper(buildOptions: BuildOptions): CodeWrapper =
buildOptions.scriptOptions.forceObjectWrapper match {
case Some(true) => ObjectCodeWrapper
case _ =>
val scalaVersionOpt = for {
maybeScalaVersion <- buildOptions.scalaOptions.scalaVersion
scalaVersion <- maybeScalaVersion.versionOpt
} yield scalaVersion
buildOptions.scalaOptions.platform.map(_.value) match {
case Some(_: Platform.JS.type) => ObjectCodeWrapper
case _ if scalaVersionOpt.exists(_.startsWith("2")) => ObjectCodeWrapper
case _ => ClassCodeWrapper
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ object DirectivesPreprocessingUtils {
directives.JavaHome.handler,
directives.Jvm.handler,
directives.MainClass.handler,
directives.ObjectWrapper.handler,
directives.Packaging.handler,
directives.Platform.handler,
directives.Plugin.handler,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ abstract class BuildTests(server: Boolean) extends munit.FunSuite {
scalaVersion = Some(MaybeScalaVersion(sv2)),
scalaBinaryVersion = None
),
scriptOptions = ScriptOptions(Some(ObjectCodeWrapper))
scriptOptions = ScriptOptions(Some(true))
)

def sv3 = "3.0.0"
Expand All @@ -68,7 +68,7 @@ abstract class BuildTests(server: Boolean) extends munit.FunSuite {
scalaVersion = Some(MaybeScalaVersion(sv3)),
scalaBinaryVersion = None
),
scriptOptions = ScriptOptions(Some(ClassCodeWrapper))
scriptOptions = ScriptOptions(None)
)

def simple(checkResults: Boolean = true): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ final case class SharedOptions(
withToolkit: Option[String] = None,
@HelpMessage("Exclude sources")
exclude: List[String] = Nil,
@HelpMessage("Force object wrapper for scripts")
@Tag(tags.experimental)
objectWrapper: Option[Boolean] = None,
) extends HasGlobalOptions {
// format: on

Expand Down Expand Up @@ -348,7 +351,7 @@ final case class SharedOptions(
platform = platformOpt.map(o => Positioned(List(Position.CommandLine()), o))
),
scriptOptions = bo.ScriptOptions(
codeWrapper = None
forceObjectWrapper = objectWrapper
),
scalaJsOptions = scalaJsOptions(js),
scalaNativeOptions = scalaNativeOptions(native),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ case class Name(raw: String) {
assert(raw.charAt(0) != '`', "Cannot create already-backticked identifiers")
override def toString = s"Name($backticked)"
def encoded = NameTransformer.encode(raw)
def backticked = Name.backtickWrap(raw)
def backticked = Name.backtickWrap(encoded)
}

object Name {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package scala.build.preprocessing.directives

import scala.build.directives.*
import scala.build.errors.BuildException
import scala.build.options.{BuildOptions, ScriptOptions}
import scala.cli.commands.SpecificationLevel

@DirectiveExamples("//> using objectWrapper")
@DirectiveUsage("//> using objectWrapper", "`//> using objectWrapper")
@DirectiveDescription("Set the default code wrapper for scripts to object wrapper")
@DirectiveLevel(SpecificationLevel.RESTRICTED)
final case class ObjectWrapper(
@DirectiveName("object.wrapper")
objectWrapper: Boolean = false
) extends HasBuildOptions {
def buildOptions: Either[BuildException, BuildOptions] =
val options = BuildOptions(scriptOptions =
ScriptOptions(forceObjectWrapper = Some(true))
)
Right(options)
}

object ObjectWrapper {
val handler: DirectiveHandler[ObjectWrapper] = DirectiveHandler.derive
}
Original file line number Diff line number Diff line change
Expand Up @@ -524,5 +524,50 @@ trait RunScriptTestDefinitions { _: RunTestDefinitions =>
.contains("Annotation @main in .sc scripts is not supported"))
}
}

test("object-wrapped script forced") {
val inputs = TestInputs(
os.rel / "script.sc" ->
"""//> using dep "com.lihaoyi::os-lib:0.9.1"
|@main def main(args: String*): Unit = println("Hello")
|""".stripMargin,
os.rel / "script-with-directive.sc" ->
"""//> using dep "com.lihaoyi::os-lib:0.9.1"
|//> using object.wrapper
|@main def main(args: String*): Unit = println("Hello")
|""".stripMargin,
os.rel / "munit.sc" ->
"""//> using scala "3.2.2"
|//> using dep "org.scalatest::scalatest:3.2.15"
|
|import org.scalatest.*, flatspec.*, matchers.*
|
|class PiTest extends AnyFlatSpec with should.Matchers {
| "pi calculus" should "return a precise enough pi value" in {
| math.Pi shouldBe 3.14158d +- 0.001d
| }
|}
|org.scalatest.tools.Runner.main(Array("-oDF", "-s", classOf[PiTest].getName))""".stripMargin
)
inputs.fromRoot { root =>
val res = os.proc(TestUtil.cli, "--power", "script.sc", "munit.sc", "--object-wrapper")
.call(cwd = root, mergeErrIntoOut = true, stdout = os.Pipe)

val outputNormalized: String = normalizeConsoleOutput(res.out.text())

expect(outputNormalized.contains(
"[warn] Annotation @main in .sc scripts is not supported, it will be ignored, use .scala format instead"
))

val directiveRes = os.proc(TestUtil.cli, "--power", "script-with-directive.sc", "munit.sc")
.call(cwd = root, mergeErrIntoOut = true, stdout = os.Pipe)

val directiveOutputNormalized: String = normalizeConsoleOutput(directiveRes.out.text())

expect(directiveOutputNormalized.contains(
"[warn] Annotation @main in .sc scripts is not supported, it will be ignored, use .scala format instead"
))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package scala.build.options
import scala.build.internal.CodeWrapper

final case class ScriptOptions(
codeWrapper: Option[CodeWrapper] = None
forceObjectWrapper: Option[Boolean] = None
)

object ScriptOptions {
Expand Down
4 changes: 4 additions & 0 deletions website/docs/reference/cli-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -1505,6 +1505,10 @@ Add toolkit to classPath

Exclude sources

### `--object-wrapper`

Force object wrapper for scripts

## Snippet options

Available in commands:
Expand Down
9 changes: 9 additions & 0 deletions website/docs/reference/directives.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ Specify default main class
#### Examples
`//> using mainClass helloWorld`

### ObjectWrapper

Set the default code wrapper for scripts to object wrapper

`//> using objectWrapper

#### Examples
`//> using objectWrapper`

### Packaging

Set parameters for packaging
Expand Down