mirror of https://github.com/sbt/sbt.git
Merge pull request #1312 from sbt/wip/dsl-enable-plugins
Expand the DSL abilities with Project manipulations
This commit is contained in:
commit
3b1d63d0c9
|
|
@ -9,7 +9,8 @@ env:
|
||||||
- SCRIPTED_TEST="scripted dependency-management/*2of2"
|
- SCRIPTED_TEST="scripted dependency-management/*2of2"
|
||||||
- SCRIPTED_TEST="scripted java/*"
|
- SCRIPTED_TEST="scripted java/*"
|
||||||
- SCRIPTED_TEST="scripted package/*"
|
- SCRIPTED_TEST="scripted package/*"
|
||||||
- SCRIPTED_TEST="scripted project/*"
|
- SCRIPTED_TEST="scripted project/*1of2"
|
||||||
|
- SCRIPTED_TEST="scripted project/*2of2"
|
||||||
- SCRIPTED_TEST="scripted reporter/*"
|
- SCRIPTED_TEST="scripted reporter/*"
|
||||||
- SCRIPTED_TEST="scripted run/*"
|
- SCRIPTED_TEST="scripted run/*"
|
||||||
- SCRIPTED_TEST="scripted source-dependencies/*1of3"
|
- SCRIPTED_TEST="scripted source-dependencies/*1of3"
|
||||||
|
|
@ -21,6 +22,9 @@ env:
|
||||||
# TODO - we'd like to actually test everything, but the process library has a deadlock right now
|
# TODO - we'd like to actually test everything, but the process library has a deadlock right now
|
||||||
jdk:
|
jdk:
|
||||||
- openjdk6
|
- openjdk6
|
||||||
|
# - oraclejdk7
|
||||||
notifications:
|
notifications:
|
||||||
email:
|
email:
|
||||||
- qbranch@typesafe.com
|
- qbranch@typesafe.com
|
||||||
|
before_script:
|
||||||
|
- export JVM_OPTS="-Xms1024m -Xmx1024m -XX:ReservedCodeCacheSize=128m -XX:MaxPermSize=256m"
|
||||||
|
|
@ -68,7 +68,8 @@ object BuildUtil {
|
||||||
deps(proj)(_.aggregate)
|
deps(proj)(_.aggregate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
def baseImports: Seq[String] = "import sbt._, Keys._" :: Nil
|
|
||||||
|
def baseImports: Seq[String] = "import sbt._, Keys._, dsl._" :: Nil
|
||||||
|
|
||||||
def getImports(unit: BuildUnit): Seq[String] = unit.plugins.detected.imports
|
def getImports(unit: BuildUnit): Seq[String] = unit.plugins.detected.imports
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ object EvaluateConfigurations {
|
||||||
* return a parsed, compiled + evaluated [[LoadedSbtFile]]. The result has
|
* return a parsed, compiled + evaluated [[LoadedSbtFile]]. The result has
|
||||||
* 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")
|
||||||
def apply(eval: Eval, srcs: Seq[File], imports: Seq[String]): ClassLoader => LoadedSbtFile =
|
def apply(eval: Eval, srcs: Seq[File], imports: Seq[String]): ClassLoader => 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) }
|
||||||
|
|
@ -45,6 +46,8 @@ object EvaluateConfigurations {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reads a given .sbt file and evaluates it into a sequence of setting values.
|
* Reads a given .sbt file and evaluates it into a sequence of setting values.
|
||||||
|
*
|
||||||
|
* 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]): ClassLoader => Seq[Setting[_]] =
|
||||||
evaluateConfiguration(eval, src, IO.readLines(src), imports, 0)
|
evaluateConfiguration(eval, src, IO.readLines(src), imports, 0)
|
||||||
|
|
@ -92,6 +95,8 @@ object EvaluateConfigurations {
|
||||||
*/
|
*/
|
||||||
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): ClassLoader => 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.
|
||||||
val name = file.getPath
|
val name = file.getPath
|
||||||
val parsed = parseConfiguration(lines, imports, offset)
|
val parsed = parseConfiguration(lines, imports, offset)
|
||||||
val (importDefs, projects) = if (parsed.definitions.isEmpty) (Nil, (l: ClassLoader) => Nil) else {
|
val (importDefs, projects) = if (parsed.definitions.isEmpty) (Nil, (l: ClassLoader) => Nil) else {
|
||||||
|
|
@ -101,16 +106,32 @@ object EvaluateConfigurations {
|
||||||
(imp, projs)
|
(imp, projs)
|
||||||
}
|
}
|
||||||
val allImports = importDefs.map(s => (s, -1)) ++ parsed.imports
|
val allImports = importDefs.map(s => (s, -1)) ++ parsed.imports
|
||||||
val settings = parsed.settings map {
|
val dslEntries = parsed.settings map {
|
||||||
case (settingExpression, range) =>
|
case (dslExpression, range) =>
|
||||||
evaluateSetting(eval, name, allImports, settingExpression, range)
|
evaluateDslEntry(eval, name, allImports, dslExpression, range)
|
||||||
}
|
}
|
||||||
eval.unlinkDeferred()
|
eval.unlinkDeferred()
|
||||||
val loadSettings = flatten(settings)
|
loader => {
|
||||||
loader => new LoadedSbtFile(loadSettings(loader), projects(loader), importDefs)
|
val (settingsRaw, manipulationsRaw) =
|
||||||
|
dslEntries map (_ apply loader) partition {
|
||||||
|
case internals.ProjectSettings(_) => true
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
val settings = settingsRaw flatMap {
|
||||||
|
case internals.ProjectSettings(settings) => settings
|
||||||
|
case _ => Nil
|
||||||
|
}
|
||||||
|
val manipulations = manipulationsRaw map {
|
||||||
|
case internals.ProjectManipulation(f) => f
|
||||||
|
}
|
||||||
|
val ps = projects(loader)
|
||||||
|
// TODO -get project manipulations.
|
||||||
|
new LoadedSbtFile(settings, ps, importDefs, manipulations)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/** 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. */
|
||||||
private[this] def resolveBase(f: File, p: Project) = p.copy(base = IO.resolve(f, p.base))
|
private[this] def resolveBase(f: File, p: Project) = p.copy(base = IO.resolve(f, p.base))
|
||||||
|
@deprecated("Will no longer be public.", "0.13.6")
|
||||||
def flatten(mksettings: Seq[ClassLoader => Seq[Setting[_]]]): ClassLoader => Seq[Setting[_]] =
|
def flatten(mksettings: Seq[ClassLoader => Seq[Setting[_]]]): ClassLoader => Seq[Setting[_]] =
|
||||||
loader => mksettings.flatMap(_ apply loader)
|
loader => mksettings.flatMap(_ apply loader)
|
||||||
def addOffset(offset: Int, lines: Seq[(String, Int)]): Seq[(String, Int)] =
|
def addOffset(offset: Int, lines: Seq[(String, Int)]): Seq[(String, Int)] =
|
||||||
|
|
@ -122,9 +143,34 @@ object EvaluateConfigurations {
|
||||||
* The name of the class we cast DSL "setting" (vs. definition) lines to.
|
* The name of the class we cast DSL "setting" (vs. definition) lines to.
|
||||||
*/
|
*/
|
||||||
val SettingsDefinitionName = {
|
val SettingsDefinitionName = {
|
||||||
val _ = classOf[sbt.Def.SettingsDefinition] // this line exists to try to provide a compile-time error when the following line needs to be changed
|
val _ = classOf[sbt.internals.DslEntry] // this line exists to try to provide a compile-time error when the following line needs to be changed
|
||||||
"sbt.Def.SettingsDefinition"
|
"sbt.internals.DslEntry"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This actually compiles a scala expression which represents a sbt.internals.DslEntry.
|
||||||
|
*
|
||||||
|
* @param eval The mechanism to compile and evaluate Scala expressions.
|
||||||
|
* @param name The name for the thing we're compiling
|
||||||
|
* @param imports The scala imports to have in place when we compile the expression
|
||||||
|
* @param expression The scala expression we're compiling
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
private[sbt] def evaluateDslEntry(eval: Eval, name: String, imports: Seq[(String, Int)], expression: String, range: LineRange): ClassLoader => internals.DslEntry = {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This actually compiles a scala expression which represents a Seq[Setting[_]], although the
|
* This actually compiles a scala expression which represents a Seq[Setting[_]], although the
|
||||||
* expression may be just a single setting.
|
* expression may be just a single setting.
|
||||||
|
|
@ -138,16 +184,12 @@ object EvaluateConfigurations {
|
||||||
* @return A method that given an sbt classloader, can return the actual Seq[Setting[_]] defined by
|
* @return A method that given an sbt classloader, can return the actual Seq[Setting[_]] defined by
|
||||||
* the expression.
|
* the expression.
|
||||||
*/
|
*/
|
||||||
|
@deprecated("Build DSL now includes non-Setting[_] type settings.", "0.13.6")
|
||||||
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): ClassLoader => Seq[Setting[_]] =
|
||||||
{
|
{
|
||||||
val result = try {
|
evaluateDslEntry(eval, name, imports, expression, range) andThen {
|
||||||
eval.eval(expression, imports = new EvalImports(imports, name), srcName = name, tpeName = Some(SettingsDefinitionName), line = range.start)
|
case internals.ProjectSettings(values) => values
|
||||||
} catch {
|
case _ => Nil
|
||||||
case e: sbt.compiler.EvalException => throw new MessageOnlyException(e.getMessage)
|
|
||||||
}
|
|
||||||
loader => {
|
|
||||||
val pos = RangePosition(name, range shift 1)
|
|
||||||
result.getValue(loader).asInstanceOf[SettingsDefinition].settings map (_ withPos pos)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private[this] def isSpace = (c: Char) => Character isWhitespace c
|
private[this] def isSpace = (c: Char) => Character isWhitespace c
|
||||||
|
|
|
||||||
|
|
@ -411,9 +411,17 @@ object Load {
|
||||||
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))
|
||||||
|
|
||||||
|
val hasRootAlreadyDefined = defsScala.exists(_.rootProject.isDefined)
|
||||||
|
|
||||||
val memoSettings = new mutable.HashMap[File, LoadedSbtFile]
|
val memoSettings = new mutable.HashMap[File, LoadedSbtFile]
|
||||||
def loadProjects(ps: Seq[Project]) = loadTransitive(ps, normBase, plugs, () => eval, config.injectSettings, Nil, memoSettings, config.log)
|
def loadProjects(ps: Seq[Project], createRoot: Boolean) = {
|
||||||
val loadedProjectsRaw = loadProjects(initialProjects)
|
loadTransitive(ps, normBase, plugs, () => eval, config.injectSettings, Nil, memoSettings, config.log, createRoot, uri, config.pluginManagement.context)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 hasRoot = loadedProjectsRaw.exists(_.base == normBase) || defsScala.exists(_.rootProject.isDefined)
|
||||||
val (loadedProjects, defaultBuildIfNone) =
|
val (loadedProjects, defaultBuildIfNone) =
|
||||||
if (hasRoot)
|
if (hasRoot)
|
||||||
|
|
@ -423,7 +431,7 @@ object Load {
|
||||||
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))
|
val defaultProjects = loadProjects(projectsFromBuild(b, normBase), false)
|
||||||
(defaultProjects ++ loadedProjectsRaw, b)
|
(defaultProjects ++ loadedProjectsRaw, b)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -454,56 +462,153 @@ 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))
|
||||||
|
|
||||||
private[this] def loadTransitive(newProjects: Seq[Project], buildBase: File, plugins: sbt.LoadedPlugins, eval: () => Eval, injectSettings: InjectSettings,
|
/**
|
||||||
acc: Seq[Project], memoSettings: mutable.Map[File, LoadedSbtFile], log: Logger): Seq[Project] =
|
* Loads a new set of projects, including any transitively defined projects underneath this one.
|
||||||
|
*
|
||||||
|
* We have two assumptions here:
|
||||||
|
*
|
||||||
|
* 1. The first `Project` instance we encounter defines AddSettings and gets to specify where we pull other settings.
|
||||||
|
* 2. Any project manipulation (enable/disablePlugins) is ok to be added in the order we encounter it.
|
||||||
|
*
|
||||||
|
* Any further setting is ignored, as even the SettingSet API should be deprecated/removed with sbt 1.0.
|
||||||
|
*
|
||||||
|
* Note: Lots of internal details in here that shouldn't be otherwise exposed.
|
||||||
|
*
|
||||||
|
* @param newProjects A sequence of projects we have not yet loaded, but will try to. Must not be Nil
|
||||||
|
* @param buildBase The `baseDirectory` for the entire build.
|
||||||
|
* @param plugins A misnomer, this is actually the compiled BuildDefinition (classpath and such) for this project.
|
||||||
|
* @param eval A mechanism of generating an "Eval" which can compile scala code for us.
|
||||||
|
* @param injectSettings Settings we need to inject into projects.
|
||||||
|
* @param acc An accumulated list of loaded projects. TODO - how do these differ from newProjects?
|
||||||
|
* @param memoSettings A recording of all sbt files that have been loaded so far.
|
||||||
|
* @param log The logger used for this project.
|
||||||
|
* @param makeOrDiscoverRoot True if we should autogenerate a root project.
|
||||||
|
* @param buildUri The URI of the build this is loading
|
||||||
|
* @param context The plugin management context for autogenerated IDs.
|
||||||
|
*
|
||||||
|
* @return The completely resolved/updated sequence of projects defined, with all settings expanded.
|
||||||
|
*/
|
||||||
|
private[this] def loadTransitive(
|
||||||
|
newProjects: Seq[Project],
|
||||||
|
buildBase: File,
|
||||||
|
plugins: sbt.LoadedPlugins,
|
||||||
|
eval: () => Eval,
|
||||||
|
injectSettings: InjectSettings,
|
||||||
|
acc: Seq[Project],
|
||||||
|
memoSettings: mutable.Map[File, LoadedSbtFile],
|
||||||
|
log: Logger,
|
||||||
|
makeOrDiscoverRoot: Boolean,
|
||||||
|
buildUri: URI,
|
||||||
|
context: PluginManagement.Context): Seq[Project] =
|
||||||
{
|
{
|
||||||
def loadSbtFiles(auto: AddSettings, base: File, autoPlugins: Seq[AutoPlugin], projectSettings: Seq[Setting[_]]): LoadedSbtFile =
|
// load all relevant configuration files (.sbt, as .scala already exists at this point)
|
||||||
loadSettings(auto, base, plugins, eval, injectSettings, memoSettings, autoPlugins, projectSettings)
|
def discover(auto: AddSettings, base: File): DiscoveredProjects = discoverProjects(auto, base, plugins, eval, memoSettings)
|
||||||
def loadForProjects = newProjects map { project =>
|
// Step two, Finalize a project with all its settings/configuration.
|
||||||
val autoPlugins =
|
def finalizeProject(p: Project, configFiles: Seq[File]): Project = {
|
||||||
try plugins.detected.deducePlugins(project.plugins, log)
|
val loadedFiles = configFiles flatMap { f => memoSettings.get(f) }
|
||||||
catch { case e: AutoPluginException => throw translateAutoPluginException(e, project) }
|
resolveProject(p, loadedFiles, plugins, injectSettings, memoSettings, log)
|
||||||
val autoConfigs = autoPlugins.flatMap(_.projectConfigurations)
|
|
||||||
val loadedSbtFiles = loadSbtFiles(project.auto, project.base, autoPlugins, project.settings)
|
|
||||||
// add the automatically selected settings, record the selected AutoPlugins, and register the automatically selected configurations
|
|
||||||
val transformed = project.copy(settings = loadedSbtFiles.settings).setAutoPlugins(autoPlugins).prefixConfigs(autoConfigs: _*)
|
|
||||||
(transformed, loadedSbtFiles.projects)
|
|
||||||
}
|
}
|
||||||
def defaultLoad = loadSbtFiles(AddSettings.defaultSbtFiles, buildBase, Nil, Nil).projects
|
// Discover any new project definition for the base directory of this project, and load all settings.
|
||||||
val (nextProjects, loadedProjects) =
|
// Also return any newly discovered project instances.
|
||||||
if (newProjects.isEmpty) // load the .sbt files in the root directory to look for Projects
|
def discoverAndLoad(p: Project): (Project, Seq[Project]) = {
|
||||||
(defaultLoad, acc)
|
val (root, discovered, files) = discover(p.auto, p.base) match {
|
||||||
else {
|
case DiscoveredProjects(Some(root), rest, files) =>
|
||||||
val (transformed, np) = loadForProjects.unzip
|
// TODO - We assume here the project defined in a build.sbt WINS because the original was
|
||||||
(np.flatten, transformed ++ acc)
|
// 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)
|
||||||
}
|
}
|
||||||
|
val finalRoot = finalizeProject(root, files)
|
||||||
if (nextProjects.isEmpty)
|
finalRoot -> discovered
|
||||||
loadedProjects
|
}
|
||||||
else
|
// Load all config files AND finalize the project at the root directory, if it exists.
|
||||||
loadTransitive(nextProjects, buildBase, plugins, eval, injectSettings, loadedProjects, memoSettings, log)
|
// 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)
|
||||||
|
case Nil if makeOrDiscoverRoot =>
|
||||||
|
log.debug(s"[Loading] Scanning directory ${buildBase}")
|
||||||
|
// TODO - Here we want to fully discover everything and make a default build...
|
||||||
|
discover(AddSettings.defaultSbtFiles, buildBase) match {
|
||||||
|
case DiscoveredProjects(Some(root), discovered, files) =>
|
||||||
|
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, acc :+ finalRoot, memoSettings, log, false, buildUri, context)
|
||||||
|
// Here we need to create a root project...
|
||||||
|
case DiscoveredProjects(None, discovered, files) =>
|
||||||
|
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 refs = existingIds map (id => ProjectRef(buildUri, id))
|
||||||
|
val defaultID = autoID(buildBase, context, existingIds)
|
||||||
|
val root = finalizeProject(Build.defaultAggregatedProject(defaultID, buildBase, refs), files)
|
||||||
|
val result = (acc ++ otherProjects) :+ root
|
||||||
|
log.debug(s"[Loading] Done in ${buildBase}, returning: ${result.map(_.id).mkString("(", ", ", ")")}")
|
||||||
|
result
|
||||||
|
}
|
||||||
|
case Nil =>
|
||||||
|
log.debug(s"[Loading] Done in ${buildBase}, returning: ${acc.map(_.id).mkString("(", ", ", ")")}")
|
||||||
|
acc
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private[this] def translateAutoPluginException(e: AutoPluginException, project: Project): AutoPluginException =
|
private[this] def translateAutoPluginException(e: AutoPluginException, project: Project): AutoPluginException =
|
||||||
e.withPrefix(s"Error determining plugins for project '${project.id}' in ${project.base}:\n")
|
e.withPrefix(s"Error determining plugins for project '${project.id}' in ${project.base}:\n")
|
||||||
|
|
||||||
private[this] def loadSettings(auto: AddSettings, projectBase: File, loadedPlugins: sbt.LoadedPlugins, eval: () => Eval, injectSettings: InjectSettings, memoSettings: mutable.Map[File, LoadedSbtFile], autoPlugins: Seq[AutoPlugin], buildScalaFiles: Seq[Setting[_]]): LoadedSbtFile =
|
/**
|
||||||
{
|
* Represents the results of flushing out a directory and discovering all the projects underneath it.
|
||||||
lazy val defaultSbtFiles = configurationSources(projectBase)
|
* THis will return one completely loaded project, and any newly discovered (and unloaded) projects.
|
||||||
def settings(ss: Seq[Setting[_]]) = new LoadedSbtFile(ss, Nil, Nil)
|
*
|
||||||
val loader = loadedPlugins.loader
|
* @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).
|
||||||
|
*/
|
||||||
|
private[this] case class DiscoveredProjects(root: Option[Project], nonRoot: Seq[Project], sbtFiles: Seq[File])
|
||||||
|
|
||||||
def merge(ls: Seq[LoadedSbtFile]): LoadedSbtFile = (LoadedSbtFile.empty /: ls) { _ merge _ }
|
/**
|
||||||
def loadSettings(fs: Seq[File]): LoadedSbtFile =
|
* This method attempts to resolve/apply all configuration loaded for a project. It is responsible for the following:
|
||||||
merge(fs.sortBy(_.getName).map(memoLoadSettingsFile))
|
*
|
||||||
def memoLoadSettingsFile(src: File): LoadedSbtFile = memoSettings.get(src) getOrElse {
|
* 1. Apply any manipulations defined in .sbt files.
|
||||||
val lf = loadSettingsFile(src)
|
* 2. Detecting which autoPlugins are enabled for the project.
|
||||||
memoSettings.put(src, lf.clearProjects) // don't load projects twice
|
* 3. Ordering all Setting[_]s for the project
|
||||||
lf
|
*
|
||||||
|
*
|
||||||
|
* @param rawProject The original project, with nothing manipulated since it was evaluated/discovered.
|
||||||
|
* @param configFiles All configuration files loaded for this project. Used to discover project manipulations
|
||||||
|
* @param loadedPlugins The project definition (and classloader) of the build.
|
||||||
|
* @param globalUserSettings All the settings contributed from the ~/.sbt/<version> directory
|
||||||
|
* @param memoSettings A recording of all loaded files (our files should reside in there). We should need not load any
|
||||||
|
* sbt file to resolve a project.
|
||||||
|
* @param log A logger to report auto-plugin issues to.
|
||||||
|
*/
|
||||||
|
private[this] def resolveProject(
|
||||||
|
rawProject: Project,
|
||||||
|
configFiles: Seq[LoadedSbtFile],
|
||||||
|
loadedPlugins: sbt.LoadedPlugins,
|
||||||
|
globalUserSettings: InjectSettings,
|
||||||
|
memoSettings: mutable.Map[File, LoadedSbtFile],
|
||||||
|
log: Logger): Project = {
|
||||||
|
import AddSettings._
|
||||||
|
// 1. Apply all the project manipulations from .sbt files in order
|
||||||
|
val transformedProject =
|
||||||
|
configFiles.flatMap(_.manipulations).foldLeft(rawProject) { (prev, t) =>
|
||||||
|
t(prev)
|
||||||
}
|
}
|
||||||
def loadSettingsFile(src: File): LoadedSbtFile =
|
// 2. Discover all the autoplugins and contributed configurations.
|
||||||
EvaluateConfigurations.evaluateSbtFile(eval(), src, IO.readLines(src), loadedPlugins.detected.imports, 0)(loader)
|
val autoPlugins =
|
||||||
|
try loadedPlugins.detected.deducePlugins(transformedProject.plugins, log)
|
||||||
|
catch { case e: AutoPluginException => throw translateAutoPluginException(e, transformedProject) }
|
||||||
|
val autoConfigs = autoPlugins.flatMap(_.projectConfigurations)
|
||||||
|
|
||||||
import AddSettings.{ User, SbtFiles, DefaultSbtFiles, Plugins, AutoPlugins, Sequence, BuildScalaFiles }
|
// 3. Use AddSettings instance to order all Setting[_]s appropriately
|
||||||
|
val allSettings = {
|
||||||
|
// TODO - This mechanism of applying settings could be off... It's in two places now...
|
||||||
|
lazy val defaultSbtFiles = configurationSources(transformedProject.base)
|
||||||
|
// Grabs the plugin settings for old-style sbt plugins.
|
||||||
def pluginSettings(f: Plugins) = {
|
def pluginSettings(f: Plugins) = {
|
||||||
val included = loadedPlugins.detected.plugins.values.filter(f.include) // don't apply the filter to AutoPlugins, only Plugins
|
val included = loadedPlugins.detected.plugins.values.filter(f.include) // don't apply the filter to AutoPlugins, only Plugins
|
||||||
included.flatMap(p => p.settings.filter(isProjectThis) ++ p.projectSettings)
|
included.flatMap(p => p.settings.filter(isProjectThis) ++ p.projectSettings)
|
||||||
|
|
@ -512,18 +617,76 @@ object Load {
|
||||||
// intended in the AddSettings.AutoPlugins filter.
|
// intended in the AddSettings.AutoPlugins filter.
|
||||||
def autoPluginSettings(f: AutoPlugins) =
|
def autoPluginSettings(f: AutoPlugins) =
|
||||||
autoPlugins.filter(f.include).flatMap(_.projectSettings)
|
autoPlugins.filter(f.include).flatMap(_.projectSettings)
|
||||||
|
// Grab all the settigns we already loaded from sbt files
|
||||||
def expand(auto: AddSettings): LoadedSbtFile = auto match {
|
def settings(files: Seq[File]): Seq[Setting[_]] =
|
||||||
case BuildScalaFiles => settings(buildScalaFiles)
|
for {
|
||||||
case User => settings(injectSettings.projectLoaded(loader))
|
file <- files
|
||||||
case sf: SbtFiles => loadSettings(sf.files.map(f => IO.resolve(projectBase, f)))
|
config <- (memoSettings get file).toSeq
|
||||||
case sf: DefaultSbtFiles => loadSettings(defaultSbtFiles.filter(sf.include))
|
setting <- config.settings
|
||||||
case p: Plugins => settings(pluginSettings(p))
|
} yield setting
|
||||||
case p: AutoPlugins => settings(autoPluginSettings(p))
|
// Expand the AddSettings instance into a real Seq[Setting[_]] we'll use on the project
|
||||||
case q: Sequence => (LoadedSbtFile.empty /: q.sequence) { (b, add) => b.merge(expand(add)) }
|
def expandSettings(auto: AddSettings): Seq[Setting[_]] = auto match {
|
||||||
|
case BuildScalaFiles => rawProject.settings
|
||||||
|
case User => globalUserSettings.projectLoaded(loadedPlugins.loader)
|
||||||
|
case sf: SbtFiles => settings(sf.files.map(f => IO.resolve(rawProject.base, f)))
|
||||||
|
case sf: DefaultSbtFiles => settings(defaultSbtFiles.filter(sf.include))
|
||||||
|
case p: Plugins => pluginSettings(p)
|
||||||
|
case p: AutoPlugins => autoPluginSettings(p)
|
||||||
|
case q: Sequence => (Seq.empty[Setting[_]] /: q.sequence) { (b, add) => b ++ expandSettings(add) }
|
||||||
}
|
}
|
||||||
expand(auto)
|
expandSettings(transformedProject.auto)
|
||||||
}
|
}
|
||||||
|
// Finally, a project we can use in buildStructure.
|
||||||
|
transformedProject.copy(settings = allSettings).setAutoPlugins(autoPlugins).prefixConfigs(autoConfigs: _*)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method attempts to discover all Project/settings it can using the configured AddSettings and project base.
|
||||||
|
*
|
||||||
|
* @param auto The AddSettings of the defining project (or default) we use to determine which build.sbt files to read.
|
||||||
|
* @param projectBase The directory we're currently loading projects/definitions from.
|
||||||
|
* @param eval A mechanism of executing/running scala code.
|
||||||
|
* @param memoSettings A recording of all files we've parsed.
|
||||||
|
*/
|
||||||
|
private[this] def discoverProjects(
|
||||||
|
auto: AddSettings,
|
||||||
|
projectBase: File,
|
||||||
|
loadedPlugins: sbt.LoadedPlugins,
|
||||||
|
eval: () => Eval,
|
||||||
|
memoSettings: mutable.Map[File, LoadedSbtFile]): DiscoveredProjects = {
|
||||||
|
// Default sbt files to read, if needed
|
||||||
|
lazy val defaultSbtFiles = configurationSources(projectBase)
|
||||||
|
// Classloader of the build
|
||||||
|
val loader = loadedPlugins.loader
|
||||||
|
// How to load an individual file for use later.
|
||||||
|
def loadSettingsFile(src: File): LoadedSbtFile =
|
||||||
|
EvaluateConfigurations.evaluateSbtFile(eval(), src, IO.readLines(src), loadedPlugins.detected.imports, 0)(loader)
|
||||||
|
// How to merge SbtFiles we read into one thing
|
||||||
|
def merge(ls: Seq[LoadedSbtFile]): LoadedSbtFile = (LoadedSbtFile.empty /: ls) { _ merge _ }
|
||||||
|
// Loads a given file, or pulls from the cache.
|
||||||
|
def memoLoadSettingsFile(src: File): LoadedSbtFile = memoSettings.get(src) getOrElse {
|
||||||
|
val lf = loadSettingsFile(src)
|
||||||
|
memoSettings.put(src, lf.clearProjects) // don't load projects twice
|
||||||
|
lf
|
||||||
|
}
|
||||||
|
// Loads a set of sbt files, sorted by their lexical name (current behavior of sbt).
|
||||||
|
def loadFiles(fs: Seq[File]): LoadedSbtFile =
|
||||||
|
merge(fs.sortBy(_.getName).map(memoLoadSettingsFile))
|
||||||
|
|
||||||
|
// Finds all the build files associated with this project
|
||||||
|
import AddSettings.{ User, SbtFiles, DefaultSbtFiles, Plugins, AutoPlugins, Sequence, BuildScalaFiles }
|
||||||
|
def associatedFiles(auto: AddSettings): Seq[File] = auto match {
|
||||||
|
case sf: SbtFiles => sf.files.map(f => IO.resolve(projectBase, f))
|
||||||
|
case sf: DefaultSbtFiles => defaultSbtFiles.filter(sf.include)
|
||||||
|
case q: Sequence => (Seq.empty[File] /: q.sequence) { (b, add) => b ++ associatedFiles(add) }
|
||||||
|
case _ => Seq.empty
|
||||||
|
}
|
||||||
|
val rawFiles = associatedFiles(auto)
|
||||||
|
val rawProjects = loadFiles(rawFiles).projects
|
||||||
|
val (root, nonRoot) = rawProjects.partition(_.base == projectBase)
|
||||||
|
// TODO - good error message if more than one root project
|
||||||
|
DiscoveredProjects(root.headOption, nonRoot, rawFiles)
|
||||||
|
}
|
||||||
|
|
||||||
@deprecated("No longer used.", "0.13.0")
|
@deprecated("No longer used.", "0.13.0")
|
||||||
def globalPluginClasspath(globalPlugin: Option[GlobalPlugin]): Seq[Attributed[File]] =
|
def globalPluginClasspath(globalPlugin: Option[GlobalPlugin]): Seq[Attributed[File]] =
|
||||||
|
|
@ -531,6 +694,7 @@ object Load {
|
||||||
case Some(cp) => cp.data.fullClasspath
|
case Some(cp) => cp.data.fullClasspath
|
||||||
case None => Nil
|
case None => Nil
|
||||||
}
|
}
|
||||||
|
/** These are the settings defined when loading a project "meta" build. */
|
||||||
val autoPluginSettings: Seq[Setting[_]] = inScope(GlobalScope in LocalRootProject)(Seq(
|
val autoPluginSettings: Seq[Setting[_]] = inScope(GlobalScope in LocalRootProject)(Seq(
|
||||||
sbtPlugin :== true,
|
sbtPlugin :== true,
|
||||||
pluginData := {
|
pluginData := {
|
||||||
|
|
@ -725,10 +889,19 @@ object Load {
|
||||||
def buildUtil(root: URI, units: Map[URI, sbt.LoadedBuildUnit], keyIndex: KeyIndex, data: Settings[Scope]): BuildUtil[ResolvedProject] = BuildUtil(root, units, keyIndex, data)
|
def buildUtil(root: URI, units: Map[URI, sbt.LoadedBuildUnit], keyIndex: KeyIndex, data: Settings[Scope]): BuildUtil[ResolvedProject] = BuildUtil(root, units, keyIndex, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
final case class LoadBuildConfiguration(stagingDirectory: File, classpath: Seq[Attributed[File]], loader: ClassLoader,
|
final case class LoadBuildConfiguration(
|
||||||
compilers: Compilers, evalPluginDef: (sbt.BuildStructure, State) => PluginData, definesClass: DefinesClass,
|
stagingDirectory: File,
|
||||||
delegates: sbt.LoadedBuild => Scope => Seq[Scope], scopeLocal: ScopeLocal,
|
classpath: Seq[Attributed[File]],
|
||||||
pluginManagement: PluginManagement, injectSettings: Load.InjectSettings, globalPlugin: Option[GlobalPlugin], extraBuilds: Seq[URI],
|
loader: ClassLoader,
|
||||||
|
compilers: Compilers,
|
||||||
|
evalPluginDef: (sbt.BuildStructure, State) => PluginData,
|
||||||
|
definesClass: DefinesClass,
|
||||||
|
delegates: sbt.LoadedBuild => Scope => Seq[Scope],
|
||||||
|
scopeLocal: ScopeLocal,
|
||||||
|
pluginManagement: PluginManagement,
|
||||||
|
injectSettings: Load.InjectSettings,
|
||||||
|
globalPlugin: Option[GlobalPlugin],
|
||||||
|
extraBuilds: Seq[URI],
|
||||||
log: Logger) {
|
log: Logger) {
|
||||||
@deprecated("Use `classpath`.", "0.13.0")
|
@deprecated("Use `classpath`.", "0.13.0")
|
||||||
lazy val globalPluginClasspath = classpath
|
lazy val globalPluginClasspath = classpath
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,19 @@ import Def.Setting
|
||||||
* Represents the exported contents of a .sbt file. Currently, that includes the list of settings,
|
* Represents the exported contents of a .sbt file. Currently, that includes the list of settings,
|
||||||
* the values of Project vals, and the import statements for all defined vals/defs.
|
* the values of Project vals, and the import statements for all defined vals/defs.
|
||||||
*/
|
*/
|
||||||
private[sbt] final class LoadedSbtFile(val settings: Seq[Setting[_]], val projects: Seq[Project], val importedDefs: Seq[String]) {
|
private[sbt] final class LoadedSbtFile(
|
||||||
|
val settings: Seq[Setting[_]],
|
||||||
|
val projects: Seq[Project],
|
||||||
|
val importedDefs: Seq[String],
|
||||||
|
val manipulations: Seq[Project => Project]) {
|
||||||
|
@deprecated("LoadedSbtFiles are no longer directly merged.", "0.13.6")
|
||||||
def merge(o: LoadedSbtFile): LoadedSbtFile =
|
def merge(o: LoadedSbtFile): LoadedSbtFile =
|
||||||
new LoadedSbtFile(settings ++ o.settings, projects ++ o.projects, importedDefs ++ o.importedDefs)
|
new LoadedSbtFile(settings ++ o.settings, projects ++ o.projects, importedDefs ++ o.importedDefs, manipulations)
|
||||||
def clearProjects = new LoadedSbtFile(settings, Nil, importedDefs)
|
|
||||||
|
def clearProjects = new LoadedSbtFile(settings, Nil, importedDefs, manipulations)
|
||||||
}
|
}
|
||||||
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)
|
def empty = new LoadedSbtFile(Nil, Nil, Nil, Nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
package sbt
|
||||||
|
|
||||||
|
import internals.{
|
||||||
|
DslEntry,
|
||||||
|
DslSetting,
|
||||||
|
DslEnablePlugins,
|
||||||
|
DslDisablePlugins
|
||||||
|
}
|
||||||
|
|
||||||
|
package object dsl {
|
||||||
|
def enablePlugins(ps: AutoPlugin*): DslEntry = DslEnablePlugins(ps)
|
||||||
|
def disablePlugins(ps: AutoPlugin*): DslEntry = DslDisablePlugins(ps)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
package sbt
|
||||||
|
package internals
|
||||||
|
|
||||||
|
import Def._
|
||||||
|
|
||||||
|
/** This reprsents a `Setting` expression configured by the sbt DSL. */
|
||||||
|
sealed trait DslEntry {
|
||||||
|
/** Called by the parser. Sets the position where this entry was defined in the build.sbt file. */
|
||||||
|
def withPos(pos: RangePosition): DslEntry
|
||||||
|
}
|
||||||
|
object DslEntry {
|
||||||
|
implicit def fromSettingsDef(inc: SettingsDefinition): DslEntry =
|
||||||
|
DslSetting(inc)
|
||||||
|
implicit def fromSettingsDef(inc: Seq[Setting[_]]): DslEntry =
|
||||||
|
DslSetting(inc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents a DSL entry which adds settings to the current project. */
|
||||||
|
sealed trait ProjectSettings extends DslEntry {
|
||||||
|
def toSettings: Seq[Setting[_]]
|
||||||
|
}
|
||||||
|
object ProjectSettings {
|
||||||
|
def unapply(e: DslEntry): Option[Seq[Setting[_]]] =
|
||||||
|
e match {
|
||||||
|
case e: ProjectSettings => Some(e.toSettings)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Represents a DSL entry which manipulates the current project. */
|
||||||
|
sealed trait ProjectManipulation extends DslEntry {
|
||||||
|
def toFunction: Project => Project
|
||||||
|
// TODO - Should we store this?
|
||||||
|
final def withPos(pos: RangePosition): DslEntry = this
|
||||||
|
}
|
||||||
|
object ProjectManipulation {
|
||||||
|
def unapply(e: DslEntry): Option[Project => Project] =
|
||||||
|
e match {
|
||||||
|
case e: ProjectManipulation => Some(e.toFunction)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** this represents an actually Setting[_] or Seq[Setting[_]] configured by the sbt DSL. */
|
||||||
|
case class DslSetting(settings: SettingsDefinition) extends ProjectSettings {
|
||||||
|
def toSettings = settings.settings
|
||||||
|
final def withPos(pos: RangePosition): DslEntry = DslSetting(settings.settings.map(_.withPos(pos)))
|
||||||
|
}
|
||||||
|
/** this represents an `enablePlugins()` in the sbt DSL */
|
||||||
|
case class DslEnablePlugins(plugins: Seq[AutoPlugin]) extends ProjectManipulation {
|
||||||
|
override val toFunction: Project => Project = _.enablePlugins(plugins: _*)
|
||||||
|
}
|
||||||
|
/** this represents an `disablePlugins()` in the sbt DSL */
|
||||||
|
case class DslDisablePlugins(plugins: Seq[AutoPlugin]) extends ProjectManipulation {
|
||||||
|
override val toFunction: Project => Project = _.disablePlugins(plugins: _*)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -13,7 +13,20 @@ lazy val projD = project
|
||||||
// with S selected, Q is loaded automatically, which in turn selects R
|
// with S selected, Q is loaded automatically, which in turn selects R
|
||||||
lazy val projE = project.enablePlugins(S)
|
lazy val projE = project.enablePlugins(S)
|
||||||
|
|
||||||
|
lazy val projF = project
|
||||||
|
|
||||||
|
disablePlugins(plugins.IvyPlugin)
|
||||||
|
|
||||||
check := {
|
check := {
|
||||||
|
// TODO - this will pass when the raw disablePlugin works.
|
||||||
|
val dversion = (projectID in projD).?.value // Should be None
|
||||||
|
same(dversion, None, "projectID in projD")
|
||||||
|
val rversion = projectID.?.value // Should be None
|
||||||
|
same(rversion, None, "projectID")
|
||||||
|
// Ensure with multiple .sbt files that disabling/enabling works across them
|
||||||
|
val fDel = (del in q in projF).?.value
|
||||||
|
same(fDel, Some(" Q"), "del in q in projF")
|
||||||
|
//
|
||||||
val adel = (del in projA).?.value // should be None
|
val adel = (del in projA).?.value // should be None
|
||||||
same(adel, None, "del in projA")
|
same(adel, None, "del in projA")
|
||||||
val bdel = (del in projB).?.value // should be None
|
val bdel = (del in projB).?.value // should be None
|
||||||
|
|
@ -25,10 +38,10 @@ check := {
|
||||||
same(buildValue, "build 0", "demo in ThisBuild")
|
same(buildValue, "build 0", "demo in ThisBuild")
|
||||||
val globalValue = (demo in Global).value
|
val globalValue = (demo in Global).value
|
||||||
same(globalValue, "global 0", "demo in Global")
|
same(globalValue, "global 0", "demo in Global")
|
||||||
val projValue = (demo in projC).value
|
val projValue = (demo in projC).?.value
|
||||||
same(projValue, "project projC Q R", "demo in projC")
|
same(projValue, Some("project projC Q R"), "demo in projC")
|
||||||
val qValue = (del in projC in q).value
|
val qValue = (del in projC in q).?.value
|
||||||
same(qValue, " Q R", "del in projC in q")
|
same(qValue, Some(" Q R"), "del in projC in q")
|
||||||
val optInValue = (del in projE in q).value
|
val optInValue = (del in projE in q).value
|
||||||
same(optInValue, " Q S R", "del in projE in q")
|
same(optInValue, " Q S R", "del in projE in q")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
disablePlugins(plugins.IvyPlugin)
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import sbttest.Imports._
|
||||||
|
|
||||||
|
enablePlugins(A, B)
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
import sbttest.S
|
||||||
|
|
||||||
|
disablePlugins(R)
|
||||||
|
|
@ -16,7 +16,7 @@ object Imports
|
||||||
lazy val demo = settingKey[String]("A demo setting.")
|
lazy val demo = settingKey[String]("A demo setting.")
|
||||||
lazy val del = settingKey[String]("Another demo setting.")
|
lazy val del = settingKey[String]("Another demo setting.")
|
||||||
|
|
||||||
lazy val check = settingKey[Unit]("Verifies settings are as they should be.")
|
lazy val check = taskKey[Unit]("Verifies settings are as they should be.")
|
||||||
}
|
}
|
||||||
|
|
||||||
object X extends AutoPlugin {
|
object X extends AutoPlugin {
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
|
> plugins
|
||||||
> check
|
> check
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue