Merge branch '0.13' into follow-deprecation

This commit is contained in:
Jacek Laskowski 2014-04-07 22:20:55 +02:00
commit 24f31804af
8 changed files with 378 additions and 48 deletions

View File

@ -10,13 +10,91 @@ import core.module.id.ModuleRevisionId
import core.module.descriptor.DependencyDescriptor
import core.resolve.ResolveData
import core.settings.IvySettings
import plugins.resolver.{BasicResolver, DependencyResolver, IBiblioResolver}
import plugins.resolver.{BasicResolver, DependencyResolver, IBiblioResolver, RepositoryResolver}
import plugins.resolver.{AbstractPatternsBasedResolver, AbstractSshBasedResolver, FileSystemResolver, SFTPResolver, SshResolver, URLResolver}
import plugins.repository.url.{URLRepository => URLRepo}
import plugins.repository.file.{FileRepository => FileRepo, FileResource}
import java.io.File
import org.apache.ivy.util.ChecksumHelper
import org.apache.ivy.core.module.descriptor.{Artifact=>IArtifact}
private object ConvertResolver
{
/** This class contains all the reflective lookups used in the
* checksum-friendly URL publishing shim.
*/
private object ChecksumFriendlyURLResolver {
// TODO - When we dump JDK6 support we can remove this hackery
// import java.lang.reflect.AccessibleObject
type AccessibleObject = {
def setAccessible(value: Boolean): Unit
}
private def reflectiveLookup[A <: AccessibleObject](f: Class[_] => A): Option[A] =
try {
val cls = classOf[RepositoryResolver]
val thing = f(cls)
import scala.language.reflectiveCalls
thing.setAccessible(true)
Some(thing)
} catch {
case (_: java.lang.NoSuchFieldException) |
(_: java.lang.SecurityException) |
(_: java.lang.NoSuchMethodException) => None
}
private val signerNameField: Option[java.lang.reflect.Field] =
reflectiveLookup(_.getDeclaredField("signerName"))
private val putChecksumMethod: Option[java.lang.reflect.Method] =
reflectiveLookup(_.getDeclaredMethod("putChecksum",
classOf[IArtifact], classOf[File], classOf[String],
classOf[Boolean], classOf[String]))
private val putSignatureMethod: Option[java.lang.reflect.Method] =
reflectiveLookup(_.getDeclaredMethod("putSignature",
classOf[IArtifact], classOf[File], classOf[String],
classOf[Boolean]))
}
/**
* The default behavior of ivy's overwrite flags ignores the fact that a lot of repositories
* will autogenerate checksums *for* an artifact if it doesn't already exist. Therefore
* if we succeed in publishing an artifact, we need to just blast the checksums in place.
* This acts as a "shim" on RepositoryResolvers so that we can hook our methods into
* both the IBiblioResolver + URLResolver without having to duplicate the code in two
* places. However, this does mean our use of reflection is awesome.
*
* TODO - See about contributing back to ivy.
*/
private trait ChecksumFriendlyURLResolver extends RepositoryResolver {
import ChecksumFriendlyURLResolver._
private def signerName: String = signerNameField match {
case Some(field) => field.get(this).asInstanceOf[String]
case None => null
}
override protected def put(artifact: IArtifact, src: File, dest: String, overwrite: Boolean): Unit = {
// verify the checksum algorithms before uploading artifacts!
val checksums = getChecksumAlgorithms()
val repository = getRepository()
for {
checksum <- checksums
if !ChecksumHelper.isKnownAlgorithm(checksum)
} throw new IllegalArgumentException("Unknown checksum algorithm: " + checksum)
repository.put(artifact, src, dest, overwrite);
// Fix for sbt#1156 - Artifactory will auto-generate MD5/sha1 files, so
// we need to overwrite what it has.
for (checksum <- checksums) {
putChecksumMethod match {
case Some(method) => method.invoke(this, artifact, src, dest, true: java.lang.Boolean, checksum)
case None => // TODO - issue warning?
}
}
if (signerName != null) {
putSignatureMethod match {
case None => ()
case Some(method) => method.invoke(artifact, src, dest, true: java.lang.Boolean)
}
}
}
}
/** Converts the given sbt resolver into an Ivy resolver..*/
def apply(r: Resolver, settings: IvySettings, log: Logger) =
{
@ -25,7 +103,7 @@ private object ConvertResolver
case repo: MavenRepository =>
{
val pattern = Collections.singletonList(Resolver.resolvePattern(repo.root, Resolver.mavenStyleBasePattern))
final class PluginCapableResolver extends IBiblioResolver with DescriptorRequired {
final class PluginCapableResolver extends IBiblioResolver with ChecksumFriendlyURLResolver with DescriptorRequired {
def setPatterns() { // done this way for access to protected methods.
setArtifactPatterns(pattern)
setIvyPatterns(pattern)
@ -61,7 +139,13 @@ private object ConvertResolver
}
case repo: FileRepository =>
{
val resolver = new FileSystemResolver with DescriptorRequired
val resolver = new FileSystemResolver with DescriptorRequired {
// Workaround for #1156
// Temporarily in sbt 0.13.x we deprecate overwriting
// in local files for non-changing revisions.
// This will be fully enforced in sbt 1.0.
setRepository(new WarnOnOverwriteFileRepo())
}
resolver.setName(repo.name)
initializePatterns(resolver, repo.patterns, settings)
import repo.configuration.{isLocal, isTransactional}
@ -71,7 +155,7 @@ private object ConvertResolver
}
case repo: URLRepository =>
{
val resolver = new URLResolver with DescriptorRequired
val resolver = new URLResolver with ChecksumFriendlyURLResolver with DescriptorRequired
resolver.setName(repo.name)
initializePatterns(resolver, repo.patterns, settings)
resolver
@ -135,7 +219,7 @@ private object ConvertResolver
/** 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
private[this] val repo = new WarnOnOverwriteFileRepo()
override def getResource(source: String) = {
val url = new URL(source)
if(url.getProtocol == IO.FileScheme)
@ -144,4 +228,16 @@ private object ConvertResolver
super.getResource(source)
}
}
private[this] final class WarnOnOverwriteFileRepo extends FileRepo() {
override def put(source: java.io.File, destination: String, overwrite: Boolean): Unit = {
try super.put(source, destination, overwrite)
catch {
case e: java.io.IOException if e.getMessage.contains("destination already exists") =>
import org.apache.ivy.util.Message
Message.warn(s"Attempting to overwrite $destination\n\tThis usage is deprecated and will be removed in sbt 1.0.")
super.put(source, destination, true)
}
}
}
}

View File

@ -43,7 +43,7 @@ object CustomPomParser
val JarPackagings = Set("eclipse-plugin", "hk2-jar", "orbit")
val default = new CustomPomParser(PomModuleDescriptorParser.getInstance, defaultTransform)
private[this] val TransformedHashKey = "sbtTransformHash"
private[this] val TransformedHashKey = "e:sbtTransformHash"
// A hash of the parameters transformation is based on.
// If a descriptor has a different hash, we need to retransform it.
private[this] val TransformHash: String = hash((unqualifiedKeys ++ JarPackagings).toSeq.sorted)
@ -57,8 +57,14 @@ object CustomPomParser
private[this] def transformedByThisVersion(md: ModuleDescriptor): Boolean =
{
val oldTransformedHashKey = "sbtTransformHash"
val extraInfo = md.getExtraInfo
extraInfo != null && extraInfo.get(TransformedHashKey) == TransformHash
// sbt 0.13.1 used "sbtTransformHash" instead of "e:sbtTransformHash" until #1192 so read both
Option(extraInfo).isDefined &&
((Option(extraInfo get TransformedHashKey) orElse Option(extraInfo get oldTransformedHashKey)) match {
case Some(TransformHash) => true
case _ => false
})
}
private[this] def defaultTransformImpl(parser: ModuleDescriptorParser, md: ModuleDescriptor): ModuleDescriptor =

View File

@ -7,7 +7,6 @@ import Resolver.PluginPattern
import java.io.File
import java.net.URI
import java.text.ParseException
import java.util.concurrent.Callable
import java.util.{Collection, Collections => CS}
import CS.singleton
@ -24,9 +23,7 @@ import core.settings.IvySettings
import plugins.latest.LatestRevisionStrategy
import plugins.matcher.PatternMatcher
import plugins.parser.m2.PomModuleDescriptorParser
import plugins.repository.ResourceDownloader
import plugins.resolver.{ChainResolver, DependencyResolver}
import plugins.resolver.util.ResolvedResource
import util.{Message, MessageLogger}
import util.extendable.ExtendableItem
@ -99,6 +96,8 @@ final class IvySbt(val configuration: IvyConfiguration)
def withIvy[T](log: MessageLogger)(f: Ivy => T): T =
withDefaultLogger(log)
{
// See #429 - We always insert a helper authenticator here which lets us get more useful authentication errors.
ivyint.ErrorMessageAuthenticator.install()
ivy.pushContext()
ivy.getLoggerEngine.pushLogger(log)
try { f(ivy) }
@ -356,41 +355,8 @@ private object IvySbt
case pr: ProjectResolver => true
case _ => false
}
/** This is overridden to delete outofdate artifacts of changing modules that are not listed in the metadata.
* This occurs for artifacts with classifiers, for example. */
@throws(classOf[ParseException])
override def cacheModuleDescriptor(resolver: DependencyResolver, mdRef: ResolvedResource, dd: DependencyDescriptor, moduleArtifact: IArtifact, downloader: ResourceDownloader, options: CacheMetadataOptions): ResolvedModuleRevision =
{
val rmrRaw = super.cacheModuleDescriptor(null, mdRef, dd, moduleArtifact, downloader, options)
val rmr = resetArtifactResolver(rmrRaw)
val mrid = moduleArtifact.getModuleRevisionId
def shouldClear(): Boolean = rmr != null &&
( (rmr.getReport != null && rmr.getReport.isSearched && isChanging(dd, mrid)) ||
isProjectResolver(rmr.getResolver) )
// only handle changing modules whose metadata actually changed.
// Typically, the publication date in the metadata has to change to get here.
if(shouldClear()) {
// this is the locally cached metadata as originally retrieved (e.g. the pom)
val original = rmr.getReport.getOriginalLocalFile
if(original != null) {
// delete all files in subdirectories that are older than the original metadata file's publication date
// The publication date is used because the metadata will be redownloaded for changing files,
// so the last modified time changes, but the publication date doesn't
val pubDate = rmrRaw.getPublicationDate
val lm = if(pubDate eq null) original.lastModified else pubDate.getTime
val indirectFiles = PathFinder(original.getParentFile).*(DirectoryFilter).**(-DirectoryFilter).get.toList
val older = indirectFiles.filter(f => f.lastModified < lm).toList
Message.verbose("Deleting additional old artifacts from cache for changed module " + mrid + older.mkString(":\n\t", "\n\t", ""))
IO.delete(older)
}
}
rmr
}
// ignore the original resolver wherever possible to avoid issues like #704
override def saveResolvers(descriptor: ModuleDescriptor, metadataResolverName: String, artifactResolverName: String) {}
def isChanging(dd: DependencyDescriptor, requestedRevisionId: ModuleRevisionId): Boolean =
!localOnly && (dd.isChanging || requestedRevisionId.getRevision.contains("-SNAPSHOT"))
}
manager.setArtifactPattern(PluginPattern + manager.getArtifactPattern)
manager.setDataFilePattern(PluginPattern + manager.getDataFilePattern)

View File

@ -16,7 +16,11 @@ import core.resolve.ResolveOptions
import plugins.resolver.{BasicResolver, DependencyResolver}
final class DeliverConfiguration(val deliverIvyPattern: String, val status: String, val configurations: Option[Seq[Configuration]], val logging: UpdateLogging.Value)
final class PublishConfiguration(val ivyFile: Option[File], val resolverName: String, val artifacts: Map[Artifact, File], val checksums: Seq[String], val logging: UpdateLogging.Value)
final class PublishConfiguration(val ivyFile: Option[File], val resolverName: String, val artifacts: Map[Artifact, File], val checksums: Seq[String], val logging: UpdateLogging.Value,
val overwrite: Boolean) {
def this(ivyFile: Option[File], resolverName: String, artifacts: Map[Artifact, File], checksums: Seq[String], logging: UpdateLogging.Value) =
this(ivyFile, resolverName, artifacts, checksums, logging, false)
}
final class UpdateConfiguration(val retrieve: Option[RetrieveConfiguration], val missingOk: Boolean, val logging: UpdateLogging.Value)
final class RetrieveConfiguration(val retrieveDirectory: File, val outputPattern: String)
@ -86,11 +90,11 @@ object IvyActions
import configuration._
module.withModule(log) { case (ivy, md, default) =>
val resolver = ivy.getSettings.getResolver(resolverName)
if(resolver eq null) error("Undefined resolver '" + resolverName + "'")
if(resolver eq null) sys.error("Undefined resolver '" + resolverName + "'")
val ivyArtifact = ivyFile map { file => (MDArtifact.newIvyArtifact(md), file) }
val cross = crossVersionMap(module.moduleSettings)
val as = mapArtifacts(md, cross, artifacts) ++ ivyArtifact.toList
withChecksums(resolver, checksums) { publish(md, as, resolver, overwrite = true) }
val as = mapArtifacts(md, cross, artifacts) ++ ivyArtifact.toSeq
withChecksums(resolver, checksums) { publish(md, as, resolver, overwrite = overwrite) }
}
}
private[this] def withChecksums[T](resolver: DependencyResolver, checksums: Seq[String])(act: => T): T =

View File

@ -188,7 +188,7 @@ class MakePom(val log: Logger)
<dependency>
<groupId>{mrid.getOrganisation}</groupId>
<artifactId>{mrid.getName}</artifactId>
<version>{mrid.getRevision}</version>
<version>{makeDependencyVersion(mrid.getRevision)}</version>
{ scopeElem(scope) }
{ optionalElem(optional) }
{ classifierElem(classifier) }
@ -197,6 +197,44 @@ class MakePom(val log: Logger)
</dependency>
}
def makeDependencyVersion(revision: String): String = {
def plusRange(s:String, shift:Int = 0) = {
def pow(i:Int):Int = if (i>0) 10 * pow(i-1) else 1
val (prefixVersion, lastVersion) = (s+"0"*shift).reverse.split("\\.",2) match {
case Array(revLast,revRest) =>
( revRest.reverse + ".", revLast.reverse )
case Array(revLast) => ("", revLast.reverse)
}
val lastVersionInt = lastVersion.toInt
s"[${prefixVersion}${lastVersion},${prefixVersion}${lastVersionInt+pow(shift)})"
}
val startSym=Set(']','[','(')
val stopSym=Set(']','[',')')
try {
if (revision endsWith ".+") {
plusRange(revision.substring(0,revision.length-2))
} else if (revision endsWith "+") {
val base = revision.take(revision.length-1)
// This is a heuristic. Maven just doesn't support Ivy's notions of 1+, so
// we assume version ranges never go beyond 5 siginificant digits.
(0 to 5).map(plusRange(base,_)).mkString(",")
} else if (startSym(revision(0)) && stopSym(revision(revision.length-1))) {
val start = revision(0)
val stop = revision(revision.length-1)
val mid = revision.substring(1,revision.length-1)
(if (start == ']') "(" else start) + mid + (if (stop == '[') ")" else stop)
} else revision
} catch {
case e: NumberFormatException =>
// TODO - if the version doesn't meet our expectations, maybe we just issue a hard
// error instead of softly ignoring the attempt to rewrite.
//sys.error(s"Could not fix version [$revision] into maven style version")
revision
}
}
@deprecated("No longer used and will be removed.", "0.12.1")
def classifier(dependency: DependencyDescriptor, includeTypes: Set[String]): NodeSeq =
{

View File

@ -0,0 +1,128 @@
package sbt
package ivyint
import java.lang.reflect.Field
import java.lang.reflect.Method
import java.net.Authenticator
import java.net.PasswordAuthentication
import org.apache.ivy.util.Credentials
import org.apache.ivy.util.Message
import org.apache.ivy.util.url.IvyAuthenticator
import org.apache.ivy.util.url.CredentialsStore
/**
* Helper to install an Authenticator that works with the IvyAuthenticator to provide better error messages when
* credentials don't line up.
*/
object ErrorMessageAuthenticator {
private var securityWarningLogged = false
private def originalAuthenticator: Option[Authenticator] = {
try {
val f = classOf[Authenticator].getDeclaredField("theAuthenticator");
f.setAccessible(true);
Option(f.get(null).asInstanceOf[Authenticator])
} catch {
// TODO - Catch more specific errors.
case t: Throwable =>
Message.debug("Error occurred while getting the original authenticator: " + t.getMessage)
None
}
}
private lazy val ivyOriginalField = {
val field = classOf[IvyAuthenticator].getDeclaredField("original")
field.setAccessible(true)
field
}
// Attempts to get the original authenticator form the ivy class or returns null.
private def installIntoIvy(ivy: IvyAuthenticator): Option[Authenticator] = {
// Here we install ourselves as the IvyAuthenticator's default so we get called AFTER Ivy has a chance to run.
def installIntoIvyImpl(original: Option[Authenticator]): Unit = {
val newOriginal = new ErrorMessageAuthenticator(original)
ivyOriginalField.set(ivy, newOriginal)
}
try Option(ivyOriginalField.get(ivy).asInstanceOf[Authenticator]) match {
case Some(alreadyThere: ErrorMessageAuthenticator) => // We're already installed, no need to do the work again.
case originalOpt => installIntoIvyImpl(originalOpt)
} catch {
case t: Throwable =>
Message.debug("Error occurred will trying to install debug messages into Ivy Authentication" + t.getMessage)
}
Some(ivy)
}
/** Installs the error message authenticator so we have nicer error messages when using java's URL for downloading. */
def install() {
// Actually installs the error message authenticator.
def doInstall(original: Option[Authenticator]): Unit =
try Authenticator.setDefault(new ErrorMessageAuthenticator(original))
catch {
case e: SecurityException if !securityWarningLogged =>
securityWarningLogged = true;
Message.warn("Not enough permissions to set the ErorrMessageAuthenticator. "
+ "Helpful debug messages disabled!");
}
// We will try to use the original authenticator as backup authenticator.
// Since there is no getter available, so try to use some reflection to
// obtain it. If that doesn't work, assume there is no original authenticator
def doInstallIfIvy(original: Option[Authenticator]): Unit =
original match {
case Some(installed: ErrorMessageAuthenticator) => // Ignore, we're already installed
case Some(ivy: IvyAuthenticator) => installIntoIvy(ivy)
case original => doInstall(original)
}
doInstallIfIvy(originalAuthenticator)
}
}
/**
* An authenticator which just delegates to a previous authenticator and issues *nice*
* error messages on failure to find credentials.
*
* Since ivy installs its own credentials handler EVERY TIME it resolves or publishes, we want to
* install this one at some point and eventually ivy will capture it and use it.
*/
private[sbt] final class ErrorMessageAuthenticator(original: Option[Authenticator]) extends Authenticator {
protected override def getPasswordAuthentication(): PasswordAuthentication = {
// We're guaranteed to only get here if Ivy's authentication fails
if (!isProxyAuthentication) {
val host = getRequestingHost
// TODO - levenshtein distance "did you mean" message.
Message.error(s"Unable to find credentials for [${getRequestingPrompt} @ ${host}].")
val configuredRealms = IvyCredentialsLookup.realmsForHost.getOrElse(host, Set.empty)
if(!configuredRealms.isEmpty) {
Message.error(s" Is one of these realms mispelled for host [${host}]:")
configuredRealms foreach { realm =>
Message.error(s" * ${realm}")
}
}
}
// TODO - Maybe we should work on a helpful proxy message...
// TODO - To be more maven friendly, we may want to also try to grab the "first" authentication that shows up for a server and try it.
// or maybe allow that behavior to be configured, since maven users aren't used to realms (which they should be).
// Grabs the authentication that would have been provided had we not been installed...
def originalAuthentication: Option[PasswordAuthentication] = {
Authenticator.setDefault(original.getOrElse(null))
try Option(Authenticator.requestPasswordAuthentication(
getRequestingHost,
getRequestingSite,
getRequestingPort,
getRequestingProtocol,
getRequestingPrompt,
getRequestingScheme))
finally Authenticator.setDefault(this)
}
originalAuthentication.getOrElse(null)
}
/** Returns true if this authentication if for a proxy and not for an HTTP server.
* We want to display different error messages, depending.
*/
private def isProxyAuthentication: Boolean =
getRequestorType == Authenticator.RequestorType.PROXY
}

View File

@ -0,0 +1,63 @@
package sbt
package ivyint
import org.apache.ivy.util.url.CredentialsStore
import collection.JavaConverters._
/** A key used to store credentials in the ivy credentials store. */
private[sbt] sealed trait CredentialKey
/** Represents a key in the ivy credentials store that is only specific to a host. */
private[sbt] case class Host(name: String) extends CredentialKey
/** Represents a key in the ivy credentials store that is keyed to both a host and a "realm". */
private[sbt] case class Realm(host: String, realm: String) extends CredentialKey
/**
* Helper mechanism to improve credential related error messages.
*
* This evil class exposes to us the necessary information to warn on credential failure and offer
* spelling/typo suggestions.
*/
private[sbt] object IvyCredentialsLookup {
/** Helper extractor for Ivy's key-value store of credentials. */
private object KeySplit {
def unapply(key: String): Option[(String,String)] = {
key.indexOf('@') match {
case -1 => None
case n => Some(key.take(n) -> key.drop(n+1))
}
}
}
/** Here we cheat runtime private so we can look in the credentials store.
*
* TODO - Don't bomb at class load time...
*/
private val credKeyringField = {
val tmp = classOf[CredentialsStore].getDeclaredField("KEYRING")
tmp.setAccessible(true)
tmp
}
/** All the keys for credentials in the ivy configuration store. */
def keyringKeys: Set[CredentialKey] = {
val map = credKeyringField.get(null).asInstanceOf[java.util.HashMap[String, Any]]
// make a clone of the set...
(map.keySet.asScala.map {
case KeySplit(realm, host) => Realm(host, realm)
case host => Host(host)
})(collection.breakOut)
}
/**
* A mapping of host -> realms in the ivy credentials store.
*/
def realmsForHost: Map[String, Set[String]] =
keyringKeys collect {
case x: Realm => x
} groupBy { realm =>
realm.host
} mapValues { realms =>
realms map (_.realm)
}
}

View File

@ -0,0 +1,29 @@
package sbt
import java.io.File
import org.specs2._
import mutable.Specification
object MakePomTest extends Specification
{
val mp = new MakePom(ConsoleLogger())
import mp.{makeDependencyVersion=>v}
"MakePom makeDependencyVersion" should {
"Handle .+ in versions" in {
v("1.+") must_== "[1,2)"
v("1.2.3.4.+") must_== "[1.2.3.4,1.2.3.5)"
v("12.31.42.+") must_== "[12.31.42,12.31.43)"
}
/* TODO - do we care about this case?
* 1+ --> [1,2),[10,20),[100,200),[1000,2000),[10000,20000),[100000,200000)
*/
"Handle ]* bracket in version ranges" in {
v("]1,3]") must_== "(1,3]"
v("]1.1,1.3]") must_== "(1.1,1.3]"
}
"Handle *[ bracket in version ranges" in {
v("[1,3[") must_== "[1,3)"
v("[1.1,1.3[") must_== "[1.1,1.3)"
}
}
}