Skip to content

Commit af5b021

Browse files
Add --python to repl command
1 parent 0ab6793 commit af5b021

File tree

7 files changed

+205
-71
lines changed

7 files changed

+205
-71
lines changed

modules/build/src/main/scala/scala/build/ReplArtifacts.scala

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ object ReplArtifacts {
4444
extraSourceJars: Seq[os.Path],
4545
logger: Logger,
4646
cache: FileCache[Task],
47-
directories: Directories
47+
directories: Directories,
48+
addScalapy: Option[String]
4849
): Either[BuildException, ReplArtifacts] = either {
4950
val localRepoOpt = LocalRepo.localRepo(directories.localRepoDir)
50-
val allDeps = dependencies ++ Seq(dep"com.lihaoyi:::ammonite:$ammoniteVersion")
51+
val scalapyDeps = addScalapy.map(ver => dep"me.shadaj::scalapy-core::$ver").toSeq
52+
val allDeps = dependencies ++ Seq(dep"com.lihaoyi:::ammonite:$ammoniteVersion") ++ scalapyDeps
5153
val replArtifacts = Artifacts.artifacts(
5254
Positioned.none(allDeps),
5355
localRepoOpt.toSeq,
@@ -79,13 +81,15 @@ object ReplArtifacts {
7981
extraClassPath: Seq[os.Path],
8082
logger: Logger,
8183
cache: FileCache[Task],
82-
repositories: Seq[String]
84+
repositories: Seq[String],
85+
addScalapy: Option[String]
8386
): Either[BuildException, ReplArtifacts] = either {
8487
val isScala2 = scalaParams.scalaVersion.startsWith("2.")
8588
val replDep =
8689
if (isScala2) dep"org.scala-lang:scala-compiler:${scalaParams.scalaVersion}"
8790
else dep"org.scala-lang::scala3-compiler:${scalaParams.scalaVersion}"
88-
val allDeps = dependencies ++ Seq(replDep)
91+
val scalapyDeps = addScalapy.map(ver => dep"me.shadaj::scalapy-core::$ver").toSeq
92+
val allDeps = dependencies ++ Seq(replDep) ++ scalapyDeps
8993
val replArtifacts =
9094
Artifacts.artifacts(
9195
Positioned.none(allDeps),

modules/cli-options/src/main/scala/scala/cli/commands/SharedReplOptions.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ final case class SharedReplOptions(
1111
watch: SharedWatchOptions = SharedWatchOptions(),
1212
@Recurse
1313
compileCross: CrossOptions = CrossOptions(),
14+
@Recurse
15+
sharedPython: SharedPythonOptions = SharedPythonOptions(),
1416

1517
@Group("Repl")
1618
@HelpMessage("[restricted] Use Ammonite (instead of the default Scala REPL)")

modules/cli/src/main/scala/scala/cli/commands/Repl.scala

Lines changed: 156 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
package scala.cli.commands
22

3+
import ai.kien.python.Python
34
import caseapp._
45
import coursier.cache.FileCache
56
import coursier.error.{FetchError, ResolutionError}
7+
import dependency._
68

79
import scala.build.EitherCps.{either, value}
810
import scala.build._
911
import scala.build.errors.{BuildException, CantDownloadAmmoniteError, FetchingDependenciesError}
10-
import scala.build.internal.Runner
12+
import scala.build.internal.{Constants, Runner}
1113
import scala.build.options.{BuildOptions, JavaOpt, Scope}
1214
import scala.cli.CurrentParams
13-
import scala.cli.commands.Run.maybePrintSimpleScalacOutput
15+
import scala.cli.commands.Run.{maybePrintSimpleScalacOutput, orPythonDetectionError}
1416
import scala.cli.commands.publish.ConfigUtil._
1517
import scala.cli.commands.util.CommonOps._
1618
import scala.cli.commands.util.SharedOptionsUtil._
@@ -55,7 +57,9 @@ object Repl extends ScalaCommand[ReplOptions] {
5557
useAmmoniteOpt = ammonite,
5658
ammoniteVersionOpt = ammoniteVersionOpt,
5759
ammoniteArgs = ammoniteArg
58-
)
60+
),
61+
python = sharedPython.python,
62+
pythonSetup = sharedPython.pythonSetup
5963
),
6064
internalDependencies = baseOptions.internalDependencies.copy(
6165
addRunnerDependencyOpt = baseOptions.internalDependencies.addRunnerDependencyOpt
@@ -70,7 +74,8 @@ object Repl extends ScalaCommand[ReplOptions] {
7074
Inputs.empty(Os.pwd, options.shared.markdown.enableMarkdown)
7175
}
7276
val logger = options.shared.logger
73-
val inputs = options.shared.inputs(args.all, defaultInputs = () => Some(default)).orExit(logger)
77+
val inputs =
78+
options.shared.inputs(args.remaining, defaultInputs = () => Some(default)).orExit(logger)
7479
val programArgs = args.unparsed
7580
CurrentParams.workspaceOpt = Some(inputs.workspace)
7681

@@ -98,7 +103,8 @@ object Repl extends ScalaCommand[ReplOptions] {
98103
buildOptions: BuildOptions,
99104
artifacts: Artifacts,
100105
classDir: Option[os.Path],
101-
allowExit: Boolean
106+
allowExit: Boolean,
107+
buildOpt: Option[Build.Successful]
102108
): Unit = {
103109
val res = runRepl(
104110
buildOptions,
@@ -108,7 +114,8 @@ object Repl extends ScalaCommand[ReplOptions] {
108114
directories,
109115
logger,
110116
allowExit = allowExit,
111-
options.sharedRepl.replDryRun
117+
options.sharedRepl.replDryRun,
118+
buildOpt
112119
)
113120
res match {
114121
case Left(ex) =>
@@ -125,7 +132,8 @@ object Repl extends ScalaCommand[ReplOptions] {
125132
build.options,
126133
build.artifacts,
127134
build.outputOpt,
128-
allowExit
135+
allowExit,
136+
Some(build)
129137
)
130138

131139
val cross = options.sharedRepl.compileCross.cross.getOrElse(false)
@@ -141,7 +149,8 @@ object Repl extends ScalaCommand[ReplOptions] {
141149
initialBuildOptions,
142150
artifacts,
143151
None,
144-
allowExit = !options.sharedRepl.watch.watchMode
152+
allowExit = !options.sharedRepl.watch.watchMode,
153+
buildOpt = None
145154
)
146155
if (options.sharedRepl.watch.watchMode) {
147156
// nothing to watch, just wait for Ctrl+C
@@ -194,6 +203,15 @@ object Repl extends ScalaCommand[ReplOptions] {
194203
}
195204
}
196205

206+
private def maybeAdaptForWindows(args: Seq[String]): Seq[String] =
207+
if (Properties.isWin)
208+
args.map { a =>
209+
if (a.contains(" ")) "\"" + a.replace("\"", "\\\"") + "\""
210+
else a
211+
}
212+
else
213+
args
214+
197215
private def runRepl(
198216
options: BuildOptions,
199217
programArgs: Seq[String],
@@ -202,44 +220,57 @@ object Repl extends ScalaCommand[ReplOptions] {
202220
directories: scala.build.Directories,
203221
logger: Logger,
204222
allowExit: Boolean,
205-
dryRun: Boolean
223+
dryRun: Boolean,
224+
buildOpt: Option[Build.Successful]
206225
): Either[BuildException, Unit] = either {
207226

227+
val setupPython = options.notForBloopOptions.python.getOrElse(false)
228+
208229
val cache = options.internal.cache.getOrElse(FileCache())
209230
val shouldUseAmmonite = options.notForBloopOptions.replOptions.useAmmonite
210-
val replArtifacts = value {
211-
val scalaParams = artifacts.scalaOpt
212-
.getOrElse {
213-
sys.error("Expected Scala artifacts to be fetched")
231+
232+
val scalaParams = artifacts.scalaOpt
233+
.getOrElse {
234+
sys.error("Expected Scala artifacts to be fetched")
235+
}
236+
.params
237+
238+
val scalapyJavaOpts =
239+
if (setupPython) {
240+
val props = value {
241+
val python = Python()
242+
val propsOrError = python.scalapyProperties
243+
logger.debug(s"Python Java properties: $propsOrError")
244+
propsOrError.orPythonDetectionError
245+
}
246+
props.toVector.sorted.map {
247+
case (k, v) => s"-D$k=$v"
214248
}
215-
.params
216-
val maybeReplArtifacts =
217-
if (shouldUseAmmonite)
218-
ReplArtifacts.ammonite(
219-
scalaParams,
220-
options.notForBloopOptions.replOptions.ammoniteVersion,
221-
artifacts.userDependencies,
222-
artifacts.extraClassPath,
223-
artifacts.extraSourceJars,
224-
logger,
225-
cache,
226-
directories
227-
)
228-
else
229-
ReplArtifacts.default(
230-
scalaParams,
231-
artifacts.userDependencies,
232-
artifacts.extraClassPath,
233-
logger,
234-
cache,
235-
options.finalRepositories
236-
)
237-
maybeReplArtifacts match {
238-
case Left(FetchingDependenciesError(e: ResolutionError.CantDownloadModule, positions))
239-
if shouldUseAmmonite && e.module.name.value == s"ammonite_${scalaParams.scalaVersion}" =>
240-
Left(CantDownloadAmmoniteError(e.version, scalaParams.scalaVersion, e, positions))
241-
case either @ _ => either
242249
}
250+
else
251+
Nil
252+
253+
def additionalArgs = {
254+
val pythonArgs =
255+
if (setupPython && scalaParams.scalaVersion.startsWith("2.13."))
256+
Seq("-Yimports:java.lang,scala,scala.Predef,me.shadaj.scalapy")
257+
else
258+
Nil
259+
pythonArgs ++ options.scalaOptions.scalacOptions.toSeq.map(_.value.value)
260+
}
261+
262+
def ammoniteAdditionalArgs() = {
263+
val pythonPredef =
264+
if (setupPython)
265+
"""import me.shadaj.scalapy.py
266+
|import me.shadaj.scalapy.py.PyQuote
267+
|""".stripMargin
268+
else
269+
""
270+
val predefArgs =
271+
if (pythonPredef.isEmpty) Nil
272+
else Seq("--predef-code", pythonPredef)
273+
predefArgs ++ options.notForBloopOptions.replOptions.ammoniteArgs
243274
}
244275

245276
// TODO Warn if some entries of artifacts.classPath were evicted in replArtifacts.replClassPath
@@ -264,31 +295,93 @@ object Repl extends ScalaCommand[ReplOptions] {
264295
" These will not be accessible from the REPL."
265296
)
266297

267-
val additionalArgs =
268-
if (shouldUseAmmonite)
269-
options.notForBloopOptions.replOptions.ammoniteArgs
270-
else
271-
options.scalaOptions.scalacOptions.toSeq.map(_.value.value)
298+
def actualBuild: Build.Successful =
299+
buildOpt.getOrElse {
300+
val ws = os.temp.dir()
301+
val inputs = Inputs.empty(ws, enableMarkdown = false)
302+
val sources = Sources(Nil, Nil, None, Nil, options)
303+
val scope = Scope.Main
304+
Build.Successful(
305+
inputs = inputs,
306+
options = options,
307+
scalaParams = Some(scalaParams),
308+
scope = scope,
309+
sources = Sources(Nil, Nil, None, Nil, options),
310+
artifacts = artifacts,
311+
project = value(Build.buildProject(inputs, sources, Nil, options, None, scope, logger)),
312+
output = classDir.getOrElse(ws),
313+
diagnostics = None,
314+
generatedSources = Nil,
315+
isPartial = false
316+
)
317+
}
272318

273-
val replArgs = additionalArgs ++ programArgs
319+
def maybeRunRepl(
320+
replArtifacts: ReplArtifacts,
321+
replArgs: Seq[String],
322+
extraEnv: Map[String, String] = Map.empty,
323+
extraProps: Map[String, String] = Map.empty
324+
): Unit =
325+
if (dryRun)
326+
logger.message("Dry run, not running REPL.")
327+
else {
328+
val retCode = Runner.runJvm(
329+
options.javaHome().value.javaCommand,
330+
scalapyJavaOpts ++
331+
replArtifacts.replJavaOpts ++
332+
options.javaOptions.javaOpts.toSeq.map(_.value.value) ++
333+
extraProps.toVector.sorted.map { case (k, v) => s"-D$k=$v" },
334+
classDir.toSeq ++ replArtifacts.replClassPath,
335+
replArtifacts.replMainClass,
336+
maybeAdaptForWindows(replArgs),
337+
logger,
338+
allowExecve = allowExit,
339+
extraEnv = extraEnv
340+
).waitFor()
341+
if (retCode != 0)
342+
value(Left(new ReplError(retCode)))
343+
}
274344

275-
if (dryRun)
276-
logger.message("Dry run, not running REPL.")
277-
else
278-
Runner.runJvm(
279-
options.javaHome().value.javaCommand,
280-
replArtifacts.replJavaOpts ++ options.javaOptions.javaOpts.toSeq.map(_.value.value),
281-
classDir.toSeq ++ replArtifacts.replClassPath,
282-
replArtifacts.replMainClass,
283-
if (Properties.isWin)
284-
replArgs.map { a =>
285-
if (a.contains(" ")) "\"" + a.replace("\"", "\\\"") + "\""
286-
else a
287-
}
288-
else
289-
replArgs,
345+
def defaultArtifacts(): Either[BuildException, ReplArtifacts] =
346+
ReplArtifacts.default(
347+
scalaParams,
348+
artifacts.userDependencies,
349+
artifacts.extraClassPath,
350+
logger,
351+
cache,
352+
options.finalRepositories,
353+
addScalapy = if (setupPython) Some(Constants.scalaPyVersion) else None
354+
)
355+
def ammoniteArtifacts(): Either[BuildException, ReplArtifacts] =
356+
ReplArtifacts.ammonite(
357+
scalaParams,
358+
options.notForBloopOptions.replOptions.ammoniteVersion,
359+
artifacts.userDependencies,
360+
artifacts.extraClassPath,
361+
artifacts.extraSourceJars,
290362
logger,
291-
allowExecve = allowExit
292-
).waitFor()
363+
cache,
364+
directories,
365+
addScalapy = if (setupPython) Some(Constants.scalaPyVersion) else None
366+
).left.map {
367+
case FetchingDependenciesError(e: ResolutionError.CantDownloadModule, positions)
368+
if shouldUseAmmonite && e.module.name.value == s"ammonite_${scalaParams.scalaVersion}" =>
369+
CantDownloadAmmoniteError(e.version, scalaParams.scalaVersion, e, positions)
370+
case other => other
371+
}
372+
373+
if (shouldUseAmmonite) {
374+
val replArtifacts = value(ammoniteArtifacts())
375+
val replArgs = ammoniteAdditionalArgs() ++ programArgs
376+
maybeRunRepl(replArtifacts, replArgs)
377+
}
378+
else {
379+
val replArtifacts = value(defaultArtifacts())
380+
val replArgs = additionalArgs ++ programArgs
381+
maybeRunRepl(replArtifacts, replArgs)
382+
}
293383
}
384+
385+
final class ReplError(retCode: Int)
386+
extends BuildException(s"Failed to run REPL (exit code: $retCode)")
294387
}

modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ abstract class ReplTestDefinitions(val scalaVersionOpt: Option[String])
99

1010
private lazy val extraOptions = scalaVersionArgs ++ TestUtil.extraOptions
1111

12-
protected def versionNumberString: String = actualScalaVersion
12+
protected def versionNumberString: String =
13+
if (actualScalaVersion.startsWith("2.")) actualScalaVersion
14+
// Scala 3 gives the 2.13 version it depends on for its standard library.
15+
// Assuming it's the same Scala 3 version as the integration tests here.
16+
else Properties.versionNumberString
1317

1418
test("default dry run") {
1519
TestInputs.empty.fromRoot { root =>
@@ -42,4 +46,35 @@ abstract class ReplTestDefinitions(val scalaVersionOpt: Option[String])
4246
ammoniteTest()
4347
}
4448

49+
test("ammonite scalapy") {
50+
TestInputs.empty.fromRoot { root =>
51+
val ammArgs = Seq(
52+
"-c",
53+
"""println("Hello" + " from Scala " + scala.util.Properties.versionNumberString)
54+
|// py.Dynamic.global.print("Hello from", "ScalaPy") // doesn't work
55+
|println(py"'Hello from '" + py"'ScalaPy'")
56+
|""".stripMargin
57+
)
58+
.map {
59+
if (Properties.isWin)
60+
a => if (a.contains(" ")) "\"" + a.replace("\"", "\\\"") + "\"" else a
61+
else
62+
identity
63+
}
64+
.flatMap(arg => Seq("--ammonite-arg", arg))
65+
val res = os.proc(
66+
TestUtil.cli,
67+
"repl",
68+
"-v",
69+
"-v",
70+
extraOptions,
71+
"--ammonite",
72+
"--python",
73+
ammArgs
74+
).call(cwd = root)
75+
val lines = res.out.trim().linesIterator.toVector
76+
expect(lines == Seq(s"Hello from Scala $versionNumberString", "Hello from ScalaPy"))
77+
}
78+
}
79+
4580
}

0 commit comments

Comments
 (0)