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.
This commit is contained in:
Josh Suereth 2014-08-12 12:35:37 -04:00
parent 0bfb5a4118
commit a1b3117a42
9 changed files with 181 additions and 53 deletions

View File

@ -153,8 +153,8 @@ final class Eval(optionsNoncp: Seq[String], classpath: Seq[File], mkReporter: Se
finally { unlinkAll() } finally { unlinkAll() }
} }
val classFiles = getClassFiles(backing, moduleName) val generatedFiles = getGeneratedFiles(backing, moduleName)
new EvalIntermediate(extra, loader, classFiles, moduleName) new EvalIntermediate(extra, loader, generatedFiles, moduleName)
} }
// location of the cached type or definition information // location of the cached type or definition information
private[this] def cacheFile(base: File, moduleName: String): File = new File(base, moduleName + ".cache") 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 private[this] def classExists(dir: File, name: String) = (new File(dir, name + ".class")).exists
// TODO: use the code from Analyzer // 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] = private[this] def getClassFiles(backing: Option[File], moduleName: String): Seq[File] =
backing match { backing match {
case None => Nil case None => Nil
case Some(dir) => dir listFiles moduleClassFilter(moduleName) 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 { private[this] def moduleClassFilter(moduleName: String) = new java.io.FilenameFilter {
def accept(dir: File, s: String) = def accept(dir: File, s: String) =
(s contains moduleName) && (s endsWith ".class") (s contains moduleName) && (s endsWith ".class")

View File

@ -60,7 +60,7 @@ val p = {
property("val test") = secure { property("val test") = secure {
val defs = (ValTestContent, 1 to 7) :: Nil val defs = (ValTestContent, 1 to 7) :: Nil
val res = eval.evalDefinitions(defs, new EvalImports(Nil, ""), "<defs>", "scala.Int" :: Nil) val res = eval.evalDefinitions(defs, new EvalImports(Nil, ""), "<defs>", None, "scala.Int" :: Nil)
label("Val names", res.valNames) |: (res.valNames.toSet == ValTestNames) label("Val names", res.valNames) |: (res.valNames.toSet == ValTestNames)
} }

View File

@ -23,6 +23,11 @@ import scala.annotation.tailrec
* *
*/ */
object EvaluateConfigurations { 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. * 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. * 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") @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) } 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) } 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. * 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) evaluateConfiguration(eval, src, IO.readLines(src), imports, 0)
/** /**
@ -76,7 +81,7 @@ object EvaluateConfigurations {
* *
* @return Just the Setting[_] instances defined in the .sbt file. * @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) val l = evaluateSbtFile(eval, file, lines, imports, offset)
loader => l(loader).settings 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 * @return A function which can take an sbt classloader and return the raw types/configuratoin
* which was compiled/parsed for the given file. * 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 // 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. // detection for which project project manipulations should be applied.
@ -112,13 +117,15 @@ object EvaluateConfigurations {
evaluateDslEntry(eval, name, allImports, dslExpression, range) evaluateDslEntry(eval, name, allImports, dslExpression, range)
} }
eval.unlinkDeferred() eval.unlinkDeferred()
// Tracks all the files we generated from evaluating the sbt file.
val allGeneratedFiles = (definitions.generated ++ dslEntries.flatMap(_.generated))
loader => { loader => {
val projects = val projects =
definitions.values(loader).collect { definitions.values(loader).collect {
case p: Project => resolveBase(file.getParentFile, p) case p: Project => resolveBase(file.getParentFile, p)
} }
val (settingsRaw, manipulationsRaw) = val (settingsRaw, manipulationsRaw) =
dslEntries map (_ apply loader) partition { dslEntries map (_.result apply loader) partition {
case internals.ProjectSettings(_) => true case internals.ProjectSettings(_) => true
case _ => false case _ => false
} }
@ -130,7 +137,7 @@ object EvaluateConfigurations {
case internals.ProjectManipulation(f) => f case internals.ProjectManipulation(f) => f
} }
// TODO -get project manipulations. // 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. */ /** 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. * @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 * @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 { val result = try {
eval.eval(expression, imports = new EvalImports(imports, name), srcName = name, tpeName = Some(SettingsDefinitionName), line = range.start) eval.eval(expression, imports = new EvalImports(imports, name), srcName = name, tpeName = Some(SettingsDefinitionName), line = range.start)
} catch { } catch {
case e: sbt.compiler.EvalException => throw new MessageOnlyException(e.getMessage) case e: sbt.compiler.EvalException => throw new MessageOnlyException(e.getMessage)
} }
loader => { // TODO - keep track of configuration classes defined.
val pos = RangePosition(name, range shift 1) TrackedEvalResult(result.generated,
result.getValue(loader).asInstanceOf[internals.DslEntry].withPos(pos) loader => {
} val pos = RangePosition(name, range shift 1)
result.getValue(loader).asInstanceOf[internals.DslEntry].withPos(pos)
})
} }
/** /**
@ -189,9 +200,9 @@ object EvaluateConfigurations {
* the expression. * 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. @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 internals.ProjectSettings(values) => values
case _ => Nil case _ => Nil
} }

View File

@ -75,7 +75,13 @@ object Load {
{ {
val eval = mkEval(data(config.globalPluginClasspath), base, defaultEvalOptions) val eval = mkEval(data(config.globalPluginClasspath), base, defaultEvalOptions)
val imports = BuildUtil.baseImports ++ BuildUtil.importAllRoot(config.globalPluginNames) 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 = def loadGlobal(state: State, base: File, global: File, config: sbt.LoadBuildConfiguration): sbt.LoadBuildConfiguration =
if (base != global && global.exists) { if (base != global && global.exists) {
@ -238,8 +244,34 @@ object Load {
def mkEval(classpath: Seq[File], base: File, options: Seq[String]): Eval = def mkEval(classpath: Seq[File], base: File, options: Seq[String]): Eval =
new Eval(options, classpath, s => new ConsoleReporter(s), Some(evalOutputDirectory(base))) 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 = 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 = 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) 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 plugs = plugins(defDir, s, config.copy(pluginManagement = config.pluginManagement.forPlugin))
val defsScala = plugs.detected.builds.values 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) lazy val eval = mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions)
val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase)) val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase))
@ -407,25 +440,29 @@ object Load {
val memoSettings = new mutable.HashMap[File, LoadedSbtFile] val memoSettings = new mutable.HashMap[File, LoadedSbtFile]
def loadProjects(ps: Seq[Project], createRoot: Boolean) = { 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) val loadedProjectsRaw = loadProjects(initialProjects, !hasRootAlreadyDefined)
// TODO - As of sbt 0.13.6 we should always have a default root project from // 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) // 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... // 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 hasRoot = loadedProjectsRaw.projects.exists(_.base == normBase) || defsScala.exists(_.rootProject.isDefined)
val (loadedProjects, defaultBuildIfNone) = val (loadedProjects, defaultBuildIfNone, keepClassFiles) =
if (hasRoot) if (hasRoot)
(loadedProjectsRaw, Build.defaultEmpty) (loadedProjectsRaw.projects, Build.defaultEmpty, loadedProjectsRaw.generatedConfigClassFiles)
else { else {
val existingIDs = loadedProjectsRaw.map(_.id) val existingIDs = loadedProjectsRaw.projects.map(_.id)
val refs = existingIDs.map(id => ProjectRef(uri, id)) val refs = existingIDs.map(id => ProjectRef(uri, id))
val defaultID = autoID(normBase, config.pluginManagement.context, existingIDs) val defaultID = autoID(normBase, config.pluginManagement.context, existingIDs)
val b = Build.defaultAggregated(defaultID, refs) val b = Build.defaultAggregated(defaultID, refs)
val defaultProjects = loadProjects(projectsFromBuild(b, normBase), false) 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 val defs = if (defsScala.isEmpty) defaultBuildIfNone :: Nil else defsScala
// HERE we pull out the defined vals from memoSettings and unify them all so // HERE we pull out the defined vals from memoSettings and unify them all so
// we can use them later. // we can use them later.
@ -458,6 +495,8 @@ object Load {
private[this] def projectsFromBuild(b: Build, base: File): Seq[Project] = private[this] def projectsFromBuild(b: Build, base: File): Seq[Project] =
b.projectDefinitions(base).map(resolveBase(base)) 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. * Loads a new set of projects, including any transitively defined projects underneath this one.
* *
@ -498,10 +537,12 @@ object Load {
log: Logger, log: Logger,
makeOrDiscoverRoot: Boolean, makeOrDiscoverRoot: Boolean,
buildUri: URI, 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) // 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. // Step two, Finalize a project with all its settings/configuration.
def finalizeProject(p: Project, configFiles: Seq[File]): Project = { def finalizeProject(p: Project, configFiles: Seq[File]): Project = {
val loadedFiles = configFiles flatMap { f => memoSettings.get(f) } 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. // Discover any new project definition for the base directory of this project, and load all settings.
// Also return any newly discovered project instances. // Also return any newly discovered project instances.
def discoverAndLoad(p: Project): (Project, Seq[Project]) = { def discoverAndLoad(p: Project): (Project, Seq[Project], Seq[File]) = {
val (root, discovered, files) = discover(p.auto, p.base) match { val (root, discovered, files, generated) = discover(p.auto, p.base) match {
case DiscoveredProjects(Some(root), rest, files) => case DiscoveredProjects(Some(root), rest, files, generated) =>
// TODO - We assume here the project defined in a build.sbt WINS because the original was // 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 // a phony. However, we may want to 'merge' the two, or only do this if the original was a default
// generated project. // generated project.
(root, rest, files) (root, rest, files, generated)
case DiscoveredProjects(None, rest, files) => (p, rest, files) case DiscoveredProjects(None, rest, files, generated) => (p, rest, files, generated)
} }
val finalRoot = finalizeProject(root, files) 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. // Load all config files AND finalize the project at the root directory, if it exists.
// Continue loading if we find any more. // Continue loading if we find any more.
newProjects match { newProjects match {
case Seq(next, rest @ _*) => case Seq(next, rest @ _*) =>
log.debug(s"[Loading] Loading project ${next.id} @ ${next.base}") log.debug(s"[Loading] Loading project ${next.id} @ ${next.base}")
val (finished, discovered) = discoverAndLoad(next) val (finished, discovered, generated) = discoverAndLoad(next)
loadTransitive(rest ++ discovered, buildBase, plugins, eval, injectSettings, acc :+ finished, memoSettings, log, false, buildUri, context) loadTransitive(rest ++ discovered, buildBase, plugins, eval, injectSettings, acc :+ finished, memoSettings, log, false, buildUri, context, generated)
case Nil if makeOrDiscoverRoot => case Nil if makeOrDiscoverRoot =>
log.debug(s"[Loading] Scanning directory ${buildBase}") log.debug(s"[Loading] Scanning directory ${buildBase}")
discover(AddSettings.defaultSbtFiles, buildBase) match { 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(",")}") log.debug(s"[Loading] Found root project ${root.id} w/ remaining ${discovered.map(_.id).mkString(",")}")
val finalRoot = finalizeProject(root, files) 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... // 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(",")}") 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 // 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 otherProjects = loadTransitive(discovered, buildBase, plugins, eval, injectSettings, acc, memoSettings, log, false, buildUri, context, Nil)
val existingIds = otherProjects map (_.id) val otherGenerated = otherProjects.generatedConfigClassFiles
val existingIds = otherProjects.projects map (_.id)
val refs = existingIds map (id => ProjectRef(buildUri, id)) val refs = existingIds map (id => ProjectRef(buildUri, id))
val defaultID = autoID(buildBase, context, existingIds) val defaultID = autoID(buildBase, context, existingIds)
val root = finalizeProject(Build.defaultAggregatedProject(defaultID, buildBase, refs), files) 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("(", ", ", ")")}") log.debug(s"[Loading] Done in ${buildBase}, returning: ${result.map(_.id).mkString("(", ", ", ")")}")
result LoadedProjects(result, generated ++ otherGenerated)
} }
case Nil => case Nil =>
log.debug(s"[Loading] Done in ${buildBase}, returning: ${acc.map(_.id).mkString("(", ", ", ")")}") 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 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 nonRoot Any sub-projects discovered from this directory
* @param sbtFiles Any sbt file loaded during this discovery (used later to complete the project). * @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: * 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 case _ => Seq.empty
} }
val rawFiles = associatedFiles(auto) val rawFiles = associatedFiles(auto)
val rawProjects = loadFiles(rawFiles).projects val loadedFiles = loadFiles(rawFiles)
val rawProjects = loadedFiles.projects
val (root, nonRoot) = rawProjects.partition(_.base == projectBase) val (root, nonRoot) = rawProjects.partition(_.base == projectBase)
// TODO - good error message if more than one root project // 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]] = def globalPluginClasspath(globalPlugin: Option[GlobalPlugin]): Seq[Attributed[File]] =

View File

@ -14,17 +14,19 @@ private[sbt] final class LoadedSbtFile(
val manipulations: Seq[Project => Project], val manipulations: Seq[Project => Project],
// TODO - we may want to expose a simpler interface on top of here for the set command, // TODO - we may want to expose a simpler interface on top of here for the set command,
// rather than what we have now... // rather than what we have now...
val definitions: DefinedSbtValues) { val definitions: DefinedSbtValues,
@deprecated("LoadedSbtFiles are no longer directly merged.", "0.13.6") val generatedFiles: Seq[File]) {
// We still use merge for now. We track originating sbt file in an alternative manner.
def merge(o: LoadedSbtFile): LoadedSbtFile = def merge(o: LoadedSbtFile): LoadedSbtFile =
new LoadedSbtFile( new LoadedSbtFile(
settings ++ o.settings, settings ++ o.settings,
projects ++ o.projects, projects ++ o.projects,
importedDefs ++ o.importedDefs, importedDefs ++ o.importedDefs,
manipulations, 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 { private[sbt] object LoadedSbtFile {
/** Represents an empty .sbt file: no Projects, imports, or settings.*/ /** 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)
} }

View File

@ -45,6 +45,8 @@
[1489]: https://github.com/sbt/sbt/pull/1489 [1489]: https://github.com/sbt/sbt/pull/1489
[1494]: https://github.com/sbt/sbt/pull/1494 [1494]: https://github.com/sbt/sbt/pull/1494
[1516]: https://github.com/sbt/sbt/pull/1516 [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 [@dansanduleac]: https://github.com/dansanduleac
[@2m]: https://github.com/2m [@2m]: https://github.com/2m
@ -104,6 +106,8 @@
- set no longer removes any `++` scala version setting. [#856][856]/[#1489][1489] by [@jsuereth][@jsuereth] - set no longer removes any `++` scala version setting. [#856][856]/[#1489][1489] by [@jsuereth][@jsuereth]
- Fixes `Scope.parseScopedKey`. [#1384][1384] by [@eed3si9n][@eed3si9n] - 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 `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 ### Maven Central Repository defaults to HTTPS

View File

@ -3,3 +3,47 @@ lazy val common = project
lazy val boink = project lazy val boink = project
lazy val woof = project 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")
}

View File

@ -0,0 +1,3 @@
organization := "more"
version := "settings"

View File

@ -1 +1,8 @@
> compile > compile
> saveNumConfigClasses
$ copy-file changes/extras.sbt extras.sbt
> reload
> checkDifferentConfigClasses
$ delete extras.sbt
> reload
> checkNumConfigClasses