Skip to content

Commit 5ed4f79

Browse files
authored
Add support for local .m2 in publish local (#4179)
1 parent 2352850 commit 5ed4f79

7 files changed

Lines changed: 189 additions & 4 deletions

File tree

modules/cli/src/main/scala/scala/cli/commands/publish/Publish.scala

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
256256
workingDir,
257257
ivy2HomeOpt,
258258
publishLocal = false,
259+
m2Local = false,
260+
m2HomeOpt = None,
259261
forceSigningExternally = options.signingCli.forceSigningExternally.getOrElse(false),
260262
parallelUpload = options.parallelUpload,
261263
options.watch.watch,
@@ -279,6 +281,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
279281
workingDir: => os.Path,
280282
ivy2HomeOpt: Option[os.Path],
281283
publishLocal: Boolean,
284+
m2Local: Boolean = false,
285+
m2HomeOpt: Option[os.Path] = None,
282286
forceSigningExternally: Boolean,
283287
parallelUpload: Option[Boolean],
284288
watch: Boolean,
@@ -309,6 +313,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
309313
workingDir = workingDir,
310314
ivy2HomeOpt = ivy2HomeOpt,
311315
publishLocal = publishLocal,
316+
m2Local = m2Local,
317+
m2HomeOpt = m2HomeOpt,
312318
logger = logger,
313319
allowExit = false,
314320
forceSigningExternally = forceSigningExternally,
@@ -342,6 +348,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
342348
workingDir = workingDir,
343349
ivy2HomeOpt = ivy2HomeOpt,
344350
publishLocal = publishLocal,
351+
m2Local = m2Local,
352+
m2HomeOpt = m2HomeOpt,
345353
logger = logger,
346354
allowExit = true,
347355
forceSigningExternally = forceSigningExternally,
@@ -363,6 +371,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
363371
workingDir: os.Path,
364372
ivy2HomeOpt: Option[os.Path],
365373
publishLocal: Boolean,
374+
m2Local: Boolean,
375+
m2HomeOpt: Option[os.Path],
366376
logger: Logger,
367377
allowExit: Boolean,
368378
forceSigningExternally: Boolean,
@@ -419,6 +429,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
419429
workingDir = workingDir,
420430
ivy2HomeOpt = ivy2HomeOpt,
421431
publishLocal = publishLocal,
432+
m2Local = m2Local,
433+
m2HomeOpt = m2HomeOpt,
422434
logger = logger,
423435
forceSigningExternally = forceSigningExternally,
424436
parallelUpload = parallelUpload,
@@ -687,6 +699,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
687699
workingDir: os.Path,
688700
ivy2HomeOpt: Option[os.Path],
689701
publishLocal: Boolean,
702+
m2Local: Boolean,
703+
m2HomeOpt: Option[os.Path],
690704
logger: Logger,
691705
forceSigningExternally: Boolean,
692706
parallelUpload: Option[Boolean],
@@ -741,7 +755,8 @@ object Publish extends ScalaCommand[PublishOptions] with BuildCommandHelpers {
741755
lazy val es =
742756
Executors.newSingleThreadScheduledExecutor(Util.daemonThreadFactory("publish-retry"))
743757

744-
if publishLocal then RepoParams.ivy2Local(ivy2HomeOpt)
758+
if publishLocal && m2Local then RepoParams.m2Local(m2HomeOpt)
759+
else if publishLocal then RepoParams.ivy2Local(ivy2HomeOpt)
745760
else
746761
value {
747762
publishOptions.contextual(isCi).repository match {

modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocal.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ object PublishLocal extends ScalaCommand[PublishLocalOptions] {
3535
Publish.maybePrintLicensesAndExit(options.publishParams)
3636
Publish.maybePrintChecksumsAndExit(options.sharedPublish)
3737

38+
if options.m2 && options.sharedPublish.ivy2Home.exists(_.trim.nonEmpty) then {
39+
logger.error("--m2 and --ivy2-home are mutually exclusive.")
40+
sys.exit(1)
41+
}
42+
3843
val baseOptions = buildOptionsOrExit(options)
3944
val inputs = options.shared.inputs(args.all).orExit(logger)
4045
CurrentParams.workspaceOpt = Some(inputs.workspace)
@@ -71,6 +76,10 @@ object PublishLocal extends ScalaCommand[PublishLocalOptions] {
7176
.filter(_.trim.nonEmpty)
7277
.map(os.Path(_, os.pwd))
7378

79+
val m2HomeOpt = options.m2Home
80+
.filter(_.trim.nonEmpty)
81+
.map(os.Path(_, os.pwd))
82+
7483
Publish.doRun(
7584
inputs = inputs,
7685
logger = logger,
@@ -81,6 +90,8 @@ object PublishLocal extends ScalaCommand[PublishLocalOptions] {
8190
workingDir = workingDir,
8291
ivy2HomeOpt = ivy2HomeOpt,
8392
publishLocal = true,
93+
m2Local = options.m2,
94+
m2HomeOpt = m2HomeOpt,
8495
forceSigningExternally = options.scalaSigning.forceSigningExternally.getOrElse(false),
8596
parallelUpload = Some(true),
8697
watch = options.watch.watch,

modules/cli/src/main/scala/scala/cli/commands/publish/PublishLocalOptions.scala

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

55
import scala.cli.commands.pgp.PgpScalaSigningOptions
66
import scala.cli.commands.shared.*
7+
import scala.cli.commands.tags
78

89
// format: off
910
@HelpMessage(PublishLocalOptions.helpMessage, "", PublishLocalOptions.detailedHelpMessage)
@@ -22,14 +23,27 @@ final case class PublishLocalOptions(
2223
sharedPublish: SharedPublishOptions = SharedPublishOptions(),
2324
@Recurse
2425
scalaSigning: PgpScalaSigningOptions = PgpScalaSigningOptions(),
26+
27+
@Group(HelpGroup.Publishing.toString)
28+
@HelpMessage("Publish to the local Maven repository (defaults to ~/.m2/repository) instead of Ivy2 local")
29+
@Name("mavenLocal")
30+
@Tag(tags.experimental)
31+
@Tag(tags.inShortHelp)
32+
m2: Boolean = false,
33+
34+
@Group(HelpGroup.Publishing.toString)
35+
@HelpMessage("Set the local Maven repository path (defaults to ~/.m2/repository)")
36+
@ValueDescription("path")
37+
@Tag(tags.experimental)
38+
m2Home: Option[String] = None,
2539
) extends HasSharedOptions with HasSharedWatchOptions
2640
// format: on
2741

2842
object PublishLocalOptions {
2943
implicit lazy val parser: Parser[PublishLocalOptions] = Parser.derive
3044
implicit lazy val help: Help[PublishLocalOptions] = Help.derive
3145
val cmdName = "publish local"
32-
private val helpHeader = "Publishes build artifacts to the local Ivy2 repository."
46+
private val helpHeader = "Publishes build artifacts to the local Ivy2 or Maven repository."
3347
private val docWebsiteSuffix = "publishing/publish-local"
3448
val helpMessage: String =
3549
s"""$helpHeader

modules/cli/src/main/scala/scala/cli/commands/publish/RepoParams.scala

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ object RepoParams {
8080
repo match {
8181
case "ivy2-local" =>
8282
RepoParams.ivy2Local(ivy2HomeOpt)
83+
case "m2-local" | "maven-local" =>
84+
RepoParams.m2Local(None)
8385
case "sonatype" | "central" | "maven-central" | "mvn-central" =>
8486
logger.message(s"Using Portal OSSRH Staging API: $sonatypeOssrhStagingApiBase")
8587
RepoParams.centralRepo(
@@ -245,4 +247,19 @@ object RepoParams {
245247
)
246248
}
247249

250+
def m2Local(m2HomeOpt: Option[os.Path]): RepoParams = {
251+
val base = m2HomeOpt.getOrElse(os.home / ".m2" / "repository")
252+
RepoParams(
253+
repo = PublishRepository.Simple(MavenRepository(base.toNIO.toUri.toASCIIString)),
254+
targetRepoOpt = None,
255+
hooks = Hooks.dummy,
256+
isIvy2LocalLike = false,
257+
defaultParallelUpload = true,
258+
supportsSig = true,
259+
acceptsChecksums = true,
260+
shouldSign = false,
261+
shouldAuthenticate = false
262+
)
263+
}
264+
248265
}

modules/integration/src/test/scala/scala/cli/integration/PublishLocalTestDefinitions.scala

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,116 @@ abstract class PublishLocalTestDefinitions extends ScalaCliSuite with TestScalaV
349349
}
350350
}
351351

352+
test("publish local --m2") {
353+
val expectedFiles = {
354+
val modName = s"${PublishTestInputs.testName}_$testedPublishedScalaVersion"
355+
val base =
356+
os.rel / PublishTestInputs.testOrg.split('.').toSeq / modName / testPublishVersion
357+
val baseFiles = Seq(
358+
base / s"$modName-$testPublishVersion.jar",
359+
base / s"$modName-$testPublishVersion.pom",
360+
base / s"$modName-$testPublishVersion-sources.jar",
361+
base / s"$modName-$testPublishVersion-javadoc.jar"
362+
)
363+
baseFiles
364+
.flatMap { f =>
365+
val md5 = f / os.up / s"${f.last}.md5"
366+
val sha1 = f / os.up / s"${f.last}.sha1"
367+
Seq(f, md5, sha1)
368+
}
369+
.toSet
370+
}
371+
372+
PublishTestInputs.inputs()
373+
.fromRoot { root =>
374+
os.proc(
375+
TestUtil.cli,
376+
"--power",
377+
"publish",
378+
"local",
379+
".",
380+
"--m2",
381+
"--m2-home",
382+
(root / "m2repo").toString,
383+
extraOptions
384+
)
385+
.call(cwd = root)
386+
val m2Local = root / "m2repo"
387+
val foundFiles = os.walk(m2Local)
388+
.filter(os.isFile(_))
389+
.map(_.relativeTo(m2Local))
390+
.toSet
391+
val missingFiles = expectedFiles -- foundFiles
392+
val unexpectedFiles = foundFiles -- expectedFiles
393+
if (missingFiles.nonEmpty)
394+
pprint.err.log(missingFiles)
395+
if (unexpectedFiles.nonEmpty)
396+
pprint.err.log(unexpectedFiles)
397+
expect(missingFiles.isEmpty)
398+
expect(unexpectedFiles.isEmpty)
399+
}
400+
}
401+
402+
test("publish local --m2 twice") {
403+
PublishTestInputs.inputs().fromRoot { root =>
404+
val m2Repo = root / "m2repo"
405+
val modName = s"${PublishTestInputs.testName}_$testedPublishedScalaVersion"
406+
val jarPath = m2Repo /
407+
PublishTestInputs.testOrg.split('.').toSeq /
408+
modName / testPublishVersion / s"$modName-$testPublishVersion.jar"
409+
410+
def publishLocal(): os.CommandResult =
411+
os.proc(
412+
TestUtil.cli,
413+
"--power",
414+
"publish",
415+
"local",
416+
".",
417+
"--m2",
418+
"--m2-home",
419+
m2Repo.toString,
420+
"--working-dir",
421+
os.rel / "work-dir",
422+
extraOptions
423+
)
424+
.call(cwd = root)
425+
426+
lazy val depsCp: String =
427+
os.proc(
428+
TestUtil.cs,
429+
"fetch",
430+
"--classpath",
431+
s"com.lihaoyi:os-lib_$testedPublishedScalaVersion:0.11.3"
432+
)
433+
.call(cwd = root)
434+
.out.trim()
435+
436+
def output(): String =
437+
os.proc(
438+
"java",
439+
"-cp",
440+
s"$jarPath${java.io.File.pathSeparator}$depsCp",
441+
"Project"
442+
)
443+
.call(cwd = root)
444+
.out.trim()
445+
446+
val expectedMessage1 = "Hello"
447+
val expectedMessage2 = "olleH"
448+
publishLocal()
449+
val output1 = output()
450+
expect(output1 == expectedMessage1)
451+
452+
os.write.over(
453+
root / PublishTestInputs.projectFilePath,
454+
PublishTestInputs.projFile(expectedMessage2)
455+
)
456+
publishLocal()
457+
val output2 = output()
458+
expect(output2 == expectedMessage2)
459+
}
460+
}
461+
352462
if actualScalaVersion.startsWith("3") then
353463
test("publish local with compileOnly.dep") {
354464
TestInputs(

website/docs/reference/cli-options.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,24 @@ Proceed as if publishing, but do not upload / write artifacts to the remote repo
10881088
### `--parallel-upload`
10891089

10901090
[Internal]
1091+
## Publish local options
1092+
1093+
Available in commands:
1094+
1095+
[`publish local`](./commands.md#publish-local)
1096+
1097+
<!-- Automatically generated, DO NOT EDIT MANUALLY -->
1098+
1099+
### `--m2`
1100+
1101+
Aliases: `--maven-local`
1102+
1103+
Publish to the local Maven repository (defaults to ~/.m2/repository) instead of Ivy2 local
1104+
1105+
### `--m2-home`
1106+
1107+
Set the local Maven repository path (defaults to ~/.m2/repository)
1108+
10911109
## Publish params options
10921110

10931111
Available in commands:

website/docs/reference/commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,15 +261,15 @@ Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [c
261261

262262
## publish local
263263

264-
Publishes build artifacts to the local Ivy2 repository.
264+
Publishes build artifacts to the local Ivy2 or Maven repository.
265265

266266
For detailed documentation refer to our website: https://scala-cli.virtuslab.org/docs/commands/publishing/publish-local
267267

268268
The `publish-local` sub-command is experimental.
269269
Please bear in mind that non-ideal user experience should be expected.
270270
If you encounter any bugs or have feedback to share, make sure to reach out to the maintenance team at https://github.com/VirtusLab/scala-cli
271271

272-
Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [pgp scala signing](./cli-options.md#pgp-scala-signing-options), [power](./cli-options.md#power-options), [publish](./cli-options.md#publish-options), [publish params](./cli-options.md#publish-params-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options)
272+
Accepts option groups: [benchmarking](./cli-options.md#benchmarking-options), [compilation server](./cli-options.md#compilation-server-options), [coursier](./cli-options.md#coursier-options), [cross](./cli-options.md#cross-options), [debug](./cli-options.md#debug-options), [dependency](./cli-options.md#dependency-options), [global suppress warning](./cli-options.md#global-suppress-warning-options), [help group](./cli-options.md#help-group-options), [input](./cli-options.md#input-options), [jvm](./cli-options.md#jvm-options), [logging](./cli-options.md#logging-options), [main class](./cli-options.md#main-class-options), [markdown](./cli-options.md#markdown-options), [pgp scala signing](./cli-options.md#pgp-scala-signing-options), [power](./cli-options.md#power-options), [publish](./cli-options.md#publish-options), [publish local](./cli-options.md#publish-local-options), [publish params](./cli-options.md#publish-params-options), [python](./cli-options.md#python-options), [Scala.js](./cli-options.md#scalajs-options), [Scala Native](./cli-options.md#scala-native-options), [scalac](./cli-options.md#scalac-options), [scalac extra](./cli-options.md#scalac-extra-options), [scope](./cli-options.md#scope-options), [semantic db](./cli-options.md#semantic-db-options), [shared](./cli-options.md#shared-options), [snippet](./cli-options.md#snippet-options), [source generator](./cli-options.md#source-generator-options), [suppress warning](./cli-options.md#suppress-warning-options), [verbosity](./cli-options.md#verbosity-options), [version](./cli-options.md#version-options), [watch](./cli-options.md#watch-options), [workspace](./cli-options.md#workspace-options)
273273

274274
## publish setup
275275

0 commit comments

Comments
 (0)