diff --git a/README.md b/README.md index 013b320d8..b398095e6 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,14 @@ sbt === sbt is a build tool for Scala, Java, and more. + For general documentation, see http://www.scala-sbt.org/. Issues and Pull Requests ------------------------ Please read [CONTRIBUTING] carefully before opening a GitHub Issue. + The short version: try [StackOverflow] and [sbt-dev]. Don't open an Issue. sbt 0.13 diff --git a/ivy/src/main/scala/sbt/CustomPomParser.scala b/ivy/src/main/scala/sbt/CustomPomParser.scala index 7b5664daf..d37804cbc 100644 --- a/ivy/src/main/scala/sbt/CustomPomParser.scala +++ b/ivy/src/main/scala/sbt/CustomPomParser.scala @@ -51,7 +51,15 @@ object CustomPomParser { 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) + private[this] def makeCoords(mrid: ModuleRevisionId): String = s"${mrid.getOrganisation}:${mrid.getName}:${mrid.getRevision}" + + // We now include the ModuleID in a hash, to ensure that parent-pom transformations don't corrupt child poms. + private[this] def MakeTransformHash(md: ModuleDescriptor): String = { + val coords: String = makeCoords(md.getModuleRevisionId) + + hash((unqualifiedKeys ++ JarPackagings ++ Set(coords)).toSeq.sorted) + } + private[this] def hash(ss: Seq[String]): String = Hash.toHex(Hash(ss.flatMap(_ getBytes "UTF-8").toArray)) // Unfortunately, ModuleDescriptorParserRegistry is add-only and is a singleton instance. @@ -65,11 +73,12 @@ object CustomPomParser { { val oldTransformedHashKey = "sbtTransformHash" val extraInfo = md.getExtraInfo + val MyHash = MakeTransformHash(md) // 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 + case Some(MyHash) => true + case _ => false }) } @@ -95,10 +104,10 @@ object CustomPomParser { val mergeDuplicates = IvySbt.hasDuplicateDependencies(md.getDependencies) val unqualify = toUnqualify(filtered) - if (unqualify.isEmpty && extraDepAttributes.isEmpty && !convertArtifacts && !mergeDuplicates) - md - else - addExtra(unqualify, extraDepAttributes, parser, md) + + // Here we always add extra attributes. There's a scenario where parent-pom information corrupts child-poms with "e:" namespaced xml elements + // and we have to force the every generated xml file to have the appropriate xml namespace + addExtra(unqualify, extraDepAttributes, parser, md) } // The element of the pom is used to store additional metadata, such as for sbt plugins or for the base URL for API docs. // This is done because the pom XSD does not appear to allow extra metadata anywhere else. @@ -185,7 +194,7 @@ object CustomPomParser { for (l <- md.getLicenses) dmd.addLicense(l) for ((key, value) <- md.getExtraInfo.asInstanceOf[java.util.Map[String, String]].asScala) dmd.addExtraInfo(key, value) - dmd.addExtraInfo(TransformedHashKey, TransformHash) // mark as transformed by this version, so we don't need to do it again + dmd.addExtraInfo(TransformedHashKey, MakeTransformHash(md)) // mark as transformed by this version, so we don't need to do it again for ((key, value) <- md.getExtraAttributesNamespaces.asInstanceOf[java.util.Map[String, String]].asScala) dmd.addExtraAttributeNamespace(key, value) IvySbt.addExtraNamespace(dmd) diff --git a/ivy/src/main/scala/sbt/JsonUtil.scala b/ivy/src/main/scala/sbt/JsonUtil.scala index 88d7e1e90..156c9aa44 100644 --- a/ivy/src/main/scala/sbt/JsonUtil.scala +++ b/ivy/src/main/scala/sbt/JsonUtil.scala @@ -25,8 +25,34 @@ private[sbt] object JsonUtil { } def toLite(ur: UpdateReport): UpdateReportLite = UpdateReportLite(ur.configurations map { cr => - ConfigurationReportLite(cr.configuration, cr.details) + ConfigurationReportLite(cr.configuration, cr.details map { oar => + new OrganizationArtifactReport(oar.organization, oar.name, oar.modules map { mr => + new ModuleReport( + mr.module, mr.artifacts, mr.missingArtifacts, mr.status, + mr.publicationDate, mr.resolver, mr.artifactResolver, + mr.evicted, mr.evictedData, mr.evictedReason, + mr.problem, mr.homepage, mr.extraAttributes, + mr.isDefault, mr.branch, mr.configurations, mr.licenses, + summarizeCallers(mr.callers)) + }) + }) }) + // #1763/#2030. Caller takes up 97% of space, so we need to shrink it down, + // but there are semantics associated with some of them. + def summarizeCallers(callers: Seq[Caller]): Seq[Caller] = + if (callers.isEmpty) callers + else { + // Use the first element to represent all callers + val head = callers.head + val caller = new Caller( + head.caller, head.callerConfigurations, head.callerExtraAttributes, + callers exists { _.isForceDependency }, + callers exists { _.isChangingDependency }, + callers exists { _.isTransitiveDependency }, + callers exists { _.isDirectlyForceDependency }) + Seq(caller) + } + def fromLite(lite: UpdateReportLite, cachedDescriptor: File): UpdateReport = { val stats = new UpdateStats(0L, 0L, 0L, false) diff --git a/ivy/src/main/scala/sbt/MakePom.scala b/ivy/src/main/scala/sbt/MakePom.scala index d0e1a2741..1dfa5d1fe 100644 --- a/ivy/src/main/scala/sbt/MakePom.scala +++ b/ivy/src/main/scala/sbt/MakePom.scala @@ -104,7 +104,7 @@ class MakePom(val log: Logger) { { val deps = depsInConfs(module, configurations) makeProperties(module, deps) ++ - makeDependencies(deps, includeTypes) + makeDependencies(deps, includeTypes, module.getAllExcludeRules) } { makeRepositories(ivy.getSettings, allRepositories, filterRepositories) } ) @@ -220,43 +220,65 @@ class MakePom(val log: Logger) { } val IgnoreTypes: Set[String] = Set(Artifact.SourceType, Artifact.DocType, Artifact.PomType) + @deprecated("Use `makeDependencies` variant which takes excludes", "0.13.9") def makeDependencies(dependencies: Seq[DependencyDescriptor], includeTypes: Set[String]): NodeSeq = + makeDependencies(dependencies, includeTypes, Nil) + + def makeDependencies(dependencies: Seq[DependencyDescriptor], includeTypes: Set[String], excludes: Seq[ExcludeRule]): NodeSeq = if (dependencies.isEmpty) NodeSeq.Empty else - { dependencies.map(makeDependency(_, includeTypes)) } + { dependencies.map(makeDependency(_, includeTypes, excludes)) } + @deprecated("Use `makeDependency` variant which takes excludes", "0.13.9") def makeDependency(dependency: DependencyDescriptor, includeTypes: Set[String]): NodeSeq = + makeDependency(dependency, includeTypes, Nil) + + def makeDependency(dependency: DependencyDescriptor, includeTypes: Set[String], excludes: Seq[ExcludeRule]): NodeSeq = { val artifacts = dependency.getAllDependencyArtifacts val includeArtifacts = artifacts.filter(d => includeTypes(d.getType)) if (artifacts.isEmpty) { - val (scope, optional) = getScopeAndOptional(dependency.getModuleConfigurations) - makeDependencyElem(dependency, scope, optional, None, None) + val configs = dependency.getModuleConfigurations + if (configs.filterNot(Set("sources", "docs")).nonEmpty) { + val (scope, optional) = getScopeAndOptional(dependency.getModuleConfigurations) + makeDependencyElem(dependency, scope, optional, None, None, excludes) + } else NodeSeq.Empty } else if (includeArtifacts.isEmpty) NodeSeq.Empty else - NodeSeq.fromSeq(artifacts.map(a => makeDependencyElem(dependency, a))) + NodeSeq.fromSeq(artifacts.flatMap(a => makeDependencyElem(dependency, a, excludes))) } - def makeDependencyElem(dependency: DependencyDescriptor, artifact: DependencyArtifactDescriptor): Elem = + @deprecated("Use `makeDependencyElem` variant which takes excludes", "0.13.9") + def makeDependencyElem(dependency: DependencyDescriptor, artifact: DependencyArtifactDescriptor): Option[Elem] = + makeDependencyElem(dependency, artifact, Nil) + + def makeDependencyElem(dependency: DependencyDescriptor, artifact: DependencyArtifactDescriptor, excludes: Seq[ExcludeRule]): Option[Elem] = { val configs = artifact.getConfigurations.toList match { case Nil | "*" :: Nil => dependency.getModuleConfigurations case x => x.toArray } - val (scope, optional) = getScopeAndOptional(configs) - val classifier = artifactClassifier(artifact) - val baseType = artifactType(artifact) - val tpe = (classifier, baseType) match { - case (Some(c), Some(tpe)) if Artifact.classifierType(c) == tpe => None - case _ => baseType - } - makeDependencyElem(dependency, scope, optional, classifier, tpe) + if (configs.filterNot(Set("sources", "docs")).nonEmpty) { + val (scope, optional) = getScopeAndOptional(configs) + val classifier = artifactClassifier(artifact) + val baseType = artifactType(artifact) + val tpe = (classifier, baseType) match { + case (Some(c), Some(tpe)) if Artifact.classifierType(c) == tpe => None + case _ => baseType + } + Some(makeDependencyElem(dependency, scope, optional, classifier, tpe, excludes)) + } else None } + + @deprecated("Use `makeDependencyElem` variant which takes excludes", "0.13.9") def makeDependencyElem(dependency: DependencyDescriptor, scope: Option[String], optional: Boolean, classifier: Option[String], tpe: Option[String]): Elem = + makeDependencyElem(dependency, scope, optional, classifier, tpe, Nil) + + def makeDependencyElem(dependency: DependencyDescriptor, scope: Option[String], optional: Boolean, classifier: Option[String], tpe: Option[String], excludes: Seq[ExcludeRule]): Elem = { val mrid = dependency.getDependencyRevisionId @@ -267,7 +289,7 @@ class MakePom(val log: Logger) { { optionalElem(optional) } { classifierElem(classifier) } { typeElem(tpe) } - { exclusions(dependency) } + { exclusions(dependency, excludes) } } @@ -317,9 +339,12 @@ class MakePom(val log: Logger) { (scope, opt.nonEmpty) } - def exclusions(dependency: DependencyDescriptor): NodeSeq = + @deprecated("Use `exclusions` variant which takes excludes", "0.13.9") + def exclusions(dependency: DependencyDescriptor): NodeSeq = exclusions(dependency, Nil) + + def exclusions(dependency: DependencyDescriptor, excludes: Seq[ExcludeRule]): NodeSeq = { - val excl = dependency.getExcludeRules(dependency.getModuleConfigurations) + val excl = dependency.getExcludeRules(dependency.getModuleConfigurations) ++ excludes val (warns, excls) = IvyUtil.separate(excl.map(makeExclusion)) if (warns.nonEmpty) log.warn(warns.mkString(IO.Newline)) if (excls.nonEmpty) { excls } diff --git a/ivy/src/main/scala/sbt/ivyint/CachedResolutionResolveEngine.scala b/ivy/src/main/scala/sbt/ivyint/CachedResolutionResolveEngine.scala index 14d7af22a..9f25d2172 100644 --- a/ivy/src/main/scala/sbt/ivyint/CachedResolutionResolveEngine.scala +++ b/ivy/src/main/scala/sbt/ivyint/CachedResolutionResolveEngine.scala @@ -4,6 +4,7 @@ package ivyint import java.util.Date import java.net.URL import java.io.File +import java.text.SimpleDateFormat import collection.concurrent import collection.mutable import collection.immutable.ListMap @@ -20,12 +21,19 @@ import org.apache.ivy.plugins.latest.{ ArtifactInfo => IvyArtifactInfo } import org.apache.ivy.plugins.matcher.{ MapMatcher, PatternMatcher } import Configurations.{ System => _, _ } import annotation.tailrec +import scala.concurrent.duration._ private[sbt] object CachedResolutionResolveCache { def createID(organization: String, name: String, revision: String) = ModuleRevisionId.newInstance(organization, name, revision) def sbtOrgTemp = "org.scala-sbt.temp" - def graphVersion = "0.13.8" + def graphVersion = "0.13.9" + val buildStartup: Long = System.currentTimeMillis + lazy val todayStr: String = toYyyymmdd(buildStartup) + lazy val tomorrowStr: String = toYyyymmdd(buildStartup + (1 day).toMillis) + lazy val yesterdayStr: String = toYyyymmdd(buildStartup - (1 day).toMillis) + def toYyyymmdd(timeSinceEpoch: Long): String = yyyymmdd.format(new Date(timeSinceEpoch)) + lazy val yyyymmdd: SimpleDateFormat = new SimpleDateFormat("yyyy-MM-dd") } private[sbt] class CachedResolutionResolveCache() { @@ -137,7 +145,17 @@ private[sbt] class CachedResolutionResolveCache() { val staticGraphDirectory = miniGraphPath / "static" val dynamicGraphDirectory = miniGraphPath / "dynamic" val staticGraphPath = staticGraphDirectory / pathOrg / pathName / pathRevision / "graphs" / "graph.json" - val dynamicGraphPath = dynamicGraphDirectory / logicalClock.toString / pathOrg / pathName / pathRevision / "graphs" / "graph.json" + val dynamicGraphPath = dynamicGraphDirectory / todayStr / logicalClock.toString / pathOrg / pathName / pathRevision / "graphs" / "graph.json" + def cleanDynamicGraph(): Unit = + { + val list = IO.listFiles(dynamicGraphDirectory, DirectoryFilter).toList + list filterNot { d => + (d.getName == todayStr) || (d.getName == tomorrowStr) || (d.getName == yesterdayStr) + } foreach { d => + log.debug(s"deleting old graphs $d...") + IO.delete(d) + } + } def loadMiniGraphFromFile: Option[Either[ResolveException, UpdateReport]] = (if (staticGraphPath.exists) Some(staticGraphPath) else if (dynamicGraphPath.exists) Some(dynamicGraphPath) @@ -175,6 +193,9 @@ private[sbt] class CachedResolutionResolveCache() { val gp = if (changing) dynamicGraphPath else staticGraphPath log.debug(s"saving minigraph to $gp") + if (changing) { + cleanDynamicGraph() + } JsonUtil.writeUpdateReport(ur, gp) // limit the update cache size if (updateReportCache.size > maxUpdateReportCacheSize) { diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 58b0df1c9..cd05b9c77 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -3,7 +3,7 @@ */ package sbt -import scala.concurrent.duration.Duration +import scala.concurrent.duration.{ FiniteDuration, Duration } import Attributed.data import Scope.{ fillTaskAxis, GlobalScope, ThisScope } import sbt.Compiler.InputsWithPrevious @@ -27,7 +27,7 @@ import org.apache.ivy.core.module.{ descriptor, id } import descriptor.ModuleDescriptor, id.ModuleRevisionId import java.io.{ File, PrintWriter } import java.net.{ URI, URL, MalformedURLException } -import java.util.concurrent.Callable +import java.util.concurrent.{ TimeUnit, Callable } import sbinary.DefaultProtocol.StringFormat import Cache.seqFormat import CommandStrings.ExportStream @@ -115,7 +115,8 @@ object Defaults extends BuildCommon { pomPostProcess :== idFun, pomAllRepositories :== false, pomIncludeRepository :== Classpaths.defaultRepositoryFilter, - updateOptions := UpdateOptions() + updateOptions := UpdateOptions(), + forceUpdatePeriod :== None ) /** Core non-plugin settings for sbt builds. These *must* be on every build or the sbt engine will fail to run at all. */ @@ -570,11 +571,20 @@ object Defaults extends BuildCommon { def selectedFilter(args: Seq[String]): Seq[String => Boolean] = { - val filters = args map GlobFilter.apply - if (filters.isEmpty) + def matches(nfs: Seq[NameFilter], s: String) = nfs.exists(_.accept(s)) + + val (excludeArgs, includeArgs) = args.partition(_.startsWith("-")) + + val includeFilters = includeArgs map GlobFilter.apply + val excludeFilters = excludeArgs.map(_.substring(1)).map(GlobFilter.apply) + + if (includeFilters.isEmpty && excludeArgs.isEmpty) { Seq(const(true)) - else - filters.map { f => (s: String) => f accept s } + } else if (includeFilters.isEmpty) { + Seq({ (s: String) => !matches(excludeFilters, s) }) + } else { + includeFilters.map { f => (s: String) => (f.accept(s) && !matches(excludeFilters, s)) } + } } def detectTests: Initialize[Task[Seq[TestDefinition]]] = (loadedTestFrameworks, compile, streams) map { (frameworkMap, analysis, s) => Tests.discover(frameworkMap.values.toList, analysis, s.log)._1 @@ -896,7 +906,7 @@ object Defaults extends BuildCommon { selectTests ~ options } - def distinctParser(exs: Set[String], raw: Boolean): Parser[Seq[String]] = + private def distinctParser(exs: Set[String], raw: Boolean): Parser[Seq[String]] = { import DefaultParsers._ val base = token(Space) ~> token(NotSpace - "--" examples exs) @@ -1315,7 +1325,15 @@ object Classpaths { def updateTask: Initialize[Task[UpdateReport]] = Def.task { val depsUpdated = transitiveUpdate.value.exists(!_.stats.cached) val isRoot = executionRoots.value contains resolvedScoped.value + val forceUpdate = forceUpdatePeriod.value val s = streams.value + val fullUpdateOutput = s.cacheDirectory / "out" + val forceUpdateByTime = forceUpdate match { + case None => false + case Some(period) => + val elapsedDuration = new FiniteDuration(System.currentTimeMillis() - fullUpdateOutput.lastModified(), TimeUnit.MILLISECONDS) + fullUpdateOutput.exists() && elapsedDuration > period + } val scalaProvider = appConfiguration.value.provider.scalaProvider // Only substitute unmanaged jars for managed jars when the major.minor parts of the versions the same for: @@ -1351,7 +1369,7 @@ object Classpaths { if (executionRoots.value exists { _.key == evicted.key }) EvictionWarningOptions.empty else (evictionWarningOptions in update).value cachedUpdate(s.cacheDirectory / updateCacheName.value, show, ivyModule.value, uc, transform, - skip = (skip in update).value, force = isRoot, depsUpdated = depsUpdated, + skip = (skip in update).value, force = isRoot || forceUpdateByTime, depsUpdated = depsUpdated, uwConfig = uwConfig, logicalClock = logicalClock, depDir = Some(depDir), ewo = ewo, log = s.log) } diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 59faafdae..939dd9b77 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -5,7 +5,7 @@ package sbt import java.io.File import java.net.URL -import scala.concurrent.duration.Duration +import scala.concurrent.duration.{ FiniteDuration, Duration } import Def.ScopedKey import complete._ import inc.Analysis @@ -324,6 +324,7 @@ object Keys { val publishArtifact = SettingKey[Boolean]("publish-artifact", "Enables (true) or disables (false) publishing an artifact.", AMinusSetting) val packagedArtifact = TaskKey[(Artifact, File)]("packaged-artifact", "Generates a packaged artifact, returning the Artifact and the produced File.", CTask) val checksums = SettingKey[Seq[String]]("checksums", "The list of checksums to generate and to verify for dependencies.", BSetting) + val forceUpdatePeriod = SettingKey[Option[FiniteDuration]]("force-update-period", "Duration after which to force a full update to occur", CSetting) val classifiersModule = TaskKey[GetClassifiersModule]("classifiers-module", rank = CTask) val conflictWarning = SettingKey[ConflictWarning]("conflict-warning", "Configures warnings for conflicts in dependency management.", CSetting) diff --git a/main/src/test/scala/DefaultsTest.scala b/main/src/test/scala/DefaultsTest.scala new file mode 100644 index 000000000..c09a806c0 --- /dev/null +++ b/main/src/test/scala/DefaultsTest.scala @@ -0,0 +1,53 @@ +package sbt + +import java.io._ + +import org.specs2.mutable.Specification + +object DefaultsTest extends Specification { + private def assertFiltered(filter: List[String], expected: Map[String, Boolean]) = { + val actual = expected.map(t => (t._1, Defaults.selectedFilter(filter).exists(fn => fn(t._1)))) + + actual must be equalTo (expected) + } + + "`selectedFilter`" should { + "return all tests for an empty list" in { + assertFiltered(List(), Map("Test1" -> true, "Test2" -> true)) + } + + "work correctly with exact matches" in { + assertFiltered(List("Test1", "foo"), Map("Test1" -> true, "Test2" -> false, "Foo" -> false)) + } + + "work correctly with glob" in { + assertFiltered(List("Test*"), Map("Test1" -> true, "Test2" -> true, "Foo" -> false)) + } + + "work correctly with excludes" in { + assertFiltered(List("Test*", "-Test2"), Map("Test1" -> true, "Test2" -> false, "Foo" -> false)) + } + + "work correctly without includes" in { + assertFiltered(List("-Test2"), Map("Test1" -> true, "Test2" -> false, "Foo" -> true)) + } + + "work correctly with excluded globs" in { + assertFiltered(List("Test*", "-F*"), Map("Test1" -> true, "Test2" -> true, "Foo" -> false)) + } + + "cope with multiple filters" in { + assertFiltered(List("T*1", "T*2", "-F*"), Map("Test1" -> true, "Test2" -> true, "Foo" -> false)) + } + + "cope with multiple exclusion filters, no includes" in { + assertFiltered(List("-A*", "-F*"), Map("Test1" -> true, "Test2" -> true, "AAA" -> false, "Foo" -> false)) + } + + "cope with multiple exclusion filters with includes" in { + assertFiltered(List("T*", "-T*1", "-T*2"), Map("Test1" -> false, "Test2" -> false, "Test3" -> true)) + } + } + +} + diff --git a/notes/0.13.9/cached-resolution-fix.markdown b/notes/0.13.9/cached-resolution-fix.markdown new file mode 100644 index 000000000..36b442b62 --- /dev/null +++ b/notes/0.13.9/cached-resolution-fix.markdown @@ -0,0 +1,31 @@ + [@cunei]: https://github.com/cunei + [@eed3si9n]: https://github.com/eed3si9n + [@gkossakowski]: https://github.com/gkossakowski + [@jsuereth]: https://github.com/jsuereth + + [1721]: https://github.com/sbt/sbt/issues/1721 + [2014]: https://github.com/sbt/sbt/issues/2014 + [2030]: https://github.com/sbt/sbt/pull/2030 + +### Fixes with compatibility implications + +### Improvements + +### Bug fixes + +- Fixes memory/performance issue with cached resolution. See below. + +### Cached resolution fixes + +On a larger dependency graph, the JSON file growing to be 100MB+ +with 97% of taken up by *caller* information. +The caller information is not useful once the graph is successfully resolved. +To make the matter worse, these large JSON files were never cleaned up. + +sbt 0.13.9 creates a single caller to represent all callers, +which fixes `OutOfMemoryException` seen on some builds. +This generally shrinks the size of JSON, so it should make the IO operations faster. +Dynamic graphs will be rotated with directories named after `yyyy-mm-dd`, +and stale JSON files will be cleaned up after few days. + +[#2030][2030]/[#1721][1721]/[#2014][2014] by [@eed3si9n][@eed3si9n] diff --git a/notes/0.13.9/forceupdateperiod.markdown b/notes/0.13.9/forceupdateperiod.markdown new file mode 100644 index 000000000..49e171b5d --- /dev/null +++ b/notes/0.13.9/forceupdateperiod.markdown @@ -0,0 +1,9 @@ + [@ajsquared]: https://github.com/ajsquared + + +### Changes with compatibility implications + +### Improvements +- Adds `forceUpdatePeriod` key, that takes values of `Option[FiniteDuration]`. If set, a full `update` will occur after that amount of time without needing to explicitly run the `update` task. By [@ajsquared][@ajsquared] + +### Fixes diff --git a/notes/0.13.9/fork-error-get-message.markdown b/notes/0.13.9/fork-error-get-message.markdown new file mode 100644 index 000000000..3f5176057 --- /dev/null +++ b/notes/0.13.9/fork-error-get-message.markdown @@ -0,0 +1,10 @@ + [@kamilkloch]: https://github.com/kamilkloch + [2028]: https://github.com/sbt/sbt/issues/2028 + +### Changes with compatibility implications + +### Improvements + +- Update ForkError.getMessage() to include exception's original name. [#2028][2028] by [@kamilkloch][@kamilkloch] + +### Fixes diff --git a/notes/0.13.9/history-infinite-loop.markdown b/notes/0.13.9/history-infinite-loop.markdown new file mode 100644 index 000000000..c2ae3eb51 --- /dev/null +++ b/notes/0.13.9/history-infinite-loop.markdown @@ -0,0 +1,3 @@ +### Bug fixes + +- Prevent history command(s) from going into an infinite loop [#1562][1562] by [@PanAeon][@PanAeon] \ No newline at end of file diff --git a/notes/0.13.9/pom-corruption-fix.md b/notes/0.13.9/pom-corruption-fix.md new file mode 100644 index 000000000..ab365e92d --- /dev/null +++ b/notes/0.13.9/pom-corruption-fix.md @@ -0,0 +1,14 @@ + [@jsuereth]: https://github.com/jsuereth + + [1856]: https://github.com/sbt/sbt/issues/1856 + +### Fixes with compatibility implications + +### Improvements + +### Bug fixes + +- Fixes a certain class of pom corruption that can occur in the presence of parent-poms. + + +[#1856][1856] by [@jsuereth][@jsuereth] diff --git a/notes/0.13.9/project-level-exclusions-in-the-pom.markdown b/notes/0.13.9/project-level-exclusions-in-the-pom.markdown new file mode 100644 index 000000000..f534d989e --- /dev/null +++ b/notes/0.13.9/project-level-exclusions-in-the-pom.markdown @@ -0,0 +1,12 @@ + + [@dwijnand]: http://github.com/dwijnand + [#1877]: https://github.com/sbt/sbt/issues/1877 + [#2035]: https://github.com/sbt/sbt/pull/2035 + +### Fixes with compatibility implications + +### Improvements + +### Bug fixes + +- Add dependency-level exclusions in the POM for project-level exclusions. [#1877][]/[#2035][] by [@dwijnand][] diff --git a/notes/0.13.9/source-pom.markdown b/notes/0.13.9/source-pom.markdown new file mode 100644 index 000000000..fa906e711 --- /dev/null +++ b/notes/0.13.9/source-pom.markdown @@ -0,0 +1,21 @@ + + [@cunei]: http://github.com/cunei + [2001]: https://github.com/sbt/sbt/issues/2001 + [2027]: https://github.com/sbt/sbt/pull/2027 + +### Fixes with compatibility implications + +- Starting with 0.13.9, the generated POM files no longer include dependencies on source or javadoc jars + obtained via withSources() or withJavadoc() + +### Improvements + +### Bug fixes + +### POM files no longer include certain source and javadoc jars + +When declaring library dependencies using the withSources() or withJavadoc() options, sbt was also including +in the pom file, as dependencies, the source or javadoc jars using the default Maven scope. Such dependencies +might be erroneously processed as they were regular jars by automated tools + +[#2001][2001]/[#2027][2027] by [@cunei][@cunei] diff --git a/notes/0.13.9/test-exclude-filter.markdown b/notes/0.13.9/test-exclude-filter.markdown new file mode 100644 index 000000000..d22b0efee --- /dev/null +++ b/notes/0.13.9/test-exclude-filter.markdown @@ -0,0 +1,12 @@ + + [@matthewfarwell]: http://github.com/matthewfarwell + [1970]: https://github.com/sbt/sbt/pull/1970 + +### Fixes with compatibility implications + +### Improvements + +- Support excluding tests in `testOnly`/`testQuick` with `-`, for example `-MySpec`. + [#1970][1970] by [@matthewfarwell][@matthewfarwell] + +### Bug fixes diff --git a/sbt/src/sbt-test/dependency-management/exclude-dependencies/build.sbt b/sbt/src/sbt-test/dependency-management/exclude-dependencies/build.sbt index 16bc85772..5390886f3 100644 --- a/sbt/src/sbt-test/dependency-management/exclude-dependencies/build.sbt +++ b/sbt/src/sbt-test/dependency-management/exclude-dependencies/build.sbt @@ -1,3 +1,6 @@ +import scala.xml.{ Node, _ } +import scala.xml.Utility.trim + lazy val check = taskKey[Unit]("check") val dispatch = "net.databinder.dispatch" %% "dispatch-core" % "0.11.2" @@ -31,5 +34,30 @@ lazy val root = (project in file(".")). if (bcp exists { _.data.getName contains "dispatch-core_2.11-0.11.1.jar" }) { sys.error("dispatch-core_2.11-0.11.1.jar found when it should NOT be included: " + bcp.toString) } + + val bPomXml = makePomXml(streams.value.log, (makePomConfiguration in b).value, (ivyModule in b).value) + + val repatchTwitterXml = bPomXml \ "dependencies" \ "dependency" find { d => + (d \ "groupId").text == "com.eed3si9n" && (d \ "artifactId").text == "repatch-twitter-core_2.11" + } getOrElse (sys error s"Missing repatch-twitter-core dependency: $bPomXml") + + val excludeDispatchCoreXml = + + net.databinder.dispatch + dispatch-core_2.11 + + + if (trim((repatchTwitterXml \ "exclusions" \ "exclusion").head) != trim(excludeDispatchCoreXml)) + sys error s"Missing dispatch-core exclusion: $repatchTwitterXml" + + () } - ) \ No newline at end of file + ) + +def makePomXml(log: Logger, makePomConfig: MakePomConfiguration, ivyModule: IvySbt#Module): Node = { + ivyModule.withModule[Node](log) { (ivy, md, default) => + import makePomConfig._ + new MakePom(log).toPom( + ivy, md, moduleInfo, configurations, includeTypes, extra, filterRepositories, allRepositories) + } +} diff --git a/sbt/src/sbt-test/dependency-management/force-update-period/build.sbt b/sbt/src/sbt-test/dependency-management/force-update-period/build.sbt new file mode 100644 index 000000000..ca910bdd9 --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/force-update-period/build.sbt @@ -0,0 +1,13 @@ +libraryDependencies += "log4j" % "log4j" % "1.2.16" % "compile" + +autoScalaLibrary := false + +TaskKey[Unit]("check-last-update-time") <<= streams map { (s) => + val fullUpdateOutput = s.cacheDirectory / "out" + val timeDiff = System.currentTimeMillis()-fullUpdateOutput.lastModified() + val exists = fullUpdateOutput.exists() + s.log.info(s"Amount of time since last full update: $timeDiff") + if (exists && timeDiff > 5000) { + sys.error("Full update not perfomed") + } +} \ No newline at end of file diff --git a/sbt/src/sbt-test/dependency-management/force-update-period/test b/sbt/src/sbt-test/dependency-management/force-update-period/test new file mode 100644 index 000000000..39e776d09 --- /dev/null +++ b/sbt/src/sbt-test/dependency-management/force-update-period/test @@ -0,0 +1,11 @@ +$ absent target/resolution-cache +> compile +$ exists target/resolution-cache +> checkLastUpdateTime +$ sleep 10000 +> compile +# This is expected to fail +-> checkLastUpdateTime +> set forceUpdatePeriod := Some(new scala.concurrent.duration.FiniteDuration(5000, java.util.concurrent.TimeUnit.MILLISECONDS)) +> compile +> checkLastUpdateTime \ No newline at end of file diff --git a/sbt/src/sbt-test/dependency-management/pom-parent-pom/build.sbt b/sbt/src/sbt-test/dependency-management/pom-parent-pom/build.sbt index 53d830e83..82e17b0ab 100644 --- a/sbt/src/sbt-test/dependency-management/pom-parent-pom/build.sbt +++ b/sbt/src/sbt-test/dependency-management/pom-parent-pom/build.sbt @@ -10,6 +10,8 @@ resolvers += libraryDependencies += "com.example" % "example-child" % "1.0-SNAPSHOT" +libraryDependencies += "org.apache.geronimo.specs" % "geronimo-jta_1.1_spec" % "1.1.1" + version := "1.0-SNAPSHOT" @@ -20,3 +22,18 @@ cleanExampleCache := { IO.delete(cacheDir / "com.example") } } + +val checkIvyXml = taskKey[Unit]("Checks the ivy.xml transform was correct") + +checkIvyXml := { + ivySbt.value.withIvy(streams.value.log) { ivy => + val cacheDir = ivy.getSettings.getDefaultRepositoryCacheBasedir + // TODO - Is this actually ok? + val xmlFile = + cacheDir / "org.apache.geronimo.specs" / "geronimo-jta_1.1_spec" / "ivy-1.1.1.xml" + //cacheDir / "com.example" / "example-child" / "ivy-1.0-SNAPSHOT.xml" + val lines = IO.read(xmlFile) + if(lines.isEmpty) sys.error(s"Unable to read $xmlFile, could not resolve geronimo...") + assert(lines contains "xmlns:e", s"Failed to appropriately modify ivy.xml file for sbt extra attributes!\n$lines") + } +} diff --git a/sbt/src/sbt-test/dependency-management/pom-parent-pom/test b/sbt/src/sbt-test/dependency-management/pom-parent-pom/test index c0f560f2f..68a7fec1c 100644 --- a/sbt/src/sbt-test/dependency-management/pom-parent-pom/test +++ b/sbt/src/sbt-test/dependency-management/pom-parent-pom/test @@ -1,2 +1,3 @@ > cleanExampleCache -> update \ No newline at end of file +> update +> checkIvyXml diff --git a/sbt/src/sbt-test/source-dependencies/macro-arg-dep-2-11/project/build.scala b/sbt/src/sbt-test/source-dependencies/macro-arg-dep-2-11/project/build.scala index 89cd91ef8..f823d8295 100644 --- a/sbt/src/sbt-test/source-dependencies/macro-arg-dep-2-11/project/build.scala +++ b/sbt/src/sbt-test/source-dependencies/macro-arg-dep-2-11/project/build.scala @@ -5,7 +5,7 @@ object build extends Build { val defaultSettings = Seq( libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-reflect" % _ ), incOptions := incOptions.value.withNameHashing(true), - scalaVersion := "2.11.0-RC3" + scalaVersion := "2.11.6" ) lazy val root = Project( diff --git a/sbt/src/sbt-test/source-dependencies/macro-arg-dep-nested-2-11/project/build.scala b/sbt/src/sbt-test/source-dependencies/macro-arg-dep-nested-2-11/project/build.scala index 89cd91ef8..f823d8295 100644 --- a/sbt/src/sbt-test/source-dependencies/macro-arg-dep-nested-2-11/project/build.scala +++ b/sbt/src/sbt-test/source-dependencies/macro-arg-dep-nested-2-11/project/build.scala @@ -5,7 +5,7 @@ object build extends Build { val defaultSettings = Seq( libraryDependencies <+= scalaVersion("org.scala-lang" % "scala-reflect" % _ ), incOptions := incOptions.value.withNameHashing(true), - scalaVersion := "2.11.0-RC3" + scalaVersion := "2.11.6" ) lazy val root = Project( diff --git a/sbt/src/sbt-test/tests/test-exclude/project/Build.scala b/sbt/src/sbt-test/tests/test-exclude/project/Build.scala new file mode 100644 index 000000000..7f2f30f93 --- /dev/null +++ b/sbt/src/sbt-test/tests/test-exclude/project/Build.scala @@ -0,0 +1,10 @@ +import sbt._ +import Keys._ +import Defaults._ + +object B extends Build { + lazy val root = Project("root", file("."), settings = defaultSettings ++ Seq( + libraryDependencies += "org.scalatest" %% "scalatest" % "1.9.1" % "test", + parallelExecution in test := false + )) +} diff --git a/sbt/src/sbt-test/tests/test-exclude/src/test/scala/Test.scala b/sbt/src/sbt-test/tests/test-exclude/src/test/scala/Test.scala new file mode 100644 index 000000000..c7b0235e2 --- /dev/null +++ b/sbt/src/sbt-test/tests/test-exclude/src/test/scala/Test.scala @@ -0,0 +1,15 @@ +import java.io.File +import org.scalatest.FlatSpec +import org.scalatest.matchers.ShouldMatchers + +class Test1 extends FlatSpec with ShouldMatchers { + "a test" should "pass" in { + new File("target/Test1.run").createNewFile() + } +} + +class Test2 extends FlatSpec with ShouldMatchers { + "a test" should "pass" in { + new File("target/Test2.run").createNewFile() + } +} diff --git a/sbt/src/sbt-test/tests/test-exclude/test b/sbt/src/sbt-test/tests/test-exclude/test new file mode 100644 index 000000000..913c97312 --- /dev/null +++ b/sbt/src/sbt-test/tests/test-exclude/test @@ -0,0 +1,61 @@ +# Test1 & Test2 create files Test1.run & Test2.run respectively + +# no parameters +> test-only +$ exists target/Test1.run +$ exists target/Test2.run + +$ delete target/Test1.run +$ delete target/Test2.run + + +# with explicit match +> test-only Test1* +$ exists target/Test1.run +-$ exists target/Test2.run + +$ delete target/Test1.run + + +# with explicit match and exclusion +> test-only Test* -Test1 +-$ exists target/Test1.run +$ exists target/Test2.run + +$ delete target/Test2.run + + +# with explicit match and exclusion +> test-only Test* -Test2 +$ exists target/Test1.run +-$ exists target/Test2.run + +$ delete target/Test1.run + + +# with only exclusion +> test-only -Test2 +$ exists target/Test1.run +-$ exists target/Test2.run + +$ delete target/Test1.run + + +# with only exclusion +> test-only -Test1 +-$ exists target/Test1.run +$ exists target/Test2.run + +$ delete target/Test2.run + + +# with only glob exclusion +> test-only -Test* +-$ exists target/Test1.run +-$ exists target/Test2.run + + +# with only glob exclusion +> test-only -T*1 -T*2 +-$ exists target/Test1.run +-$ exists target/Test2.run diff --git a/testing/agent/src/main/java/sbt/ForkMain.java b/testing/agent/src/main/java/sbt/ForkMain.java index 15f53ce25..142bc6aa1 100755 --- a/testing/agent/src/main/java/sbt/ForkMain.java +++ b/testing/agent/src/main/java/sbt/ForkMain.java @@ -95,13 +95,15 @@ public class ForkMain { static class ForkError extends Exception { private String originalMessage; + private String originalName; private ForkError cause; ForkError(Throwable t) { originalMessage = t.getMessage(); + originalName = t.getClass().getName(); setStackTrace(t.getStackTrace()); if (t.getCause() != null) cause = new ForkError(t.getCause()); } - public String getMessage() { return originalMessage; } + public String getMessage() { return originalName + ": " + originalMessage; } public Exception getCause() { return cause; } } diff --git a/util/complete/src/main/scala/sbt/complete/HistoryCommands.scala b/util/complete/src/main/scala/sbt/complete/HistoryCommands.scala index 762f48c6d..1e124c583 100644 --- a/util/complete/src/main/scala/sbt/complete/HistoryCommands.scala +++ b/util/complete/src/main/scala/sbt/complete/HistoryCommands.scala @@ -61,7 +61,7 @@ object HistoryCommands { def execute(f: History => Option[String]): History => Option[List[String]] = (h: History) => { - val command = f(h) + val command = f(h).filterNot(_.startsWith(Start)) val lines = h.lines.toArray command.foreach(lines(lines.length - 1) = _) h.path foreach { h => IO.writeLines(h, lines) }