Skip to content

Commit 08e21fe

Browse files
Add simple export command
1 parent 2c10ee0 commit 08e21fe

File tree

15 files changed

+626
-0
lines changed

15 files changed

+626
-0
lines changed

modules/build/src/main/scala/scala/build/preprocessing/ScalaPreprocessor.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ case object ScalaPreprocessor extends Preprocessor {
112112
scalaVersion = Some(scalaVer)
113113
)
114114
)
115+
case Seq("repository", repo) if repo.nonEmpty =>
116+
BuildOptions(
117+
classPathOptions = ClassPathOptions(
118+
extraRepositories = Seq(repo)
119+
)
120+
)
115121
case other =>
116122
val maybeOptions =
117123
// TODO Accept several platforms for cross-compilation

modules/cli/src/main/scala/scala/cli/ScalaCli.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ object ScalaCli extends CommandsEntryPoint {
2020
Clean,
2121
Compile,
2222
Directories,
23+
Export,
2324
InstallCompletions,
2425
Metabrowse,
2526
Repl,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package scala.cli.commands
2+
3+
import caseapp._
4+
5+
import scala.build.{BloopBuildClient, Build, CrossSources, Inputs, Logger, Sources}
6+
import scala.build.internal.CustomCodeWrapper
7+
import scala.build.options.BuildOptions
8+
import scala.build.GeneratedSource
9+
10+
import scala.cli.export._
11+
12+
object Export extends ScalaCommand[ExportOptions] {
13+
14+
private def prepareBuild(
15+
inputs: Inputs,
16+
buildOptions: BuildOptions,
17+
logger: Logger,
18+
verbosity: Int
19+
): (Sources, BuildOptions) = {
20+
21+
logger.log("Preparing build")
22+
23+
val crossSources = CrossSources.forInputs(
24+
inputs,
25+
Sources.defaultPreprocessors(
26+
buildOptions.scriptOptions.codeWrapper.getOrElse(CustomCodeWrapper)
27+
)
28+
)
29+
val sources = crossSources.sources(buildOptions)
30+
31+
if (verbosity >= 3)
32+
pprint.better.log(sources)
33+
34+
val options0 = buildOptions.orElse(sources.buildOptions)
35+
36+
(sources, options0)
37+
}
38+
39+
def sbtBuildTool = Sbt("1.5.5")
40+
def defaultBuildTool = sbtBuildTool
41+
42+
def run(options: ExportOptions, args: RemainingArgs): Unit = {
43+
44+
val logger = options.shared.logger
45+
val inputs = options.shared.inputsOrExit(args)
46+
val baseOptions = options.buildOptions
47+
48+
val (sources, options0) =
49+
prepareBuild(inputs, baseOptions, logger, options.shared.logging.verbosity)
50+
51+
val buildTool =
52+
if (options.sbt.getOrElse(true))
53+
sbtBuildTool
54+
else
55+
defaultBuildTool
56+
57+
val project = buildTool.export(options0, sources)
58+
59+
val output = options.output.getOrElse("dest")
60+
val dest = os.Path(output, os.pwd)
61+
if (os.exists(dest)) {
62+
System.err.println(s"Error: $output already exists.")
63+
sys.exit(1)
64+
}
65+
66+
os.makeDir.all(dest)
67+
project.writeTo(dest)
68+
}
69+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package scala.cli.commands
2+
3+
import caseapp._
4+
5+
import scala.build.options.BuildOptions
6+
7+
// format: off
8+
final case class ExportOptions(
9+
// FIXME There might be too many options for 'scala-cli export' there
10+
@Recurse
11+
shared: SharedOptions = SharedOptions(),
12+
@Recurse
13+
mainClass: MainClassOptions = MainClassOptions(),
14+
15+
sbt: Option[Boolean] = None,
16+
17+
@Name("o")
18+
output: Option[String] = None
19+
) {
20+
// format: on
21+
22+
def buildOptions: BuildOptions = {
23+
val baseOptions = shared.buildOptions(enableJmh = false, None, ignoreErrors = false)
24+
baseOptions.copy(
25+
mainClass = mainClass.mainClass.filter(_.nonEmpty)
26+
)
27+
}
28+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package scala.cli.export
2+
3+
import scala.build.options.BuildOptions
4+
import scala.build.Sources
5+
6+
sealed abstract class BuildTool extends Product with Serializable {
7+
def export(options: BuildOptions, sources: Sources): Project
8+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package scala.cli.export
2+
3+
abstract class Project extends Product with Serializable {
4+
def writeTo(dir: os.Path): Unit
5+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package scala.cli.export
2+
3+
import scala.build.internal.Constants
4+
import scala.build.options.BuildOptions
5+
import scala.build.Sources
6+
7+
import java.nio.charset.StandardCharsets
8+
9+
import coursier.ivy.IvyRepository
10+
import coursier.maven.MavenRepository
11+
import coursier.parse.RepositoryParser
12+
import dependency.{NoAttributes, ScalaNameAttributes}
13+
14+
final case class Sbt(sbtVersion: String) {
15+
def export(options: BuildOptions, sources: Sources): SbtProject = {
16+
val q = "\""
17+
val nl = System.lineSeparator()
18+
val charset = StandardCharsets.UTF_8
19+
20+
val mainSources = sources.paths.map {
21+
case (path, relPath) =>
22+
val language =
23+
if (path.last.endsWith(".java")) "java"
24+
else "scala" // FIXME Others
25+
// FIXME asSubPath might throw… Make it a SubPath earlier in the API?
26+
(relPath.asSubPath, language, os.read.bytes(path))
27+
}
28+
29+
val extraMainSources = sources.inMemory.map {
30+
case (_, relPath, content, _) =>
31+
val language =
32+
if (relPath.last.endsWith(".java")) "java"
33+
else "scala"
34+
(relPath.asSubPath, language, content.getBytes(charset))
35+
}
36+
37+
// TODO Handle Scala CLI cross-builds
38+
39+
// TODO Detect pure Java projects?
40+
41+
val (plugins, pluginSettings) =
42+
if (options.scalaJsOptions.enable)
43+
Seq(
44+
""""org.scala-js" % "sbt-scalajs" % "1.7.0""""
45+
) -> Seq(
46+
"enablePlugins(ScalaJSPlugin)",
47+
"scalaJSUseMainModuleInitializer := true"
48+
)
49+
else if (options.scalaNativeOptions.enable)
50+
Seq(
51+
""""org.scala-native" % "sbt-scala-native" % "0.4.0""""
52+
) -> Seq(
53+
"enablePlugins(ScalaNativePlugin)"
54+
)
55+
else
56+
Nil -> Nil
57+
58+
val scalaVerSetting = {
59+
val sv = options.scalaOptions.scalaVersion.getOrElse(Constants.defaultScalaVersion)
60+
s"""scalaVersion := "$sv""""
61+
}
62+
63+
val repoSettings =
64+
if (options.classPathOptions.extraRepositories.isEmpty) Nil
65+
else {
66+
val repos = options.classPathOptions
67+
.extraRepositories
68+
.map(repo => (repo, RepositoryParser.repository(repo)))
69+
.zipWithIndex
70+
.map {
71+
case ((repoStr, Right(repo: IvyRepository)), idx) =>
72+
// TODO repo.authentication?
73+
// TODO repo.metadataPatternOpt
74+
s"""Resolver.url("repo-$idx") artifacts "${repo.pattern.string}""""
75+
case ((repoStr, Right(repo: MavenRepository)), idx) =>
76+
// TODO repo.authentication?
77+
s""""repo-$idx" at "${repo.root}""""
78+
case _ =>
79+
???
80+
}
81+
Seq(s"""resolvers ++= Seq(${repos.mkString(", ")})""")
82+
}
83+
84+
val customJarsSettings =
85+
if (options.classPathOptions.extraCompileOnlyJars.isEmpty) Nil
86+
else {
87+
val jars = options.classPathOptions.extraCompileOnlyJars.map(p => s"""file("$p")""")
88+
Seq(s"""Compile / unmanagedClasspath ++= Seq(${jars.mkString(", ")})""")
89+
}
90+
91+
// TODO options.classPathOptions.extraJars
92+
93+
// TODO options.javaOptions.javaOpts
94+
// TODO options.scalaJsOptions.*
95+
// TODO options.scalaNativeOptions.*
96+
97+
// TODO options.scalaOptions.addScalaLibrary
98+
99+
val mainClassOptions = options.mainClass match {
100+
case None => Nil
101+
case Some(mainClass) =>
102+
Seq(s"""Compile / mainClass := Some("$mainClass")""")
103+
}
104+
105+
val scalacOptionsSettings =
106+
if (options.scalaOptions.scalacOptions.isEmpty) Nil
107+
else {
108+
val options0 = options
109+
.scalaOptions
110+
.scalacOptions
111+
.map(o => "\"" + o.replace("\"", "\\\"") + "\"")
112+
Seq(s"""scalacOptions ++= Seq(${options0.mkString(", ")})""")
113+
}
114+
115+
// TODO options.testOptions.frameworkOpt
116+
117+
val depSettings = {
118+
val depStrings = options.classPathOptions
119+
.extraDependencies
120+
.map { dep =>
121+
val org = dep.organization
122+
val name = dep.name
123+
val ver = dep.version
124+
// TODO dep.userParams
125+
// TODO dep.exclude
126+
// TODO dep.attributes
127+
val (sep, suffixOpt) = dep.nameAttributes match {
128+
case NoAttributes => ("%", None)
129+
case s: ScalaNameAttributes =>
130+
val suffixOpt0 =
131+
if (s.fullCrossVersion.getOrElse(false)) Some(".cross(CrossVersion.full)")
132+
else None
133+
val sep =
134+
if (s.platform.getOrElse(false)) "%%%"
135+
else "%%"
136+
(sep, suffixOpt0)
137+
}
138+
139+
val baseDep = s"""$q$org$q $sep $q$name$q % $q$ver$q"""
140+
suffixOpt.fold(baseDep)(suffix => s"($baseDep)$suffix")
141+
}
142+
143+
if (depStrings.isEmpty) Nil
144+
else if (depStrings.lengthCompare(1) == 0)
145+
Seq(s"""libraryDependencies += ${depStrings.head}""")
146+
else
147+
Seq(s"""libraryDependencies ++= Seq($nl${depStrings.map(" " + _ + nl).mkString})""")
148+
}
149+
150+
val settings =
151+
Seq(
152+
pluginSettings,
153+
Seq(scalaVerSetting),
154+
mainClassOptions,
155+
scalacOptionsSettings,
156+
repoSettings,
157+
depSettings,
158+
customJarsSettings
159+
)
160+
161+
SbtProject(
162+
plugins,
163+
settings,
164+
"1.5.5",
165+
mainSources ++ extraMainSources,
166+
Nil
167+
)
168+
}
169+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package scala.cli.export
2+
3+
import java.nio.charset.StandardCharsets
4+
5+
final case class SbtProject(
6+
plugins: Seq[String],
7+
settings: Seq[Seq[String]],
8+
sbtVersion: String,
9+
mainSources: Seq[(os.SubPath, String, Array[Byte])],
10+
testSources: Seq[(os.SubPath, String, Array[Byte])]
11+
) extends Project {
12+
def writeTo(dir: os.Path): Unit = {
13+
val nl = System.lineSeparator()
14+
val charset = StandardCharsets.UTF_8
15+
16+
val buildPropsContent = s"sbt.version=$sbtVersion" + nl
17+
os.write(
18+
dir / "project" / "build.properties",
19+
buildPropsContent.getBytes(charset),
20+
createFolders = true
21+
)
22+
23+
if (plugins.nonEmpty) {
24+
val pluginsSbtContent = plugins
25+
.map { p =>
26+
s"addSbtPlugin($p)" + nl
27+
}
28+
.mkString
29+
os.write(dir / "project" / "plugins.sbt", pluginsSbtContent.getBytes(charset))
30+
}
31+
32+
val buildSbtContent = settings
33+
.map { settings0 =>
34+
settings0.map(s => s + nl).mkString + nl
35+
}
36+
.mkString
37+
os.write(dir / "build.sbt", buildSbtContent.getBytes(charset))
38+
39+
for ((path, language, content) <- mainSources) {
40+
val path0 = dir / "src" / "main" / language / path
41+
os.write(path0, content, createFolders = true)
42+
}
43+
for ((path, language, content) <- testSources) {
44+
val path0 = dir / "src" / "test" / language / path
45+
os.write(path0, content)
46+
}
47+
}
48+
}

0 commit comments

Comments
 (0)