From a1b3117a42134fd050813d383d48de3ed218cd86 Mon Sep 17 00:00:00 2001 From: Josh Suereth Date: Tue, 12 Aug 2014 12:35:37 -0400 Subject: [PATCH] Track generated .class files and clean leftovers after project load. Fixes #1524 * Track generated .class files from Eval * While loading, join all classfiles throughout Load, lots of bookkeeping. * When a given build URI is done loading, we can look at its project/target/config-classes directory and clean out any extra items that are lingering from previous build definitions. * Add TODOs to handle the same thing in global directories. Right now, given the shared nature of these projects, it's a bit too dangerous to do so. --- .../src/main/scala/sbt/compiler/Eval.scala | 13 ++- .../test/scala/sbt/compiler/EvalTest.scala | 2 +- .../scala/sbt/EvaluateConfigurations.scala | 39 ++++--- main/src/main/scala/sbt/Load.scala | 106 +++++++++++++----- main/src/main/scala/sbt/LoadedSbtFile.scala | 12 +- notes/0.13.6.md | 4 + .../actions/eval-is-safe-and-sound/build.sbt | 46 +++++++- .../eval-is-safe-and-sound/changes/extras.sbt | 3 + .../actions/eval-is-safe-and-sound/test | 9 +- 9 files changed, 181 insertions(+), 53 deletions(-) create mode 100644 sbt/src/sbt-test/actions/eval-is-safe-and-sound/changes/extras.sbt diff --git a/main/actions/src/main/scala/sbt/compiler/Eval.scala b/main/actions/src/main/scala/sbt/compiler/Eval.scala index 1174b34d3..d075bfebc 100644 --- a/main/actions/src/main/scala/sbt/compiler/Eval.scala +++ b/main/actions/src/main/scala/sbt/compiler/Eval.scala @@ -153,8 +153,8 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se finally { unlinkAll() } } - val classFiles = getClassFiles(backing, moduleName) - new EvalIntermediate(extra, loader, classFiles, moduleName) + val generatedFiles = getGeneratedFiles(backing, moduleName) + new EvalIntermediate(extra, loader, generatedFiles, moduleName) } // location of the cached type or definition information private[this] def cacheFile(base: File, moduleName: String): File = new File(base, moduleName + ".cache") @@ -254,11 +254,20 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se private[this] def classExists(dir: File, name: String) = (new File(dir, name + ".class")).exists // TODO: use the code from Analyzer + private[this] def getGeneratedFiles(backing: Option[File], moduleName: String): Seq[File] = + backing match { + case None => Nil + case Some(dir) => dir listFiles moduleFileFilter(moduleName) + } private[this] def getClassFiles(backing: Option[File], moduleName: String): Seq[File] = backing match { case None => Nil case Some(dir) => dir listFiles moduleClassFilter(moduleName) } + private[this] def moduleFileFilter(moduleName: String) = new java.io.FilenameFilter { + def accept(dir: File, s: String) = + (s contains moduleName) + } private[this] def moduleClassFilter(moduleName: String) = new java.io.FilenameFilter { def accept(dir: File, s: String) = (s contains moduleName) && (s endsWith ".class") diff --git a/main/actions/src/test/scala/sbt/compiler/EvalTest.scala b/main/actions/src/test/scala/sbt/compiler/EvalTest.scala index efd87b126..fdbc6fe3d 100644 --- a/main/actions/src/test/scala/sbt/compiler/EvalTest.scala +++ b/main/actions/src/test/scala/sbt/compiler/EvalTest.scala @@ -60,7 +60,7 @@ val p = { property("val test") = secure { val defs = (ValTestContent, 1 to 7) :: Nil - val res = eval.evalDefinitions(defs, new EvalImports(Nil, ""), "", "scala.Int" :: Nil) + val res = eval.evalDefinitions(defs, new EvalImports(Nil, ""), "", None, "scala.Int" :: Nil) label("Val names", res.valNames) |: (res.valNames.toSet == ValTestNames) } diff --git a/main/src/main/scala/sbt/EvaluateConfigurations.scala b/main/src/main/scala/sbt/EvaluateConfigurations.scala index 7fc398259..54531c5bd 100644 --- a/main/src/main/scala/sbt/EvaluateConfigurations.scala +++ b/main/src/main/scala/sbt/EvaluateConfigurations.scala @@ -23,6 +23,11 @@ import scala.annotation.tailrec * */ object EvaluateConfigurations { + + type LazyClassLoaded[T] = ClassLoader => T + + private[sbt] case class TrackedEvalResult[T](generated: Seq[File], result: LazyClassLoaded[T]) + /** * This represents the parsed expressions in a build sbt, as well as where they were defined. */ @@ -38,7 +43,7 @@ object EvaluateConfigurations { * raw sbt-types that can be accessed and used. */ @deprecated("We no longer merge build.sbt files together unless they are in the same directory.", "0.13.6") - def apply(eval: Eval, srcs: Seq[File], imports: Seq[String]): ClassLoader => LoadedSbtFile = + def apply(eval: Eval, srcs: Seq[File], imports: Seq[String]): LazyClassLoaded[LoadedSbtFile] = { val loadFiles = srcs.sortBy(_.getName) map { src => evaluateSbtFile(eval, src, IO.readLines(src), imports, 0) } loader => (LoadedSbtFile.empty /: loadFiles) { (loaded, load) => loaded merge load(loader) } @@ -49,7 +54,7 @@ object EvaluateConfigurations { * * Note: This ignores any non-Setting[_] values in the file. */ - def evaluateConfiguration(eval: Eval, src: File, imports: Seq[String]): ClassLoader => Seq[Setting[_]] = + def evaluateConfiguration(eval: Eval, src: File, imports: Seq[String]): LazyClassLoaded[Seq[Setting[_]]] = evaluateConfiguration(eval, src, IO.readLines(src), imports, 0) /** @@ -76,7 +81,7 @@ object EvaluateConfigurations { * * @return Just the Setting[_] instances defined in the .sbt file. */ - def evaluateConfiguration(eval: Eval, file: File, lines: Seq[String], imports: Seq[String], offset: Int): ClassLoader => Seq[Setting[_]] = + def evaluateConfiguration(eval: Eval, file: File, lines: Seq[String], imports: Seq[String], offset: Int): LazyClassLoaded[Seq[Setting[_]]] = { val l = evaluateSbtFile(eval, file, lines, imports, offset) loader => l(loader).settings @@ -93,7 +98,7 @@ object EvaluateConfigurations { * @return A function which can take an sbt classloader and return the raw types/configuratoin * which was compiled/parsed for the given file. */ - private[sbt] def evaluateSbtFile(eval: Eval, file: File, lines: Seq[String], imports: Seq[String], offset: Int): ClassLoader => LoadedSbtFile = + private[sbt] def evaluateSbtFile(eval: Eval, file: File, lines: Seq[String], imports: Seq[String], offset: Int): LazyClassLoaded[LoadedSbtFile] = { // TODO - Store the file on the LoadedSbtFile (or the parent dir) so we can accurately do // detection for which project project manipulations should be applied. @@ -112,13 +117,15 @@ object EvaluateConfigurations { evaluateDslEntry(eval, name, allImports, dslExpression, range) } eval.unlinkDeferred() + // Tracks all the files we generated from evaluating the sbt file. + val allGeneratedFiles = (definitions.generated ++ dslEntries.flatMap(_.generated)) loader => { val projects = definitions.values(loader).collect { case p: Project => resolveBase(file.getParentFile, p) } val (settingsRaw, manipulationsRaw) = - dslEntries map (_ apply loader) partition { + dslEntries map (_.result apply loader) partition { case internals.ProjectSettings(_) => true case _ => false } @@ -130,7 +137,7 @@ object EvaluateConfigurations { case internals.ProjectManipulation(f) => f } // TODO -get project manipulations. - new LoadedSbtFile(settings, projects, importDefs, manipulations, definitions) + new LoadedSbtFile(settings, projects, importDefs, manipulations, definitions, allGeneratedFiles) } } /** move a project to be relative to this file after we've evaluated it. */ @@ -161,18 +168,22 @@ object EvaluateConfigurations { * @param range The original position in source of the expression, for error messages. * * @return A method that given an sbt classloader, can return the actual [[DslEntry]] defined by - * the expression. + * the expression, and the sequence of .class files generated. */ - private[sbt] def evaluateDslEntry(eval: Eval, name: String, imports: Seq[(String, Int)], expression: String, range: LineRange): ClassLoader => internals.DslEntry = { + private[sbt] def evaluateDslEntry(eval: Eval, name: String, imports: Seq[(String, Int)], expression: String, range: LineRange): TrackedEvalResult[internals.DslEntry] = { + // TODO - Should we try to namespace these between.sbt files? IF they hash to the same value, they may actually be + // exactly the same setting, so perhaps we don't care? val result = try { eval.eval(expression, imports = new EvalImports(imports, name), srcName = name, tpeName = Some(SettingsDefinitionName), line = range.start) } catch { case e: sbt.compiler.EvalException => throw new MessageOnlyException(e.getMessage) } - loader => { - val pos = RangePosition(name, range shift 1) - result.getValue(loader).asInstanceOf[internals.DslEntry].withPos(pos) - } + // TODO - keep track of configuration classes defined. + TrackedEvalResult(result.generated, + loader => { + val pos = RangePosition(name, range shift 1) + result.getValue(loader).asInstanceOf[internals.DslEntry].withPos(pos) + }) } /** @@ -189,9 +200,9 @@ object EvaluateConfigurations { * the expression. */ @deprecated("Build DSL now includes non-Setting[_] type settings.", "0.13.6") // Note: This method is used by the SET command, so we may want to evaluate that sucker a bit. - def evaluateSetting(eval: Eval, name: String, imports: Seq[(String, Int)], expression: String, range: LineRange): ClassLoader => Seq[Setting[_]] = + def evaluateSetting(eval: Eval, name: String, imports: Seq[(String, Int)], expression: String, range: LineRange): LazyClassLoaded[Seq[Setting[_]]] = { - evaluateDslEntry(eval, name, imports, expression, range) andThen { + evaluateDslEntry(eval, name, imports, expression, range).result andThen { case internals.ProjectSettings(values) => values case _ => Nil } diff --git a/main/src/main/scala/sbt/Load.scala b/main/src/main/scala/sbt/Load.scala index 922b51d76..e0512616d 100755 --- a/main/src/main/scala/sbt/Load.scala +++ b/main/src/main/scala/sbt/Load.scala @@ -75,7 +75,13 @@ object Load { { val eval = mkEval(data(config.globalPluginClasspath), base, defaultEvalOptions) val imports = BuildUtil.baseImports ++ BuildUtil.importAllRoot(config.globalPluginNames) - loader => EvaluateConfigurations(eval, files, imports)(loader).settings + loader => { + val loaded = EvaluateConfigurations(eval, files, imports)(loader) + // TODO - We have a potential leak of config-classes in the global directory right now. + // We need to find a way to clean these safely, or at least warn users about + // unused class files that could be cleaned when multiple sbt instances are not running. + loaded.settings + } } def loadGlobal(state: State, base: File, global: File, config: sbt.LoadBuildConfiguration): sbt.LoadBuildConfiguration = if (base != global && global.exists) { @@ -238,8 +244,34 @@ object Load { def mkEval(classpath: Seq[File], base: File, options: Seq[String]): Eval = new Eval(options, classpath, s => new ConsoleReporter(s), Some(evalOutputDirectory(base))) + /** + * This will clean up left-over files in the config-classes directory if they are no longer used. + * + * @param base The base directory for the build, should match the one passed into `mkEval` method. + */ + def cleanEvalClasses(base: File, keep: Seq[File]): Unit = { + val baseTarget = evalOutputDirectory(base) + val keepSet = keep.map(_.getCanonicalPath).toSet + // If there are no keeper files, this may be because cache was up-to-date and + // the files aren't properly returned, even though they should be. + // TODO - figure out where the caching of whether or not to generate classfiles occurs, and + // put cleanups there, perhaps. + if (!keepSet.isEmpty) { + def keepFile(f: File) = keepSet(f.getCanonicalPath) + import Path._ + val existing = (baseTarget.***.get).filterNot(_.isDirectory) + val toDelete = existing.filterNot(keepFile) + if (!toDelete.isEmpty) { + System.err.println(s"Discovered unused files: ${toDelete.mkString("\n * ", "\n * ", "\n")}. Note: Please reload all other running sbt instances in $base") + IO.delete(toDelete) + } + } + } + + @deprecated("This method is no longer used", "0.13.6") def configurations(srcs: Seq[File], eval: () => Eval, imports: Seq[String]): ClassLoader => LoadedSbtFile = - if (srcs.isEmpty) const(LoadedSbtFile.empty) else EvaluateConfigurations(eval(), srcs, imports) + if (srcs.isEmpty) const(LoadedSbtFile.empty) + else EvaluateConfigurations(eval(), srcs, imports) def load(file: File, s: State, config: sbt.LoadBuildConfiguration): sbt.PartBuild = load(file, builtinLoader(s, config.copy(pluginManagement = config.pluginManagement.shift, extraBuilds = Nil)), config.extraBuilds.toList) @@ -400,6 +432,7 @@ object Load { val plugs = plugins(defDir, s, config.copy(pluginManagement = config.pluginManagement.forPlugin)) val defsScala = plugs.detected.builds.values + // NOTE - because we create an eval here, we need a clean-eval later for this URI. lazy val eval = mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions) val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase)) @@ -407,25 +440,29 @@ object Load { val memoSettings = new mutable.HashMap[File, LoadedSbtFile] def loadProjects(ps: Seq[Project], createRoot: Boolean) = { - loadTransitive(ps, normBase, plugs, () => eval, config.injectSettings, Nil, memoSettings, config.log, createRoot, uri, config.pluginManagement.context) + loadTransitive(ps, normBase, plugs, () => eval, config.injectSettings, Nil, memoSettings, config.log, createRoot, uri, config.pluginManagement.context, Nil) } val loadedProjectsRaw = loadProjects(initialProjects, !hasRootAlreadyDefined) // TODO - As of sbt 0.13.6 we should always have a default root project from // here on, so the autogenerated build aggregated can be removed from this code. ( I think) // We may actually want to move it back here and have different flags in loadTransitive... - val hasRoot = loadedProjectsRaw.exists(_.base == normBase) || defsScala.exists(_.rootProject.isDefined) - val (loadedProjects, defaultBuildIfNone) = + val hasRoot = loadedProjectsRaw.projects.exists(_.base == normBase) || defsScala.exists(_.rootProject.isDefined) + val (loadedProjects, defaultBuildIfNone, keepClassFiles) = if (hasRoot) - (loadedProjectsRaw, Build.defaultEmpty) + (loadedProjectsRaw.projects, Build.defaultEmpty, loadedProjectsRaw.generatedConfigClassFiles) else { - val existingIDs = loadedProjectsRaw.map(_.id) + val existingIDs = loadedProjectsRaw.projects.map(_.id) val refs = existingIDs.map(id => ProjectRef(uri, id)) val defaultID = autoID(normBase, config.pluginManagement.context, existingIDs) val b = Build.defaultAggregated(defaultID, refs) val defaultProjects = loadProjects(projectsFromBuild(b, normBase), false) - (defaultProjects ++ loadedProjectsRaw, b) + (defaultProjects.projects ++ loadedProjectsRaw.projects, b, defaultProjects.generatedConfigClassFiles ++ loadedProjectsRaw.generatedConfigClassFiles) } + // Now we clean stale class files. + // TODO - this may cause issues with multiple sbt clients, but that should be deprecated pending sbt-server anyway + cleanEvalClasses(defDir, keepClassFiles) + val defs = if (defsScala.isEmpty) defaultBuildIfNone :: Nil else defsScala // HERE we pull out the defined vals from memoSettings and unify them all so // we can use them later. @@ -458,6 +495,8 @@ object Load { private[this] def projectsFromBuild(b: Build, base: File): Seq[Project] = b.projectDefinitions(base).map(resolveBase(base)) + // Lame hackery to keep track of our state. + private[this] case class LoadedProjects(projects: Seq[Project], generatedConfigClassFiles: Seq[File]) /** * Loads a new set of projects, including any transitively defined projects underneath this one. * @@ -498,10 +537,12 @@ object Load { log: Logger, makeOrDiscoverRoot: Boolean, buildUri: URI, - context: PluginManagement.Context): Seq[Project] = + context: PluginManagement.Context, + generatedConfigClassFiles: Seq[File]): LoadedProjects = { // load all relevant configuration files (.sbt, as .scala already exists at this point) - def discover(auto: AddSettings, base: File): DiscoveredProjects = discoverProjects(auto, base, plugins, eval, memoSettings) + def discover(auto: AddSettings, base: File): DiscoveredProjects = + discoverProjects(auto, base, plugins, eval, memoSettings) // Step two, Finalize a project with all its settings/configuration. def finalizeProject(p: Project, configFiles: Seq[File]): Project = { val loadedFiles = configFiles flatMap { f => memoSettings.get(f) } @@ -509,48 +550,49 @@ object Load { } // Discover any new project definition for the base directory of this project, and load all settings. // Also return any newly discovered project instances. - def discoverAndLoad(p: Project): (Project, Seq[Project]) = { - val (root, discovered, files) = discover(p.auto, p.base) match { - case DiscoveredProjects(Some(root), rest, files) => + def discoverAndLoad(p: Project): (Project, Seq[Project], Seq[File]) = { + val (root, discovered, files, generated) = discover(p.auto, p.base) match { + case DiscoveredProjects(Some(root), rest, files, generated) => // TODO - We assume here the project defined in a build.sbt WINS because the original was // a phony. However, we may want to 'merge' the two, or only do this if the original was a default // generated project. - (root, rest, files) - case DiscoveredProjects(None, rest, files) => (p, rest, files) + (root, rest, files, generated) + case DiscoveredProjects(None, rest, files, generated) => (p, rest, files, generated) } val finalRoot = finalizeProject(root, files) - finalRoot -> discovered + (finalRoot, discovered, generated) } // Load all config files AND finalize the project at the root directory, if it exists. // Continue loading if we find any more. newProjects match { case Seq(next, rest @ _*) => log.debug(s"[Loading] Loading project ${next.id} @ ${next.base}") - val (finished, discovered) = discoverAndLoad(next) - loadTransitive(rest ++ discovered, buildBase, plugins, eval, injectSettings, acc :+ finished, memoSettings, log, false, buildUri, context) + val (finished, discovered, generated) = discoverAndLoad(next) + loadTransitive(rest ++ discovered, buildBase, plugins, eval, injectSettings, acc :+ finished, memoSettings, log, false, buildUri, context, generated) case Nil if makeOrDiscoverRoot => log.debug(s"[Loading] Scanning directory ${buildBase}") discover(AddSettings.defaultSbtFiles, buildBase) match { - case DiscoveredProjects(Some(root), discovered, files) => + case DiscoveredProjects(Some(root), discovered, files, generated) => log.debug(s"[Loading] Found root project ${root.id} w/ remaining ${discovered.map(_.id).mkString(",")}") val finalRoot = finalizeProject(root, files) - loadTransitive(discovered, buildBase, plugins, eval, injectSettings, finalRoot +: acc, memoSettings, log, false, buildUri, context) + loadTransitive(discovered, buildBase, plugins, eval, injectSettings, finalRoot +: acc, memoSettings, log, false, buildUri, context, generated) // Here we need to create a root project... - case DiscoveredProjects(None, discovered, files) => + case DiscoveredProjects(None, discovered, files, generated) => log.debug(s"[Loading] Found non-root projects ${discovered.map(_.id).mkString(",")}") // Here we do something interesting... We need to create an aggregate root project - val otherProjects = loadTransitive(discovered, buildBase, plugins, eval, injectSettings, acc, memoSettings, log, false, buildUri, context) - val existingIds = otherProjects map (_.id) + val otherProjects = loadTransitive(discovered, buildBase, plugins, eval, injectSettings, acc, memoSettings, log, false, buildUri, context, Nil) + val otherGenerated = otherProjects.generatedConfigClassFiles + val existingIds = otherProjects.projects map (_.id) val refs = existingIds map (id => ProjectRef(buildUri, id)) val defaultID = autoID(buildBase, context, existingIds) val root = finalizeProject(Build.defaultAggregatedProject(defaultID, buildBase, refs), files) - val result = root +: (acc ++ otherProjects) + val result = root +: (acc ++ otherProjects.projects) log.debug(s"[Loading] Done in ${buildBase}, returning: ${result.map(_.id).mkString("(", ", ", ")")}") - result + LoadedProjects(result, generated ++ otherGenerated) } case Nil => log.debug(s"[Loading] Done in ${buildBase}, returning: ${acc.map(_.id).mkString("(", ", ", ")")}") - acc + LoadedProjects(acc, generatedConfigClassFiles) } } @@ -564,8 +606,13 @@ object Load { * @param root The project at "root" directory we were looking, or non if non was defined. * @param nonRoot Any sub-projects discovered from this directory * @param sbtFiles Any sbt file loaded during this discovery (used later to complete the project). + * @param generatedFile Any .class file that was generated when compiling/discovering these projects. */ - private[this] case class DiscoveredProjects(root: Option[Project], nonRoot: Seq[Project], sbtFiles: Seq[File]) + private[this] case class DiscoveredProjects( + root: Option[Project], + nonRoot: Seq[Project], + sbtFiles: Seq[File], + generatedFiles: Seq[File]) /** * This method attempts to resolve/apply all configuration loaded for a project. It is responsible for the following: @@ -682,10 +729,11 @@ object Load { case _ => Seq.empty } val rawFiles = associatedFiles(auto) - val rawProjects = loadFiles(rawFiles).projects + val loadedFiles = loadFiles(rawFiles) + val rawProjects = loadedFiles.projects val (root, nonRoot) = rawProjects.partition(_.base == projectBase) // TODO - good error message if more than one root project - DiscoveredProjects(root.headOption, nonRoot, rawFiles) + DiscoveredProjects(root.headOption, nonRoot, rawFiles, loadedFiles.generatedFiles) } def globalPluginClasspath(globalPlugin: Option[GlobalPlugin]): Seq[Attributed[File]] = diff --git a/main/src/main/scala/sbt/LoadedSbtFile.scala b/main/src/main/scala/sbt/LoadedSbtFile.scala index ef8a5d694..58b0152fa 100644 --- a/main/src/main/scala/sbt/LoadedSbtFile.scala +++ b/main/src/main/scala/sbt/LoadedSbtFile.scala @@ -14,17 +14,19 @@ private[sbt] final class LoadedSbtFile( val manipulations: Seq[Project => Project], // TODO - we may want to expose a simpler interface on top of here for the set command, // rather than what we have now... - val definitions: DefinedSbtValues) { - @deprecated("LoadedSbtFiles are no longer directly merged.", "0.13.6") + val definitions: DefinedSbtValues, + val generatedFiles: Seq[File]) { + // We still use merge for now. We track originating sbt file in an alternative manner. def merge(o: LoadedSbtFile): LoadedSbtFile = new LoadedSbtFile( settings ++ o.settings, projects ++ o.projects, importedDefs ++ o.importedDefs, manipulations, - definitions zip o.definitions) + definitions zip o.definitions, + generatedFiles ++ o.generatedFiles) - def clearProjects = new LoadedSbtFile(settings, Nil, importedDefs, manipulations, definitions) + def clearProjects = new LoadedSbtFile(settings, Nil, importedDefs, manipulations, definitions, generatedFiles) } /** @@ -74,6 +76,6 @@ private[sbt] object DefinedSbtValues { private[sbt] object LoadedSbtFile { /** Represents an empty .sbt file: no Projects, imports, or settings.*/ - def empty = new LoadedSbtFile(Nil, Nil, Nil, Nil, DefinedSbtValues.empty) + def empty = new LoadedSbtFile(Nil, Nil, Nil, Nil, DefinedSbtValues.empty, Nil) } diff --git a/notes/0.13.6.md b/notes/0.13.6.md index f04773615..d7074225e 100644 --- a/notes/0.13.6.md +++ b/notes/0.13.6.md @@ -45,6 +45,8 @@ [1489]: https://github.com/sbt/sbt/pull/1489 [1494]: https://github.com/sbt/sbt/pull/1494 [1516]: https://github.com/sbt/sbt/pull/1516 + [1465]: https://github.com/sbt/sbt/issues/1465 + [1524]: https://github.com/sbt/sbt/issues/1524 [@dansanduleac]: https://github.com/dansanduleac [@2m]: https://github.com/2m @@ -104,6 +106,8 @@ - set no longer removes any `++` scala version setting. [#856][856]/[#1489][1489] by [@jsuereth][@jsuereth] - Fixes `Scope.parseScopedKey`. [#1384][1384] by [@eed3si9n][@eed3si9n] - Fixes `build.sbt` errors causing `ArrayIndexOutOfBoundsException` due to invalid source in position. [#1181][1181] by [@eed3si9n][@eed3si9n] +- Fixes config-classes leak in loading build files. [#1524][1524] by [@jsuereth][@jsuereth] +- Fixes name-conflicts in hashed settings class files. [#1465][1465] by [@jsuereth][@jsuereth] ### Maven Central Repository defaults to HTTPS diff --git a/sbt/src/sbt-test/actions/eval-is-safe-and-sound/build.sbt b/sbt/src/sbt-test/actions/eval-is-safe-and-sound/build.sbt index ab63c3ec3..387b24f5d 100644 --- a/sbt/src/sbt-test/actions/eval-is-safe-and-sound/build.sbt +++ b/sbt/src/sbt-test/actions/eval-is-safe-and-sound/build.sbt @@ -2,4 +2,48 @@ lazy val common = project lazy val boink = project -lazy val woof = project \ No newline at end of file +lazy val woof = project + + +lazy val numConfigClasses = taskKey[Int]("counts number of config classes") + +lazy val configClassCountFile = settingKey[File]("File where we write the # of config classes") + +lazy val saveNumConfigClasses = taskKey[Unit]("Saves the number of config clases") + +lazy val checkNumConfigClasses = taskKey[Unit]("Checks the number of config clases") + +lazy val checkDifferentConfigClasses = taskKey[Unit]("Checks that the number of config classes are different.") + +configClassCountFile := (target.value / "config-count") + +numConfigClasses := { + val cdir = (baseDirectory in ThisBuild).value / "project/target/config-classes" + (cdir.*** --- cdir).get.length +} + +saveNumConfigClasses := { + IO.write(configClassCountFile.value, numConfigClasses.value.toString) +} + +def previousConfigCount = Def.task { + val previousString = IO.read(configClassCountFile.value) + try Integer.parseInt(previousString) + catch { + case t: Throwable => throw new RuntimeException(s"Failed to parse previous config file value: $previousString", t) + } +} + +checkDifferentConfigClasses := { + val previousString = IO.read(configClassCountFile.value) + val previous = previousConfigCount.value + val current = numConfigClasses.value + assert(previous != current, s"Failed to create new configuration classes. Expected: $previous, Found: $current") +} + +checkNumConfigClasses := { + val previousString = IO.read(configClassCountFile.value) + val previous = previousConfigCount.value + val current = numConfigClasses.value + assert(previous == current, s"Failed to delete extra configuration classes. Expected: $previous, Found: $current") +} \ No newline at end of file diff --git a/sbt/src/sbt-test/actions/eval-is-safe-and-sound/changes/extras.sbt b/sbt/src/sbt-test/actions/eval-is-safe-and-sound/changes/extras.sbt new file mode 100644 index 000000000..89140c994 --- /dev/null +++ b/sbt/src/sbt-test/actions/eval-is-safe-and-sound/changes/extras.sbt @@ -0,0 +1,3 @@ +organization := "more" + +version := "settings" \ No newline at end of file diff --git a/sbt/src/sbt-test/actions/eval-is-safe-and-sound/test b/sbt/src/sbt-test/actions/eval-is-safe-and-sound/test index 73a68203f..51bbe9d89 100644 --- a/sbt/src/sbt-test/actions/eval-is-safe-and-sound/test +++ b/sbt/src/sbt-test/actions/eval-is-safe-and-sound/test @@ -1 +1,8 @@ -> compile \ No newline at end of file +> compile +> saveNumConfigClasses +$ copy-file changes/extras.sbt extras.sbt +> reload +> checkDifferentConfigClasses +$ delete extras.sbt +> reload +> checkNumConfigClasses \ No newline at end of file