Skip to content

Commit 648755d

Browse files
authored
Add export --json option (#1840)
* Add export --json option * NIT Rename ProjectDescriptor subclasses * Split Json into scopes * Add structured output for dependencies, put scalaCompilerPlugins and scalacOptions into scope json * NIT group defs by return type
1 parent cf036de commit 648755d

File tree

10 files changed

+593
-29
lines changed

10 files changed

+593
-29
lines changed

modules/cli/src/main/scala/scala/cli/commands/export0/Export.scala

Lines changed: 44 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
package scala.cli.commands.export0
22

33
import caseapp.*
4+
import com.github.plokhotnyuk.jsoniter_scala.core.*
5+
import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker
6+
import com.google.gson.{Gson, GsonBuilder}
47
import coursier.cache.FileCache
58
import coursier.util.{Artifact, Task}
69

10+
import java.io.{OutputStreamWriter, PrintStream}
11+
import java.nio.charset.{Charset, StandardCharsets}
12+
713
import scala.build.EitherCps.{either, value}
814
import scala.build.*
915
import scala.build.errors.BuildException
1016
import scala.build.input.Inputs
1117
import scala.build.internal.{Constants, CustomCodeWrapper}
12-
import scala.build.options.{BuildOptions, Scope}
18+
import scala.build.options.{BuildOptions, Platform, Scope}
1319
import scala.cli.CurrentParams
1420
import scala.cli.commands.ScalaCommand
1521
import scala.cli.commands.shared.SharedOptions
1622
import scala.cli.exportCmd.*
23+
import scala.util.Using
1724

1825
object Export extends ScalaCommand[ExportOptions] {
1926
override def scalaSpecificationLevel = SpecificationLevel.RESTRICTED
@@ -53,9 +60,17 @@ object Export extends ScalaCommand[ExportOptions] {
5360
}
5461

5562
// FIXME Auto-update those
56-
def sbtBuildTool(extraSettings: Seq[String], sbtVersion: String, logger: Logger): Sbt =
57-
Sbt(sbtVersion, extraSettings, logger)
58-
def millBuildTool(cache: FileCache[Task], projectName: Option[String], logger: Logger): Mill = {
63+
def sbtProjectDescriptor(
64+
extraSettings: Seq[String],
65+
sbtVersion: String,
66+
logger: Logger
67+
): SbtProjectDescriptor =
68+
SbtProjectDescriptor(sbtVersion, extraSettings, logger)
69+
def millProjectDescriptor(
70+
cache: FileCache[Task],
71+
projectName: Option[String],
72+
logger: Logger
73+
): MillProjectDescriptor = {
5974
val launcherArtifacts = Seq(
6075
os.rel / "mill" -> s"https://github.com/lefou/millw/raw/${Constants.lefouMillwRef}/millw",
6176
os.rel / "mill.bat" -> s"https://github.com/lefou/millw/raw/${Constants.lefouMillwRef}/millw.bat"
@@ -73,9 +88,12 @@ object Export extends ScalaCommand[ExportOptions] {
7388
}
7489
val launchersTask = cache.logger.using(Task.gather.gather(launcherTasks))
7590
val launchers = launchersTask.unsafeRun()(cache.ec)
76-
Mill(Constants.millVersion, projectName, launchers, logger)
91+
MillProjectDescriptor(Constants.millVersion, projectName, launchers, logger)
7792
}
7893

94+
def jsonProjectDescriptor(projectName: Option[String], logger: Logger): JsonProjectDescriptor =
95+
JsonProjectDescriptor(projectName, logger)
96+
7997
override def sharedOptions(opts: ExportOptions): Option[SharedOptions] = Some(opts.shared)
8098

8199
override def runCommand(options: ExportOptions, args: RemainingArgs, logger: Logger): Unit = {
@@ -84,24 +102,29 @@ object Export extends ScalaCommand[ExportOptions] {
84102
val output = options.output.getOrElse("dest")
85103
val dest = os.Path(output, os.pwd)
86104
if (os.exists(dest)) {
87-
System.err.println(
105+
logger.error(
88106
s"""Error: $dest already exists.
89107
|To change the destination output directory pass --output path or remove the destination directory first.""".stripMargin
90108
)
91109
sys.exit(1)
92110
}
93111

112+
val shouldExportToJson = options.json.getOrElse(false)
94113
val shouldExportToMill = options.mill.getOrElse(false)
95114
val shouldExportToSbt = options.sbt.getOrElse(false)
96115
if (shouldExportToMill && shouldExportToSbt) {
97-
System.err.println(
116+
logger.error(
98117
s"Error: Cannot export to both mill and sbt. Please pick one build tool to export."
99118
)
100119
sys.exit(1)
101120
}
102121

103-
val buildToolName = if (shouldExportToMill) "mill" else "sbt"
104-
System.out.println(s"Exporting to a $buildToolName project...")
122+
if (!shouldExportToJson) {
123+
val buildToolName = if (shouldExportToMill) "mill" else "sbt"
124+
logger.message(s"Exporting to a $buildToolName project...")
125+
}
126+
else
127+
logger.message(s"Exporting to JSON...")
105128

106129
val inputs = options.shared.inputs(args.all).orExit(logger)
107130
CurrentParams.workspaceOpt = Some(inputs.workspace)
@@ -131,7 +154,7 @@ object Export extends ScalaCommand[ExportOptions] {
131154
svMain <- optionsMain0.scalaOptions.scalaVersion
132155
svTest <- optionsTest0.scalaOptions.scalaVersion
133156
} if (svMain != svTest) {
134-
System.err.println(
157+
logger.error(
135158
s"""Detected different Scala versions in main and test scopes.
136159
|Please set the Scala version explicitly in the main and test scope with using directives or pass -S, --scala-version as parameter""".stripMargin
137160
)
@@ -141,27 +164,30 @@ object Export extends ScalaCommand[ExportOptions] {
141164
if (
142165
optionsMain0.scalaOptions.scalaVersion.isEmpty && optionsTest0.scalaOptions.scalaVersion.nonEmpty
143166
) {
144-
System.err.println(
167+
logger.error(
145168
s"""Detected that the Scala version is only set in test scope.
146169
|Please set the Scala version explicitly in the main and test scopes with using directives or pass -S, --scala-version as parameter""".stripMargin
147170
)
148171
sys.exit(1)
149172
}
150173

151174
val sbtVersion = options.sbtVersion.getOrElse("1.6.1")
152-
def sbtBuildTool0 =
153-
sbtBuildTool(options.sbtSetting.map(_.trim).filter(_.nonEmpty), sbtVersion, logger)
154175

155-
val buildTool =
176+
def sbtProjectDescriptor0 =
177+
sbtProjectDescriptor(options.sbtSetting.map(_.trim).filter(_.nonEmpty), sbtVersion, logger)
178+
179+
val projectDescriptor =
156180
if (shouldExportToMill)
157-
millBuildTool(options.shared.coursierCache, options.project, logger)
181+
millProjectDescriptor(options.shared.coursierCache, options.project, logger)
182+
else if (shouldExportToJson)
183+
jsonProjectDescriptor(options.project, logger)
158184
else // shouldExportToSbt isn't checked, as it's treated as default
159-
sbtBuildTool0
185+
sbtProjectDescriptor0
160186

161-
val project = buildTool.`export`(optionsMain0, optionsTest0, sourcesMain, sourcesTest)
187+
val project = projectDescriptor.`export`(optionsMain0, optionsTest0, sourcesMain, sourcesTest)
162188

163189
os.makeDir.all(dest)
164190
project.writeTo(dest)
165-
System.out.println(s"Exported to: $dest")
191+
logger.message(s"Exported to: $dest")
166192
}
167193
}

modules/cli/src/main/scala/scala/cli/commands/export0/ExportOptions.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ final case class ExportOptions(
3232
@Tag(tags.restricted)
3333
@HelpMessage("Sets the export format to Mill")
3434
mill: Option[Boolean] = None,
35+
@Tag(tags.restricted)
36+
@HelpMessage("Sets the export format to Json")
37+
json: Option[Boolean] = None,
3538

3639
@Name("setting")
3740
@Group("Build Tool export options")
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package scala.cli.exportCmd
2+
3+
import com.github.plokhotnyuk.jsoniter_scala.core.{JsonValueCodec, WriterConfig, writeToStream}
4+
import com.github.plokhotnyuk.jsoniter_scala.macros.{CodecMakerConfig, JsonCodecMaker}
5+
import coursier.Dependency
6+
import coursier.util.Artifact
7+
import dependency.AnyDependency
8+
9+
import java.nio.charset.StandardCharsets
10+
11+
import scala.build.options.ConfigMonoid
12+
import scala.cli.util.SeqHelpers.*
13+
import scala.reflect.NameTransformer
14+
import scala.util.{Properties, Using}
15+
16+
final case class JsonProject(
17+
projectName: Option[String] = None,
18+
scalaVersion: Option[String] = None,
19+
platform: Option[String] = None,
20+
jvmVersion: Option[String] = None,
21+
scalaJsVersion: Option[String] = None,
22+
jsEsVersion: Option[String] = None,
23+
scalaNativeVersion: Option[String] = None,
24+
mainClass: Option[String] = None,
25+
scopes: Map[String, ScopedJsonProject] = Map.empty
26+
) extends Project {
27+
28+
def +(other: JsonProject): JsonProject =
29+
JsonProject.monoid.orElse(this, other)
30+
31+
def withScope(scopeName: String, scopedJsonProj: ScopedJsonProject): JsonProject =
32+
if (scopedJsonProj.sources.isEmpty)
33+
this
34+
else
35+
this.copy(
36+
scopes = this.scopes + (scopeName -> scopedJsonProj)
37+
)
38+
39+
def writeTo(dir: os.Path): Unit = {
40+
val config = WriterConfig.withIndentionStep(1)
41+
42+
Using(os.write.outputStream(dir / "export.json")) {
43+
outputStream =>
44+
writeToStream(
45+
this,
46+
outputStream,
47+
config
48+
)
49+
}
50+
}
51+
}
52+
53+
final case class ScopedJsonProject(
54+
sources: Seq[String] = Nil,
55+
scalacOptions: Seq[String] = Nil,
56+
scalaCompilerPlugins: Seq[ExportDependencyFormat] = Nil,
57+
dependencies: Seq[ExportDependencyFormat] = Nil,
58+
resolvers: Seq[String] = Nil,
59+
resourcesDirs: Seq[String] = Nil,
60+
customJarsDecls: Seq[String] = Nil
61+
) {
62+
63+
def +(other: ScopedJsonProject): ScopedJsonProject =
64+
ScopedJsonProject.monoid.orElse(this, other)
65+
66+
def sorted(using ord: Ordering[String]): ScopedJsonProject =
67+
ScopedJsonProject(
68+
this.sources.sorted,
69+
this.scalacOptions,
70+
this.scalaCompilerPlugins.sorted,
71+
this.dependencies.sorted,
72+
this.resolvers.sorted,
73+
this.resourcesDirs.sorted,
74+
this.customJarsDecls.sorted
75+
)
76+
}
77+
78+
object ScopedJsonProject {
79+
implicit val monoid: ConfigMonoid[ScopedJsonProject] = ConfigMonoid.derive
80+
implicit lazy val jsonCodec: JsonValueCodec[ScopedJsonProject] = JsonCodecMaker.make
81+
}
82+
83+
object JsonProject {
84+
implicit val monoid: ConfigMonoid[JsonProject] = ConfigMonoid.derive
85+
implicit lazy val jsonCodec: JsonValueCodec[JsonProject] = JsonCodecMaker.make
86+
}
87+
88+
case class ExportDependencyFormat(groupId: String, artifactId: ArtifactId, version: String)
89+
90+
case class ArtifactId(name: String, fullName: String)
91+
92+
object ExportDependencyFormat {
93+
def apply(dep: Dependency): ExportDependencyFormat = {
94+
val scalaVersionStartIndex = dep.module.name.value.lastIndexOf('_')
95+
val shortDepName = if (scalaVersionStartIndex == -1)
96+
dep.module.name.value
97+
else
98+
dep.module.name.value.take(scalaVersionStartIndex)
99+
new ExportDependencyFormat(
100+
dep.module.organization.value,
101+
ArtifactId(shortDepName, dep.module.name.value),
102+
dep.version
103+
)
104+
}
105+
106+
def apply(
107+
dep: AnyDependency,
108+
scalaParamsOpt: Option[dependency.ScalaParameters]
109+
): ExportDependencyFormat = {
110+
import scala.build.internal.Util.*
111+
dep.toCs(scalaParamsOpt)
112+
.map(ExportDependencyFormat.apply)
113+
.getOrElse(
114+
ExportDependencyFormat(
115+
dep.module.organization,
116+
ArtifactId(dep.module.name, dep.module.name),
117+
dep.version
118+
)
119+
)
120+
}
121+
122+
implicit val ordering: Ordering[ExportDependencyFormat] =
123+
Ordering.by(x => x.groupId + x.artifactId.fullName)
124+
implicit lazy val jsonCodec: JsonValueCodec[ExportDependencyFormat] = JsonCodecMaker.make
125+
}

0 commit comments

Comments
 (0)