From 0cd8849ec197b11bc575c6cbaf807f471142928d Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Thu, 10 Oct 2013 21:36:11 -0400 Subject: [PATCH] Work around various issues with Maven local repositories. Fixes #321. * when mvn does a local 'install', it doesn't update the pom.xml last modified time if the pom.xml content hasn't changed * an ivy.xml includes publicationDate, so an ivy.xml will always be touched even if the other content hasn't changed * when Ivy checks if a snapshot is uptodate + it sees a SNAPSHOT, so it knows the module metadata and artifacts might change + it then checks the lastModified time of the metadata + if unchanged, it uses the cached information + if useOrigin is effectively false (either it is explicitly false or a resource is remote/isLocal=false), this means that a new artifact won't be retrieved * the Ivy IBiblioResolver + must be used for Maven repositories for proper behavior (no FileResolver, for example) + only returns URLResources, even for file: URLs + a FileResource is needed in combination with useOrigin to avoid copying artifacts from .m2/repository/ This commit fixes the above by setting a custom URLRepository on a constructed IBiblioResolver. This URLRepository returns FileResources for file: URLs and standard URLResources for others. The returned FileResource has isLocal=true and sbt sets useOrigin=true by default, so the artifacts are used from the origin. If it turns out a similar situation happens when mvn publishes to remote repositories, it is likely the fix for that would be to figure out how to disable the lastModified check on the metadata and always download the metadata. This would be slower, however. --- ivy/src/main/scala/sbt/ConvertResolver.scala | 16 +++++++ .../dependency-management/mvn-local/build.sbt | 28 ++++++++++++ .../mvn-local/changes/libA.scala | 1 + .../mvn-local/changes/libDeps.sbt | 1 + .../mvn-local/changes/mainB1.scala | 1 + .../mvn-local/changes/mainB2.scala | 3 ++ .../mvn-local/changes/mvnLocal.sbt | 1 + .../dependency-management/mvn-local/test | 44 +++++++++++++++++++ util/io/src/main/scala/sbt/IO.scala | 2 +- 9 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 sbt/src/sbt-test/dependency-management/mvn-local/build.sbt create mode 100644 sbt/src/sbt-test/dependency-management/mvn-local/changes/libA.scala create mode 100644 sbt/src/sbt-test/dependency-management/mvn-local/changes/libDeps.sbt create mode 100644 sbt/src/sbt-test/dependency-management/mvn-local/changes/mainB1.scala create mode 100644 sbt/src/sbt-test/dependency-management/mvn-local/changes/mainB2.scala create mode 100644 sbt/src/sbt-test/dependency-management/mvn-local/changes/mvnLocal.sbt create mode 100644 sbt/src/sbt-test/dependency-management/mvn-local/test diff --git a/ivy/src/main/scala/sbt/ConvertResolver.scala b/ivy/src/main/scala/sbt/ConvertResolver.scala index 91fa137a2..58ba6d4ac 100644 --- a/ivy/src/main/scala/sbt/ConvertResolver.scala +++ b/ivy/src/main/scala/sbt/ConvertResolver.scala @@ -3,6 +3,7 @@ */ package sbt +import java.net.URL import java.util.Collections import org.apache.ivy.{core,plugins} import core.module.id.ModuleRevisionId @@ -11,6 +12,8 @@ import core.resolve.ResolveData import core.settings.IvySettings import plugins.resolver.{BasicResolver, DependencyResolver, IBiblioResolver} import plugins.resolver.{AbstractPatternsBasedResolver, AbstractSshBasedResolver, FileSystemResolver, SFTPResolver, SshResolver, URLResolver} +import plugins.repository.url.{URLRepository => URLRepo} +import plugins.repository.file.{FileRepository => FileRepo, FileResource} private object ConvertResolver { @@ -29,6 +32,7 @@ private object ConvertResolver } } val resolver = new PluginCapableResolver + resolver.setRepository(new LocalIfFileRepo) initializeMavenStyle(resolver, repo.name, repo.root) resolver.setPatterns() // has to be done after initializeMavenStyle, which calls methods that overwrite the patterns resolver @@ -128,4 +132,16 @@ private object ConvertResolver patterns.ivyPatterns.foreach(p => resolver.addIvyPattern(settings substitute p)) patterns.artifactPatterns.foreach(p => resolver.addArtifactPattern(settings substitute p)) } + /** A custom Ivy URLRepository that returns FileResources for file URLs. + * This allows using the artifacts from the Maven local repository instead of copying them to the Ivy cache. */ + private[this] final class LocalIfFileRepo extends URLRepo { + private[this] val repo = new FileRepo + override def getResource(source: String) = { + val url = new URL(source) + if(url.getProtocol == IO.FileScheme) + new FileResource(repo, IO.toFile(url)) + else + super.getResource(source) + } + } } diff --git a/sbt/src/sbt-test/dependency-management/mvn-local/build.sbt b/sbt/src/sbt-test/dependency-management/mvn-local/build.sbt new file mode 100644 index 000000000..f29ff5029 --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/mvn-local/build.sbt @@ -0,0 +1,28 @@ +organization in ThisBuild := "org.example" + +version in ThisBuild := "1.0-SNAPSHOT" + + +lazy val main = project.settings( + uniqueName, + libraryDependencies += (projectID in library).value +) + +lazy val library = project.settings(uniqueName) + +def uniqueName = + name := (name.value + "-" + randomSuffix( (baseDirectory in ThisBuild).value)) + +// better long-term approach to a clean cache/local +// would be to not use the actual ~/.m2/repository +def randomSuffix(base: File) = { + // need to persist it so that it doesn't change across reloads + val persist = base / "suffix" + if(persist.exists) + IO.read(persist) + else { + val s = Hash.halfHashString(System.currentTimeMillis.toString) + IO.write(persist, s) + s + } +} diff --git a/sbt/src/sbt-test/dependency-management/mvn-local/changes/libA.scala b/sbt/src/sbt-test/dependency-management/mvn-local/changes/libA.scala new file mode 100644 index 000000000..6ea495dda --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/mvn-local/changes/libA.scala @@ -0,0 +1 @@ +object A { val x = 3 } diff --git a/sbt/src/sbt-test/dependency-management/mvn-local/changes/libDeps.sbt b/sbt/src/sbt-test/dependency-management/mvn-local/changes/libDeps.sbt new file mode 100644 index 000000000..f61fd2191 --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/mvn-local/changes/libDeps.sbt @@ -0,0 +1 @@ +libraryDependencies += "junit" % "junit" % "4.11" diff --git a/sbt/src/sbt-test/dependency-management/mvn-local/changes/mainB1.scala b/sbt/src/sbt-test/dependency-management/mvn-local/changes/mainB1.scala new file mode 100644 index 000000000..53f6f4eea --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/mvn-local/changes/mainB1.scala @@ -0,0 +1 @@ +object B { val y = A.x } diff --git a/sbt/src/sbt-test/dependency-management/mvn-local/changes/mainB2.scala b/sbt/src/sbt-test/dependency-management/mvn-local/changes/mainB2.scala new file mode 100644 index 000000000..65b80c3bd --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/mvn-local/changes/mainB2.scala @@ -0,0 +1,3 @@ +import org.junit.Test + +object B { val y = A.x } diff --git a/sbt/src/sbt-test/dependency-management/mvn-local/changes/mvnLocal.sbt b/sbt/src/sbt-test/dependency-management/mvn-local/changes/mvnLocal.sbt new file mode 100644 index 000000000..3e70dbffe --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/mvn-local/changes/mvnLocal.sbt @@ -0,0 +1 @@ +resolvers := (Resolver.mavenLocal +: resolvers.value) diff --git a/sbt/src/sbt-test/dependency-management/mvn-local/test b/sbt/src/sbt-test/dependency-management/mvn-local/test new file mode 100644 index 000000000..a6b7f81d1 --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/mvn-local/test @@ -0,0 +1,44 @@ +> library/publishM2 + +# should fail because local Maven repository not added yet +-> main/update + +# should succeed now that local Maven repository is added +$ copy-file changes/mvnLocal.sbt main/build.sbt +> reload +> main/update + +# should succeed because 'main' doesn't actually use anything from the library yet +> main/compile + +$ copy-file changes/mainB1.scala main/B.scala +# should fail because there is nothing in the library jar and 'main' actually uses it now +-> main/compile + +$ copy-file changes/libA.scala library/A.scala +> library/publishM2 +# should succeed even without 'update' because Ivy should use the jar from the origin and not copy it to its cache +> main/compile + +# should still succeed with an explicit 'update' +> main/update +> main/compile + + + +# update B.scala to depend on a dependency that 'library' doesn't declare yet +$ delete main/B.scala +$ copy-file changes/mainB2.scala main/B.scala + +# it should fail to compile because that dependency isn't declared +-> main/compile + + +# add a dependency to 'library' and use it from 'main' +# this will require the metadata to be updated correctly and not just the artifact +$ copy-file changes/libDeps.sbt library/build.sbt +> reload +> library/publishM2 + +# should succeed now that the dependency is there +> main/compile diff --git a/util/io/src/main/scala/sbt/IO.scala b/util/io/src/main/scala/sbt/IO.scala index 9534eea1d..ed976572b 100644 --- a/util/io/src/main/scala/sbt/IO.scala +++ b/util/io/src/main/scala/sbt/IO.scala @@ -29,7 +29,7 @@ object IO /** The size of the byte or char buffer used in various methods.*/ private val BufferSize = 8192 /** File scheme name */ - private[this] val FileScheme = "file" + private[sbt] val FileScheme = "file" /** The newline string for this system, as obtained by the line.separator system property. */ val Newline = System.getProperty("line.separator")