Skip to content

Commit 19b8f0c

Browse files
Merge pull request #1336 from alexarchambault/repl-python
Add --python to repl command
2 parents 6bc6cbf + af5b021 commit 19b8f0c

File tree

7 files changed

+220
-78
lines changed

7 files changed

+220
-78
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: 171 additions & 70 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) =>
@@ -117,6 +124,17 @@ object Repl extends ScalaCommand[ReplOptions] {
117124
case Right(()) =>
118125
}
119126
}
127+
def doRunReplFromBuild(
128+
build: Build.Successful,
129+
allowExit: Boolean
130+
): Unit =
131+
doRunRepl(
132+
build.options,
133+
build.artifacts,
134+
build.outputOpt,
135+
allowExit,
136+
Some(build)
137+
)
120138

121139
val cross = options.sharedRepl.compileCross.cross.getOrElse(false)
122140
val configDb = options.shared.configDb
@@ -131,7 +149,8 @@ object Repl extends ScalaCommand[ReplOptions] {
131149
initialBuildOptions,
132150
artifacts,
133151
None,
134-
allowExit = !options.sharedRepl.watch.watchMode
152+
allowExit = !options.sharedRepl.watch.watchMode,
153+
buildOpt = None
135154
)
136155
if (options.sharedRepl.watch.watchMode) {
137156
// nothing to watch, just wait for Ctrl+C
@@ -154,10 +173,9 @@ object Repl extends ScalaCommand[ReplOptions] {
154173
) { res =>
155174
for (builds <- res.orReport(logger))
156175
builds.main match {
157-
case s: Build.Successful =>
158-
doRunRepl(s.options, s.artifacts, s.outputOpt, allowExit = false)
159-
case _: Build.Failed => buildFailed(allowExit = false)
160-
case _: Build.Cancelled => buildCancelled(allowExit = false)
176+
case s: Build.Successful => doRunReplFromBuild(s, allowExit = false)
177+
case _: Build.Failed => buildFailed(allowExit = false)
178+
case _: Build.Cancelled => buildCancelled(allowExit = false)
161179
}
162180
}
163181
try WatchUtil.waitForCtrlC()
@@ -178,14 +196,22 @@ object Repl extends ScalaCommand[ReplOptions] {
178196
)
179197
.orExit(logger)
180198
builds.main match {
181-
case s: Build.Successful =>
182-
doRunRepl(s.options, s.artifacts, s.outputOpt, allowExit = true)
183-
case _: Build.Failed => buildFailed(allowExit = true)
184-
case _: Build.Cancelled => buildCancelled(allowExit = true)
199+
case s: Build.Successful => doRunReplFromBuild(s, allowExit = true)
200+
case _: Build.Failed => buildFailed(allowExit = true)
201+
case _: Build.Cancelled => buildCancelled(allowExit = true)
185202
}
186203
}
187204
}
188205

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+
189215
private def runRepl(
190216
options: BuildOptions,
191217
programArgs: Seq[String],
@@ -194,44 +220,57 @@ object Repl extends ScalaCommand[ReplOptions] {
194220
directories: scala.build.Directories,
195221
logger: Logger,
196222
allowExit: Boolean,
197-
dryRun: Boolean
223+
dryRun: Boolean,
224+
buildOpt: Option[Build.Successful]
198225
): Either[BuildException, Unit] = either {
199226

227+
val setupPython = options.notForBloopOptions.python.getOrElse(false)
228+
200229
val cache = options.internal.cache.getOrElse(FileCache())
201230
val shouldUseAmmonite = options.notForBloopOptions.replOptions.useAmmonite
202-
val replArtifacts = value {
203-
val scalaParams = artifacts.scalaOpt
204-
.getOrElse {
205-
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"
206248
}
207-
.params
208-
val maybeReplArtifacts =
209-
if (shouldUseAmmonite)
210-
ReplArtifacts.ammonite(
211-
scalaParams,
212-
options.notForBloopOptions.replOptions.ammoniteVersion,
213-
artifacts.userDependencies,
214-
artifacts.extraClassPath,
215-
artifacts.extraSourceJars,
216-
logger,
217-
cache,
218-
directories
219-
)
220-
else
221-
ReplArtifacts.default(
222-
scalaParams,
223-
artifacts.userDependencies,
224-
artifacts.extraClassPath,
225-
logger,
226-
cache,
227-
options.finalRepositories
228-
)
229-
maybeReplArtifacts match {
230-
case Left(FetchingDependenciesError(e: ResolutionError.CantDownloadModule, positions))
231-
if shouldUseAmmonite && e.module.name.value == s"ammonite_${scalaParams.scalaVersion}" =>
232-
Left(CantDownloadAmmoniteError(e.version, scalaParams.scalaVersion, e, positions))
233-
case either @ _ => either
234249
}
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
235274
}
236275

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

259-
val additionalArgs =
260-
if (shouldUseAmmonite)
261-
options.notForBloopOptions.replOptions.ammoniteArgs
262-
else
263-
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+
}
264318

265-
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+
}
266344

267-
if (dryRun)
268-
logger.message("Dry run, not running REPL.")
269-
else
270-
Runner.runJvm(
271-
options.javaHome().value.javaCommand,
272-
replArtifacts.replJavaOpts ++ options.javaOptions.javaOpts.toSeq.map(_.value.value),
273-
classDir.toSeq ++ replArtifacts.replClassPath,
274-
replArtifacts.replMainClass,
275-
if (Properties.isWin)
276-
replArgs.map { a =>
277-
if (a.contains(" ")) "\"" + a.replace("\"", "\\\"") + "\""
278-
else a
279-
}
280-
else
281-
replArgs,
345+
def defaultArtifacts(): Either[BuildException, ReplArtifacts] =
346+
ReplArtifacts.default(
347+
scalaParams,
348+
artifacts.userDependencies,
349+
artifacts.extraClassPath,
282350
logger,
283-
allowExecve = allowExit
284-
).waitFor()
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,
362+
logger,
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+
}
285383
}
384+
385+
final class ReplError(retCode: Int)
386+
extends BuildException(s"Failed to run REPL (exit code: $retCode)")
286387
}

0 commit comments

Comments
 (0)