Skip to content

Commit 8381e63

Browse files
authored
Merge pull request #1052 from Gedochao/scalac-help
Support for printing help from scala compiler without passing any sources
2 parents 6962fc8 + 4b4e463 commit 8381e63

File tree

14 files changed

+381
-74
lines changed

14 files changed

+381
-74
lines changed

modules/build/src/main/scala/scala/build/compiler/SimpleJavaCompiler.scala

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ import scala.build.internal.Runner
66
import scala.build.{Logger, Project}
77
import scala.util.Properties
88

9+
/** A simple Java compiler to handle pure Java projects.
10+
*
11+
* @param defaultJavaCommand
12+
* the default `java` command to be used
13+
* @param defaultJavaOptions
14+
* the default jvm options to be used with the `java` command
15+
*/
916
final case class SimpleJavaCompiler(
1017
defaultJavaCommand: String,
1118
defaultJavaOptions: Seq[String]
@@ -16,8 +23,9 @@ final case class SimpleJavaCompiler(
1623
logger: Logger
1724
): Boolean =
1825
project.sources.isEmpty || {
19-
val javacCommand = SimpleJavaCompiler.javaCommand(project, "javac")
20-
.getOrElse(defaultJavaCommand)
26+
val javacCommand =
27+
project.javaHomeOpt.map(javaHome => SimpleJavaCompiler.javaCommand(javaHome, "javac"))
28+
.getOrElse(defaultJavaCommand)
2129

2230
val args = project.javacOptions ++
2331
Seq(
@@ -42,10 +50,9 @@ final case class SimpleJavaCompiler(
4250

4351
object SimpleJavaCompiler {
4452

45-
def javaCommand(project: Project, command: String = "java"): Option[String] =
46-
project.javaHomeOpt.map { javaHome =>
47-
val ext = if (Properties.isWin) ".exe" else ""
48-
val path = javaHome / "bin" / s"$command$ext"
49-
path.toString
50-
}
53+
def javaCommand(javaHome: os.Path, command: String = "java"): String = {
54+
val ext = if (Properties.isWin) ".exe" else ""
55+
val path = javaHome / "bin" / s"$command$ext"
56+
path.toString
57+
}
5158
}

modules/build/src/main/scala/scala/build/compiler/SimpleScalaCompiler.scala

Lines changed: 146 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ import java.io.File
55
import scala.build.internal.Runner
66
import scala.build.{Logger, Positioned, Project}
77

8+
/** A simple Scala compiler designed to handle scaladocs, Java projects & get `scalac` outputs.
9+
*
10+
* @param defaultJavaCommand
11+
* the default `java` command to be used
12+
* @param defaultJavaOptions
13+
* the default jvm options to be used with the `java` command
14+
* @param scaladoc
15+
* a flag for setting whether this compiler will handle scaladocs
16+
*/
817
final case class SimpleScalaCompiler(
918
defaultJavaCommand: String,
1019
defaultJavaOptions: Seq[String],
@@ -24,47 +33,164 @@ final case class SimpleScalaCompiler(
2433
override def usesClassDir: Boolean =
2534
!scaladoc
2635

36+
/** Run a synthetic (created in runtime) `scalac` as a JVM process with the specified parameters
37+
*
38+
* @param mainClass
39+
* the main class of the synthetic Scala compiler
40+
* @param javaHomeOpt
41+
* Java home path (optional)
42+
* @param javacOptions
43+
* options to be passed for the Java compiler
44+
* @param scalacOptions
45+
* options to be passed for the Scala compiler
46+
* @param classPath
47+
* class path to be passed to `scalac`
48+
* @param compilerClassPath
49+
* class path for the Scala compiler itself
50+
* @param sources
51+
* sources to be passed when running `scalac` (optional)
52+
* @param outputDir
53+
* output directory for the compiler (optional)
54+
* @param cwd
55+
* working directory for running the compiler
56+
* @param logger
57+
* logger
58+
* @return
59+
* compiler process exit code
60+
*/
2761
private def runScalacLike(
28-
project: Project,
2962
mainClass: String,
30-
outputDir: os.Path,
63+
javaHomeOpt: Option[os.Path],
64+
javacOptions: Seq[String],
65+
scalacOptions: Seq[String],
66+
classPath: Seq[os.Path],
67+
compilerClassPath: Seq[os.Path],
68+
sources: Seq[String],
69+
outputDir: Option[os.Path],
70+
cwd: os.Path,
3171
logger: Logger
32-
): Boolean = {
72+
): Int = {
3373

34-
os.makeDir.all(outputDir)
74+
outputDir.foreach(os.makeDir.all(_))
3575

3676
// initially adapted from https://github.com/VirtusLab/scala-cli/pull/103/files#diff-d13a7e6d602b8f84d9177e3138487872f0341d006accfe425886a561f029a9c3R120 and around
77+
val outputDirArgs = outputDir.map(od => Seq("-d", od.toString())).getOrElse(Nil)
78+
val classPathArgs =
79+
if (classPath.nonEmpty)
80+
Seq("-cp", classPath.map(_.toString).mkString(File.pathSeparator))
81+
else Nil
3782

38-
val args =
39-
project.scalaCompiler.map(_.scalacOptions).getOrElse(Nil) ++
40-
Seq(
41-
"-d",
42-
outputDir.toString,
43-
"-cp",
44-
project.classPath.map(_.toString).mkString(File.pathSeparator)
45-
) ++
46-
project.sources.map(_.toString)
83+
val args = scalacOptions ++ outputDirArgs ++ classPathArgs ++ sources
4784

48-
val javaCommand = SimpleJavaCompiler.javaCommand(project).getOrElse(defaultJavaCommand)
85+
val javaCommand =
86+
javaHomeOpt.map(SimpleJavaCompiler.javaCommand(_)).getOrElse(defaultJavaCommand)
4987

5088
val javaOptions = defaultJavaOptions ++
51-
project.javacOptions
89+
javacOptions
5290
.filter(_.startsWith("-J"))
5391
.map(_.stripPrefix("-J"))
5492

55-
val res = Runner.runJvm(
93+
Runner.runJvm(
5694
javaCommand,
5795
javaOptions,
58-
project.scalaCompiler.map(_.compilerClassPath.map(_.toIO)).getOrElse(Nil),
96+
compilerClassPath.map(_.toIO),
5997
mainClass,
6098
args,
6199
logger,
62-
cwd = Some(project.workspace)
100+
cwd = Some(cwd)
63101
).waitFor()
102+
}
64103

104+
/** Run a synthetic (created in runtime) `scalac` as a JVM process for a given
105+
* [[scala.build.Project]]
106+
*
107+
* @param project
108+
* project to be compiled
109+
* @param mainClass
110+
* the main class of the synthetic Scala compiler
111+
* @param outputDir
112+
* the scala compiler output directory
113+
* @param logger
114+
* logger
115+
* @return
116+
* true if the process returned no errors, false otherwise
117+
*/
118+
private def runScalacLikeForProject(
119+
project: Project,
120+
mainClass: String,
121+
outputDir: os.Path,
122+
logger: Logger
123+
): Boolean = {
124+
val res = runScalacLike(
125+
mainClass = mainClass,
126+
javaHomeOpt = project.javaHomeOpt,
127+
javacOptions = project.javacOptions,
128+
scalacOptions = project.scalaCompiler.map(_.scalacOptions).getOrElse(Nil),
129+
classPath = project.classPath,
130+
compilerClassPath = project.scalaCompiler.map(_.compilerClassPath).getOrElse(Nil),
131+
sources = project.sources.map(_.toString),
132+
outputDir = Some(outputDir),
133+
cwd = project.workspace,
134+
logger = logger
135+
)
65136
res == 0
66137
}
67138

139+
/** Run a synthetic (created in runtime) `scalac` as a JVM process with minimal parameters. (i.e.
140+
* to print `scalac` help)
141+
*
142+
* @param scalaVersion
143+
* Scala version for which `scalac` is to be created
144+
* @param javaHomeOpt
145+
* Java home path (optional)
146+
* @param javacOptions
147+
* options to be passed for the Java compiler
148+
* @param scalacOptions
149+
* options to be passed for the Scala compiler
150+
* @param fullClassPath
151+
* classpath to be passed to the compiler (optional)
152+
* @param compilerClassPath
153+
* classpath of the compiler itself
154+
* @param logger
155+
* logger
156+
* @return
157+
* compiler process exit code
158+
*/
159+
def runSimpleScalacLike(
160+
scalaVersion: String,
161+
javaHomeOpt: Option[os.Path],
162+
javacOptions: Seq[String],
163+
scalacOptions: Seq[String],
164+
fullClassPath: Seq[os.Path],
165+
compilerClassPath: Seq[os.Path],
166+
logger: Logger
167+
): Int =
168+
compilerMainClass(scalaVersion) match {
169+
case Some(mainClass) =>
170+
runScalacLike(
171+
mainClass = mainClass,
172+
javaHomeOpt = javaHomeOpt,
173+
javacOptions = javacOptions,
174+
scalacOptions = scalacOptions,
175+
classPath = fullClassPath,
176+
compilerClassPath = compilerClassPath,
177+
sources = Nil,
178+
outputDir = None,
179+
cwd = os.pwd,
180+
logger = logger
181+
)
182+
case _ => 1
183+
}
184+
185+
private def compilerMainClass(scalaVersion: String): Option[String] =
186+
if (scalaVersion.startsWith("2."))
187+
Some {
188+
if (scaladoc) "scala.tools.nsc.ScalaDoc"
189+
else "scala.tools.nsc.Main"
190+
}
191+
else if (scaladoc) None
192+
else Some("dotty.tools.dotc.Main")
193+
68194
def compile(
69195
project: Project,
70196
logger: Logger
@@ -74,22 +200,13 @@ final case class SimpleScalaCompiler(
74200
project.scalaCompiler match {
75201
case Some(compiler) =>
76202
val isScala2 = compiler.scalaVersion.startsWith("2.")
77-
val mainClassOpt =
78-
if (isScala2)
79-
Some {
80-
if (scaladoc) "scala.tools.nsc.ScalaDoc"
81-
else "scala.tools.nsc.Main"
82-
}
83-
else if (scaladoc) None
84-
else Some("dotty.tools.dotc.Main")
85-
86-
mainClassOpt.forall { mainClass =>
203+
compilerMainClass(compiler.scalaVersion).forall { mainClass =>
87204

88205
val outputDir =
89206
if (isScala2 && scaladoc) project.scaladocDir
90207
else project.classesDir
91208

92-
runScalacLike(project, mainClass, outputDir, logger)
209+
runScalacLikeForProject(project, mainClass, outputDir, logger)
93210
}
94211

95212
case None =>

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import caseapp._
55
// format: off
66
final case class CompileCrossOptions(
77
@HelpMessage("Cross-compile sources")
8-
@ExtraName("X")
98
@Hidden
109
cross: Option[Boolean] = None
1110
)

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import caseapp._
44

55
// format: off
66
final case class CrossOptions(
7-
@ExtraName("X")
87
cross: Option[Boolean] = None
98
)
109
// format: on

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

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package scala.cli.commands
22

33
import caseapp._
4-
import caseapp.core.Arg
4+
import caseapp.core.{Arg, Error}
55
import caseapp.core.parser.{Argument, NilParser, StandardArgument}
66
import caseapp.core.util.Formatter
77
import com.github.plokhotnyuk.jsoniter_scala.core._
@@ -33,29 +33,34 @@ object ScalacOptions {
3333
Set("-V", "-W", "-X", "-Y")
3434
private val scalacOptionsPrefixes =
3535
Set("-g", "-language", "-opt", "-P", "-target") ++ scalacOptionsPurePrefixes
36+
37+
/** This includes all the scalac options which disregard inputs and print a help and/or context
38+
* message instead.
39+
*/
40+
val ScalacPrintOptions: Set[String] =
41+
scalacOptionsPurePrefixes ++ Set("-help", "-Xshow-phases", "-Vphases")
42+
3643
private val scalacOptionsArgument: Argument[List[String]] =
3744
new Argument[List[String]] {
3845

39-
val underlying = StandardArgument[List[String]](scalacOptionsArg)
46+
val underlying: StandardArgument[List[String]] = StandardArgument(scalacOptionsArg)
4047

41-
val arg = scalacOptionsArg
48+
val arg: Arg = scalacOptionsArg
4249

43-
def withDefaultOrigin(origin: String) = this
44-
def init = Some(Nil)
50+
def withDefaultOrigin(origin: String): Argument[List[String]] = this
51+
def init: Option[List[String]] = Some(Nil)
4552
def step(
4653
args: List[String],
4754
index: Int,
4855
acc: Option[List[String]],
4956
formatter: Formatter[Name]
50-
) =
57+
): Either[(Error, List[String]), Option[(Option[List[String]], List[String])]] =
5158
args match {
52-
case h :: t
53-
if scalacOptionsPrefixes.exists(h.startsWith) &&
54-
!scalacOptionsPurePrefixes.contains(h) =>
59+
case h :: t if scalacOptionsPrefixes.exists(h.startsWith) =>
5560
Right(Some((Some(h :: acc.getOrElse(Nil)), t)))
5661
case _ => underlying.step(args, index, acc, formatter)
5762
}
58-
def get(acc: Option[List[String]], formatter: Formatter[Name]) =
63+
def get(acc: Option[List[String]], formatter: Formatter[Name]): Either[Error, List[String]] =
5964
Right(acc.getOrElse(Nil))
6065
}
6166

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ final case class SharedOptions(
4242
@Name("scalaBin")
4343
@Name("B")
4444
scalaBinaryVersion: Option[String] = None,
45+
46+
@Group("Scala")
47+
@HelpMessage("Show help for scalac. This is an alias for --scalac-option -help")
48+
scalacHelp: Boolean = false,
4549

4650
@Group("Java")
4751
@HelpMessage("Add extra JARs in the class path")

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import scala.cli.CurrentParams
1010
import scala.cli.commands.util.SharedOptionsUtil._
1111

1212
object Compile extends ScalaCommand[CompileOptions] {
13-
override def group = "Main"
14-
override def sharedOptions(options: CompileOptions) = Some(options.shared)
13+
override def group = "Main"
14+
override def sharedOptions(options: CompileOptions): Option[SharedOptions] = Some(options.shared)
1515

16-
def outputPath(options: CompileOptions) =
16+
def outputPath(options: CompileOptions): Option[os.Path] =
1717
options.output.filter(_.nonEmpty).map(p => os.Path(p, Os.pwd))
1818

1919
def run(options: CompileOptions, args: RemainingArgs): Unit = {
2020
maybePrintGroupHelp(options)
21+
maybePrintSimpleScalacOutput(options, options.shared.buildOptions())
2122
CurrentParams.verbosity = options.shared.logging.verbosity
2223
val inputs = options.shared.inputsOrExit(args)
2324
CurrentParams.workspaceOpt = Some(inputs.workspace)

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ import scala.cli.packaging.{Library, NativeImage}
3030
import scala.util.Properties
3131

3232
object Package extends ScalaCommand[PackageOptions] {
33-
override def name = "package"
34-
override def group = "Main"
35-
override def sharedOptions(options: PackageOptions) = Some(options.shared)
33+
override def name = "package"
34+
override def group = "Main"
35+
override def sharedOptions(options: PackageOptions): Option[SharedOptions] = Some(options.shared)
3636
def run(options: PackageOptions, args: RemainingArgs): Unit = {
3737
maybePrintGroupHelp(options)
38+
maybePrintSimpleScalacOutput(options, options.buildOptions)
3839
CurrentParams.verbosity = options.shared.logging.verbosity
3940
val inputs = options.shared.inputsOrExit(args.remaining)
4041
CurrentParams.workspaceOpt = Some(inputs.workspace)
@@ -138,7 +139,7 @@ object Package extends ScalaCommand[PackageOptions] {
138139
lazy val validPackageScalaNative =
139140
Seq(PackageType.LibraryJar, PackageType.SourceJar, PackageType.DocJar)
140141

141-
(forcedPackageTypeOpt -> build.options.platform.value) match {
142+
forcedPackageTypeOpt -> build.options.platform.value match {
142143
case (Some(forcedPackageType), _) => Right(forcedPackageType)
143144
case (_, _) if build.options.notForBloopOptions.packageOptions.isDockerEnabled =>
144145
for (basePackageType <- basePackageTypeOpt)

0 commit comments

Comments
 (0)