mirror of https://github.com/sbt/sbt.git
[2.x] fix: Substitute [organisation] literally by defaulting Patterns.isMavenCompatible to false (#9344)
**Problem** In an Ivy resolver pattern, the [organisation]/[organization] token has its dots rewritten to slashes (org.example → org/example), behaving like Ivy's [orgPath] token rather than being literal. Per the [Apache Ivy spec](http://ant.apache.org/ivy/history/latest-milestone/concept.html), [organisation] should be substituted literally and [orgPath] is the slash-separated form. Root cause: Patterns.isMavenCompatible defaulted to true, which sbt forwards to Apache Ivy as setM2compatible(true); with m2-compatibility on, Ivy rewrites the [organisation] token to slash form. A user supplying a custom Ivy pattern (e.g. for an SFTP/SSH resolver) inherited that default and got the wrong paths, with no obvious indication why. The only workaround was the non-obvious withIsMavenCompatible(false). **Solution** Flip the default of Patterns.isMavenCompatible from true to false, so a hand-written Patterns keeps [organisation] literal by default — matching the Ivy spec.
This commit is contained in:
parent
c33b795c78
commit
73d164fa84
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
// DO NOT EDIT MANUALLY
|
||||
package sbt.librarymanagement
|
||||
/** @param isMavenCompatible When false (the default), the [organisation]/[organization] token is substituted literally,
|
||||
so an organization such as `org.example` stays `org.example` in resolved paths, matching the
|
||||
Apache Ivy specification. When true, the Ivy engine rewrites the [organisation] token to the
|
||||
slash-separated form (`org/example`), as in a Maven m2-compatible repository layout. */
|
||||
final class Patterns private (
|
||||
val ivyPatterns: Vector[String],
|
||||
val artifactPatterns: Vector[String],
|
||||
|
|
@ -11,7 +15,7 @@ final class Patterns private (
|
|||
val descriptorOptional: Boolean,
|
||||
val skipConsistencyCheck: Boolean) extends Serializable {
|
||||
|
||||
private def this() = this(Vector.empty, Vector.empty, true, false, false)
|
||||
private def this() = this(Vector.empty, Vector.empty, false, false, false)
|
||||
|
||||
override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match {
|
||||
case x: Patterns => (this.ivyPatterns == x.ivyPatterns) && (this.artifactPatterns == x.artifactPatterns) && (this.isMavenCompatible == x.isMavenCompatible) && (this.descriptorOptional == x.descriptorOptional) && (this.skipConsistencyCheck == x.skipConsistencyCheck)
|
||||
|
|
|
|||
|
|
@ -532,7 +532,18 @@
|
|||
"fields": [
|
||||
{ "name": "ivyPatterns", "type": "String*", "default": "Vector.empty", "since": "0.0.1" },
|
||||
{ "name": "artifactPatterns", "type": "String*", "default": "Vector.empty", "since": "0.0.1" },
|
||||
{ "name": "isMavenCompatible", "type": "boolean", "default": "true", "since": "0.0.1" },
|
||||
{
|
||||
"name": "isMavenCompatible",
|
||||
"doc": [
|
||||
"When false (the default), the [organisation]/[organization] token is substituted literally,",
|
||||
"so an organization such as `org.example` stays `org.example` in resolved paths, matching the",
|
||||
"Apache Ivy specification. When true, the Ivy engine rewrites the [organisation] token to the",
|
||||
"slash-separated form (`org/example`), as in a Maven m2-compatible repository layout."
|
||||
],
|
||||
"type": "boolean",
|
||||
"default": "false",
|
||||
"since": "0.0.1"
|
||||
},
|
||||
{ "name": "descriptorOptional", "type": "boolean", "default": "false", "since": "0.0.1" },
|
||||
{ "name": "skipConsistencyCheck", "type": "boolean", "default": "false", "since": "0.0.1" }
|
||||
],
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ private[librarymanagement] abstract class MavenRepositoryFunctions {
|
|||
private[librarymanagement] abstract class PatternsFunctions {
|
||||
implicit def defaultPatterns: Patterns = Resolver.defaultPatterns
|
||||
|
||||
def apply(artifactPatterns: String*): Patterns = Patterns(true, artifactPatterns*)
|
||||
def apply(artifactPatterns: String*): Patterns = Patterns(false, artifactPatterns*)
|
||||
def apply(isMavenCompatible: Boolean, artifactPatterns: String*): Patterns = {
|
||||
val patterns = artifactPatterns.toVector
|
||||
Patterns()
|
||||
|
|
@ -293,13 +293,27 @@ private[librarymanagement] abstract class ResolverFunctions {
|
|||
construct(name, SshConnection(None, hostname, port), resolvePatterns(basePath, basePatterns))
|
||||
}
|
||||
|
||||
/** A factory to construct an interface to an Ivy SSH resolver. */
|
||||
/**
|
||||
* A factory to construct an interface to an Ivy SSH resolver.
|
||||
*
|
||||
* The implicit `basePatterns` default to [[mavenStylePatterns]] (`isMavenCompatible = true`), under
|
||||
* which the `[organisation]` token is rendered in slash-separated form. To keep the organization
|
||||
* literal (e.g. `org.example`), supply Ivy-style patterns such as a [[Patterns]] built with
|
||||
* `isMavenCompatible = false` (the [[Patterns]] default). See issue #535.
|
||||
*/
|
||||
object ssh extends Define[SshRepository] {
|
||||
protected def construct(name: String, connection: SshConnection, patterns: Patterns) =
|
||||
SshRepository(name, connection, patterns, None)
|
||||
}
|
||||
|
||||
/** A factory to construct an interface to an Ivy SFTP resolver. */
|
||||
/**
|
||||
* A factory to construct an interface to an Ivy SFTP resolver.
|
||||
*
|
||||
* The implicit `basePatterns` default to [[mavenStylePatterns]] (`isMavenCompatible = true`), under
|
||||
* which the `[organisation]` token is rendered in slash-separated form. To keep the organization
|
||||
* literal (e.g. `org.example`), supply Ivy-style patterns such as a [[Patterns]] built with
|
||||
* `isMavenCompatible = false` (the [[Patterns]] default). See issue #535.
|
||||
*/
|
||||
object sftp extends Define[SftpRepository] {
|
||||
protected def construct(name: String, connection: SshConnection, patterns: Patterns) =
|
||||
SftpRepository(name, connection, patterns)
|
||||
|
|
@ -369,7 +383,18 @@ private[librarymanagement] abstract class ResolverFunctions {
|
|||
else normBase + "/" + pattern
|
||||
}
|
||||
def defaultFileConfiguration = FileConfiguration(true, None)
|
||||
def mavenStylePatterns = Patterns().withArtifactPatterns(Vector(mavenStyleBasePattern))
|
||||
|
||||
/**
|
||||
* Maven-compatible layout (`isMavenCompatible = true`). The `[organisation]`/`[organization]` token
|
||||
* is rendered in slash-separated form (`org.example` becomes `org/example`) by the Ivy engine.
|
||||
*/
|
||||
def mavenStylePatterns =
|
||||
Patterns().withArtifactPatterns(Vector(mavenStyleBasePattern)).withIsMavenCompatible(true)
|
||||
|
||||
/**
|
||||
* Ivy-style layout (`isMavenCompatible = false`). The `[organisation]`/`[organization]` token is
|
||||
* substituted literally, keeping the dots (`org.example` stays `org.example`).
|
||||
*/
|
||||
def ivyStylePatterns = defaultIvyPatterns // Patterns(Nil, Nil, false)
|
||||
|
||||
def defaultPatterns = mavenStylePatterns
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import java.net.URI
|
|||
|
||||
import sbt.internal.librarymanagement.UnitSpec
|
||||
|
||||
object ResolverTest extends UnitSpec {
|
||||
class ResolverTest extends UnitSpec {
|
||||
|
||||
"Resolver url" should "propagate pattern descriptorOptional and skipConsistencyCheck." in {
|
||||
val pats = Vector("[orgPath]")
|
||||
|
|
@ -27,4 +27,60 @@ object ResolverTest extends UnitSpec {
|
|||
assert(patterns.skipConsistencyCheck)
|
||||
assert(patterns.descriptorOptional)
|
||||
}
|
||||
|
||||
"Patterns" should "default to isMavenCompatible = false (literal [organisation], see #535)." in {
|
||||
Patterns().isMavenCompatible shouldBe false
|
||||
}
|
||||
|
||||
it should "default the varargs shorthand to isMavenCompatible = false, like the builder forms." in {
|
||||
// Patterns("[organisation]/...") must agree with Patterns().withArtifactPatterns(...) so that the
|
||||
// organization token is treated consistently regardless of how the Patterns is constructed (#535).
|
||||
Patterns("[organisation]/[module]/[artifact]-[revision].[ext]").isMavenCompatible shouldBe false
|
||||
}
|
||||
|
||||
"Resolver.mavenStylePatterns" should "stay Maven-compatible (isMavenCompatible = true)." in {
|
||||
Resolver.mavenStylePatterns.isMavenCompatible shouldBe true
|
||||
}
|
||||
|
||||
"Resolver.ivyStylePatterns" should "be Ivy-style (isMavenCompatible = false)." in {
|
||||
Resolver.ivyStylePatterns.isMavenCompatible shouldBe false
|
||||
}
|
||||
|
||||
"Resolver.sftp" should "keep an Ivy SFTP resolver's custom [organisation] patterns literal by default." in {
|
||||
// Reproduces the configuration from issue #535: a custom Ivy pattern using [organisation].
|
||||
// With the isMavenCompatible = false default, sbt stores the token verbatim and instructs the
|
||||
// Ivy engine (via setM2compatible(false)) not to rewrite the organization to slash form.
|
||||
val ivy = "/var/ivy/repo/[organisation]/[module]/ivys/ivy-[revision].xml"
|
||||
val artifact = "/var/ivy/repo/[organisation]/[module]/[type]s/[artifact]-[revision].[ext]"
|
||||
val patterns = Resolver
|
||||
.sftp("repo", "example.org", 22)(using
|
||||
Patterns().withIvyPatterns(Vector(ivy)).withArtifactPatterns(Vector(artifact))
|
||||
)
|
||||
.patterns
|
||||
patterns.ivyPatterns shouldBe Vector(ivy)
|
||||
patterns.artifactPatterns shouldBe Vector(artifact)
|
||||
patterns.isMavenCompatible shouldBe false
|
||||
}
|
||||
|
||||
it should "stay Maven-compatible when constructed with the default mavenStylePatterns." in {
|
||||
Resolver.sftp("repo", "example.org", 22).patterns.isMavenCompatible shouldBe true
|
||||
}
|
||||
|
||||
// The historical fluent syntax from issue #535, `Resolver.sftp(...).artifacts(...).ivys(...)`, was
|
||||
// removed in sbt 2.x; `withPatterns` is the supported replacement. These cases mirror the original
|
||||
// report's exact patterns (both the `[organization]` and `[organisation]` spellings the reporter
|
||||
// tried) to lock the behavior down from the client side.
|
||||
it should "keep the issue #535 patterns literal when set via withPatterns (both org spellings)." in {
|
||||
val ivys = "/var/ivy/cirque-repo/[organisation]/[module]/ivys/ivy-[revision].xml"
|
||||
val artifacts =
|
||||
"/var/ivy/cirque-repo/[organization]/[module]/[type]s/[artifact]-[revision].[ext]"
|
||||
val repo = Resolver
|
||||
.sftp("Cirque-ivy-repo", "daisyduck.cirque.dk", 22)
|
||||
.withPatterns(
|
||||
Patterns().withIvyPatterns(Vector(ivys)).withArtifactPatterns(Vector(artifacts))
|
||||
)
|
||||
repo.patterns.ivyPatterns shouldBe Vector(ivys)
|
||||
repo.patterns.artifactPatterns shouldBe Vector(artifacts)
|
||||
repo.patterns.isMavenCompatible shouldBe false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2023, Scala center
|
||||
* Copyright 2011 - 2022, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal.librarymanagement
|
||||
|
||||
import org.apache.ivy.core.settings.IvySettings
|
||||
import org.apache.ivy.plugins.resolver.AbstractPatternsBasedResolver
|
||||
import sbt.internal.librarymanagement.ivy.UpdateOptions
|
||||
import sbt.internal.util.ConsoleLogger
|
||||
import sbt.librarymanagement.{ Patterns, URLRepository }
|
||||
import verify.BasicTestSuite
|
||||
|
||||
import scala.jdk.CollectionConverters.*
|
||||
|
||||
// Regression coverage for sbt/sbt#535: the organization token in an Ivy SFTP/SSH/URL resolver pattern
|
||||
// should be rendered literally unless the resolver is explicitly Maven-compatible. ConvertResolver is the
|
||||
// boundary sbt owns: it maps Patterns.isMavenCompatible onto Apache Ivy's setM2compatible, which is what
|
||||
// makes Ivy keep [organisation] literal (false) or rewrite it to slash form (true).
|
||||
object ConvertResolverSpec extends BasicTestSuite {
|
||||
private val log = ConsoleLogger()
|
||||
|
||||
// The configuration from issue #535: a custom Ivy pattern using the [organisation] token.
|
||||
private val orgPatterns =
|
||||
Patterns()
|
||||
.withIvyPatterns(
|
||||
Vector("https://example.org/repo/[organisation]/[module]/ivys/ivy-[revision].xml")
|
||||
)
|
||||
.withArtifactPatterns(
|
||||
Vector(
|
||||
"https://example.org/repo/[organisation]/[module]/[type]s/[artifact]-[revision].[ext]"
|
||||
)
|
||||
)
|
||||
|
||||
private def convert(patterns: Patterns): AbstractPatternsBasedResolver =
|
||||
ConvertResolver(URLRepository("test-repo", patterns), new IvySettings, UpdateOptions(), log)
|
||||
.asInstanceOf[AbstractPatternsBasedResolver]
|
||||
|
||||
test("the default Patterns keeps the Ivy resolver non-m2compatible (issue #535)") {
|
||||
assert(!orgPatterns.isMavenCompatible)
|
||||
assert(!convert(orgPatterns).isM2compatible)
|
||||
}
|
||||
|
||||
test("isMavenCompatible = false renders the [organisation] token literally") {
|
||||
val resolver = convert(orgPatterns)
|
||||
assert(!resolver.isM2compatible)
|
||||
// sbt forwards the pattern verbatim; with m2compatible off Ivy does not rewrite the organization.
|
||||
assert(resolver.getArtifactPatterns.asScala.exists(_.toString.contains("[organisation]")))
|
||||
}
|
||||
|
||||
test(
|
||||
"isMavenCompatible = true makes the Ivy resolver m2compatible (organization rewritten to slash form)"
|
||||
) {
|
||||
assert(convert(orgPatterns.withIsMavenCompatible(true)).isM2compatible)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
## `Patterns.isMavenCompatible` now defaults to `false`
|
||||
|
||||
The default value of `Patterns.isMavenCompatible` changed from `true` to `false` so that the
|
||||
`[organisation]`/`[organization]` token in an Ivy pattern is substituted literally, matching the
|
||||
Apache Ivy specification (see [#535](https://github.com/sbt/sbt/issues/535)).
|
||||
|
||||
### What changed
|
||||
|
||||
```scala
|
||||
// before: organization "org.example" rendered as "org/example"
|
||||
// now: organization "org.example" rendered as "org.example"
|
||||
val p = Patterns()
|
||||
.withIvyPatterns(Vector("[organisation]/[module]/ivys/ivy-[revision].xml"))
|
||||
.withArtifactPatterns(Vector("[organisation]/[module]/[type]s/[artifact]-[revision].[ext]"))
|
||||
```
|
||||
|
||||
Use the `[orgPath]` token if you want the slash-separated form regardless of the flag, or set
|
||||
`isMavenCompatible = true` explicitly to opt back into the previous Maven m2-compatible behavior:
|
||||
|
||||
```scala
|
||||
Patterns(/* ... */).withIsMavenCompatible(true)
|
||||
```
|
||||
|
||||
### Who is affected
|
||||
|
||||
- Custom Ivy/SFTP/SSH/URL/file resolvers built with a hand-written `Patterns` that uses the
|
||||
`[organisation]` token and previously relied on the implicit dot-to-slash rewrite.
|
||||
- Under the Ivy engine, the organization is now rendered literally for these patterns.
|
||||
- Under Coursier (the default since sbt 1.3), SFTP/SSH resolvers are ignored entirely, so they
|
||||
are unaffected. Custom **URL/file** pattern resolvers, however, can still be affected: Coursier
|
||||
chooses between Maven-base extraction and Ivy-pattern parsing based on `isMavenCompatible`, so a
|
||||
resolver that relied on the old `true` default may now be parsed as an Ivy-pattern repository.
|
||||
Set `isMavenCompatible = true` explicitly to keep the Maven handling.
|
||||
- The built-in `Resolver.mavenStylePatterns` is unchanged: it remains explicitly
|
||||
`isMavenCompatible = true`, so `Resolver.url`/`Resolver.file`/`Resolver.sftp`/`Resolver.ssh`
|
||||
constructed with the default patterns continue to use the Maven layout.
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
// Regression guard related to sbt/sbt#535.
|
||||
//
|
||||
// Patterns.isMavenCompatible now defaults to false (so a custom Ivy pattern keeps [organisation]
|
||||
// literal). Resolver.file still uses Resolver.mavenStylePatterns, which is explicitly Maven-compatible,
|
||||
// so publishing to it must keep producing the standard slash-separated Maven layout.
|
||||
|
||||
ThisBuild / scalaVersion := "2.12.21"
|
||||
|
||||
lazy val lib = project
|
||||
.settings(
|
||||
organization := "org.example",
|
||||
name := "lib",
|
||||
version := "1.0",
|
||||
publishMavenStyle := true,
|
||||
publishTo := Some(Resolver.file("dist", (ThisBuild / baseDirectory).value / "repo"))
|
||||
)
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# publish under the default (Coursier/ivyless) engine
|
||||
> lib/publish
|
||||
|
||||
# the organization must be laid out slash-separated (Maven layout) via mavenStylePatterns,
|
||||
# which stays Maven-compatible even though the Patterns default flipped to false
|
||||
$ exists repo/org/example/lib_2.12/1.0/lib_2.12-1.0.jar
|
||||
$ exists repo/org/example/lib_2.12/1.0/lib_2.12-1.0.pom
|
||||
|
||||
# the literal dotted organization directory (Ivy layout) must NOT be produced
|
||||
-$ exists repo/org.example/lib_2.12/1.0/lib_2.12-1.0.jar
|
||||
Loading…
Reference in New Issue