Skip to content

Commit ef2ca68

Browse files
authoredOct 18, 2022
Fix: Avoid requiring -i to propagate output (#3)
* Avoid requiring `-i` to propagate output * Return scala-cli stdout instead of printing it
1 parent b71e15f commit ef2ca68

File tree

3 files changed

+101
-11
lines changed

3 files changed

+101
-11
lines changed
 

‎README.md

-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ Benefits:
7878
- more reliable incremental compilation
7979

8080
Drawbacks:
81-
- when compiling things in non-interactive mode, the output of Scala CLI, that prints errors and warnings, is sometimes trapped - use of `./mill -i` is recommended, which slows Mill commands a bit
8281
- no-op incremental compilation (when no sources changed, and nothing new needs to be compiled) has a small but noticeable cost - it takes a small amount of time (maybe in the ~100s of ms), which adds up when running Mill tasks involving numerous modules
8382

8483
Limitations:

‎src/ProcessUtils.scala

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package scala.cli.mill
2+
3+
import mill.main.client.InputPumper
4+
import os.SubProcess
5+
import java.io.PipedInputStream
6+
7+
// Adapted from Mill Jvm.scala https://github.com/com-lihaoyi/mill/blob/f96162ecb41a9dfbac0bc524b77e09093fd61029/main/src/mill/modules/Jvm.scala#L37
8+
// Changes:
9+
// - return stdout instead of printing it
10+
// - return Either[Unit, os.SubProcess.OutputStream] in runSubprocess instead of Unit and to receive os.Shellable* instead of Seq[String]
11+
// - receive os.Shellable* instead of Seq[String]
12+
// - avoid receiving env and cwd since we don't pass them
13+
object ProcessUtils {
14+
/**
15+
* Runs a generic subprocess and waits for it to terminate.
16+
*/
17+
def runSubprocess(command: os.Shellable*): Either[Unit, os.SubProcess.OutputStream] = {
18+
val process = spawnSubprocess(command)
19+
val shutdownHook = new Thread("subprocess-shutdown") {
20+
override def run(): Unit = {
21+
System.err.println("Host JVM shutdown. Forcefully destroying subprocess ...")
22+
process.destroy()
23+
}
24+
}
25+
Runtime.getRuntime().addShutdownHook(shutdownHook)
26+
try {
27+
process.waitFor()
28+
} catch {
29+
case e: InterruptedException =>
30+
System.err.println("Interrupted. Forcefully destroying subprocess ...")
31+
process.destroy()
32+
// rethrow
33+
throw e
34+
} finally {
35+
Runtime.getRuntime().removeShutdownHook(shutdownHook)
36+
}
37+
if (process.exitCode() == 0) Right(process.stdout)
38+
else Left(())
39+
}
40+
41+
/**
42+
* Spawns a generic subprocess, streaming the stdout and stderr to the
43+
* console. If the System.out/System.err have been substituted, makes sure
44+
* that the subprocess's stdout and stderr streams go to the subtituted
45+
* streams
46+
*/
47+
def spawnSubprocess(
48+
command: os.Shellable*
49+
): SubProcess = {
50+
// If System.in is fake, then we pump output manually rather than relying
51+
// on `os.Inherit`. That is because `os.Inherit` does not follow changes
52+
// to System.in/System.out/System.err, so the subprocess's streams get sent
53+
// to the parent process's origin outputs even if we want to direct them
54+
// elsewhere
55+
if (System.in.isInstanceOf[PipedInputStream]) {
56+
val process = os.proc(command).spawn(
57+
stdin = os.Pipe,
58+
stdout = os.Pipe,
59+
stderr = os.Pipe
60+
)
61+
62+
val sources = Seq(
63+
(process.stderr, System.err, "spawnSubprocess.stderr", false, () => true),
64+
(System.in, process.stdin, "spawnSubprocess.stdin", true, () => process.isAlive())
65+
)
66+
67+
for ((std, dest, name, checkAvailable, runningCheck) <- sources) {
68+
val t = new Thread(
69+
new InputPumper(std, dest, checkAvailable, () => runningCheck()),
70+
name
71+
)
72+
t.setDaemon(true)
73+
t.start()
74+
}
75+
76+
process
77+
} else {
78+
os.proc(command).spawn(
79+
stdin = os.Inherit,
80+
stdout = os.Pipe,
81+
stderr = os.Inherit
82+
)
83+
}
84+
}
85+
86+
}

‎src/ScalaCliCompile.scala

+15-10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import coursier.cache.{ArchiveCache, FileCache}
88
import coursier.cache.loggers.{FallbackRefreshDisplay, ProgressBarRefreshDisplay, RefreshLogger}
99
import coursier.util.Artifact
1010
import mill._
11+
import mill.api.Result
1112
import mill.scalalib.ScalaModule
1213
import mill.scalalib.api.CompilationResult
1314

@@ -33,7 +34,7 @@ trait ScalaCliCompile extends ScalaModule {
3334
else
3435
new FallbackRefreshDisplay
3536
)
36-
val cache = FileCache().withLogger(logger)
37+
val cache = FileCache().withLogger(logger)
3738
val artifact = Artifact(url).withChanging(compileScalaCliIsChanging)
3839
val archiveCache = ArchiveCache()
3940
.withCache(cache)
@@ -110,13 +111,13 @@ trait ScalaCliCompile extends ScalaModule {
110111
.filter(os.exists(_))
111112
val workspace = T.dest / "workspace"
112113
os.makeDir.all(workspace)
113-
val classFilesDir =
114-
if (sourceFiles.isEmpty) out / "classes"
114+
val classFilesDirEither =
115+
if (sourceFiles.isEmpty) Right(out / "classes")
115116
else {
116117
def asOpt[T](opt: String, values: IterableOnce[T]): Seq[String] =
117118
values.iterator.toList.flatMap(v => Seq(opt, v.toString))
118119

119-
val proc = os.proc(
120+
val outputEither = ProcessUtils.runSubprocess(
120121
cli,
121122
extraScalaCliHeadOptions(),
122123
Seq("compile", "--classpath"),
@@ -130,13 +131,17 @@ trait ScalaCliCompile extends ScalaModule {
130131
sourceFiles
131132
)
132133

133-
val compile = proc.call()
134-
val out = compile.out.trim()
135-
136-
os.Path(out.split(File.pathSeparator).head)
134+
outputEither.map { output =>
135+
val out = output.trim()
136+
os.Path(out.split(File.pathSeparator).head)
137+
}
137138
}
138-
139-
CompilationResult(out / "unused.txt", PathRef(classFilesDir))
139+
classFilesDirEither match {
140+
case Right(classFilesDir) =>
141+
Result.Success(CompilationResult(out / "unused.txt", PathRef(classFilesDir)))
142+
case Left(()) =>
143+
Result.Failure("Compilation failed")
144+
}
140145
}
141146
}
142147
else

0 commit comments

Comments
 (0)
Please sign in to comment.