From ee5374c77bb3238167bb374045eb098d02c02d3a Mon Sep 17 00:00:00 2001 From: azdrojowa123 <33667003+azdrojowa123@users.noreply.github.com> Date: Mon, 24 Aug 2026 21:38:34 +0200 Subject: [PATCH] [2.0.x] fix: Fixes --addPluginSbtFile getting lost after reboot (#9669) reboot restarts sbt with a fresh state, so the extra plugin sbt files registered in BasicKeys.extraMetaSbtFiles were dropped and their plugins disappeared. Prepend an early(addPluginSbtFile=) command for each registered file to the arguments handed to the restarted sbt, so they are re-registered. --------- Co-authored-by: Claude Opus 5 (1M context) --- main-command/src/main/scala/sbt/State.scala | 35 ++++++++++++-- .../sbt-test/global-plugin/global-plugin/test | 6 +++ .../project/addPluginSbtFile-reboot/build.sbt | 46 +++++++++++++++++++ .../temp with spaces/extraC.sbt | 1 + .../addPluginSbtFile-reboot/temp/extraA.sbt | 1 + .../addPluginSbtFile-reboot/temp/extraB.sbt | 1 + .../project/addPluginSbtFile-reboot/test | 43 +++++++++++++++++ 7 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/build.sbt create mode 100644 sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp with spaces/extraC.sbt create mode 100644 sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp/extraA.sbt create mode 100644 sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp/extraB.sbt create mode 100644 sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/test diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index b1afeb374..ec575bc9a 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -248,12 +248,40 @@ object State { val app = state.configuration.provider new Reboot( app.scalaProvider.version, - state.remainingCommands map { case e: Exec => e.commandLine }, + addPluginSbtFileArguments(state) ::: state.remainingCommands.map(_.commandLine), app.id, state.configuration.baseDirectory ) } + /** + * Builds the `early(...)` commands that add the extra plugin sbt files back after a reboot. + * A reboot clears the state, so sbt forgets these files. The commands are passed to the new + * sbt as arguments, and they run before it loads the build. + * + * The `early(addPluginSbtFile=...)` form is used, not `--addPluginSbtFile=...`, because the + * `--` form loses the quotes around the path, and then a path with a space in it fails. + * A path with a space can be added using `addPluginSbtFile ""`. + */ + private[sbt] def addPluginSbtFileArguments(state: State): List[String] = + state.get(BasicKeys.extraMetaSbtFiles).toList.flatten.distinct.map { vf => + val path = vf match { + case f: xsbti.PathBasedFile => f.toPath.toString + case f => f.id + } + val command = s"${BasicCommandStrings.AddPluginSbtFileCommand}=${quote(path)}" + s"${BasicCommandStrings.EarlyCommand}($command)" + } + + /** + * Puts `path` in quotes so the new sbt session reads it back as one whole string. There the path goes + * through the `Parsers.StringBasic` in `BasicCommands.addPluginSbtFileParser`, which stops at + * the first space when there are no quotes. A path with a space can be added, so it also has + * to come back after a reboot. + */ + private def quote(path: String): String = + s"\"${path.replace("\\", "\\\\").replace("\"", "\\\"")}\"" + @deprecated("Import State._ or State.StateOpsImpl to access state extension methods", "1.3.0") def stateOps(s: State): StateOps = new StateOpsImpl(s) @@ -331,8 +359,9 @@ object State { StartServer :: remaining.dropWhile(!_.startsWith(ReportResult)).tail ::: "shell" :: Nil case _ => remaining } - if (currentOnly) throw new RebootCurrent(fullRemaining) - else throw new xsbti.FullReload(fullRemaining.toArray, full) + val arguments = State.addPluginSbtFileArguments(s) ::: fullRemaining + if (currentOnly) throw new RebootCurrent(arguments) + else throw new xsbti.FullReload(arguments.toArray, full) } def reload = runExitHooks().setNext(new Return(defaultReload(s))) diff --git a/sbt-app/src/sbt-test/global-plugin/global-plugin/test b/sbt-app/src/sbt-test/global-plugin/global-plugin/test index ae785f180..bd79c7bb5 100644 --- a/sbt-app/src/sbt-test/global-plugin/global-plugin/test +++ b/sbt-app/src/sbt-test/global-plugin/global-plugin/test @@ -13,3 +13,9 @@ $ copy-file changes/global-plugins.sbt global/plugins/plugins.sbt $ copy-file changes/plugins.sbt project/plugins.sbt > reload > check + +# The tests of one scripted run share a directory, and `global` is the part of it that is kept between +# them, so delete what this test installed there. +$ delete global/plugins global/useGlobalAutoPlugin.sbt +$ absent global/plugins global/useGlobalAutoPlugin.sbt +> reload diff --git a/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/build.sbt b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/build.sbt new file mode 100644 index 000000000..5a1a82fcf --- /dev/null +++ b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/build.sbt @@ -0,0 +1,46 @@ +import sbt.internal.LoadedBuild + +lazy val root = project.in(file(".")) + +def detectedPlugins(lb: LoadedBuild): Seq[String] = + lb.units(lb.root).unit.plugins.detected.autoPlugins.map(_.name) + +InputKey[Unit]("checkPlugins") := { + val args = Def.spaceDelimited("").parsed + val detected = detectedPlugins(loadedBuild.value) + args.foreach { name => + assert( + detected.exists(_.contains(name)), + s"expected plugin $name to be detected, got: ${detected.mkString(", ")}" + ) + } +} + +InputKey[Unit]("checkPluginsAbsent") := { + val args = Def.spaceDelimited("").parsed + val detected = detectedPlugins(loadedBuild.value) + args.foreach { name => + assert( + !detected.exists(_.contains(name)), + s"expected plugin $name not to be detected, got: ${detected.mkString(", ")}" + ) + } +} + +// Compares the whole list of registered paths, in order. This also catches a path that comes +// back from a reboot changed, doubled or in a different place, not only one that is lost. +// With no arguments it asserts that no file is registered. +InputKey[Unit]("checkFiles") := { + val expected = Def.spaceDelimited("").parsed.toList + val actual = state.value.get(BasicKeys.extraMetaSbtFiles).toList.flatten.map(_.id) + assert( + actual == expected, + s"expected registered files to be [${expected.mkString(", ")}], got: [${actual.mkString(", ")}]" + ) +} + +// Tests in a scripted group share one sbt session, and the extra files now survive reboot on +// purpose, so they have to be dropped before the next test. +commands += Command.command("clearExtraPluginSbtFiles") { s => + s.remove(BasicKeys.extraMetaSbtFiles) +} diff --git a/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp with spaces/extraC.sbt b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp with spaces/extraC.sbt new file mode 100644 index 000000000..ecbbaada7 --- /dev/null +++ b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp with spaces/extraC.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.11") diff --git a/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp/extraA.sbt b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp/extraA.sbt new file mode 100644 index 000000000..ddfa827f9 --- /dev/null +++ b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp/extraA.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") diff --git a/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp/extraB.sbt b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp/extraB.sbt new file mode 100644 index 000000000..fdcd4e64d --- /dev/null +++ b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/temp/extraB.sbt @@ -0,0 +1 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.22.0") diff --git a/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/test b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/test new file mode 100644 index 000000000..42f3eada7 --- /dev/null +++ b/sbt-app/src/sbt-test/project/addPluginSbtFile-reboot/test @@ -0,0 +1,43 @@ +# Regression test for sbt/sbt#4303: files added with --addPluginSbtFile must survive reboot. + +# Neither plugin is on the meta-build classpath to begin with, and nothing is registered. +> checkPluginsAbsent BuildInfoPlugin ScalaJSPlugin ScalaNativePlugin +> checkFiles + +# Add the first extra plugin sbt file and check that its plugin is picked up. +> early(addPluginSbtFile=temp/extraA.sbt); reload +> checkPlugins BuildInfoPlugin +> checkFiles temp/extraA.sbt + +# The first reboot must not lose it. +> reboot +> checkPlugins BuildInfoPlugin +> checkFiles temp/extraA.sbt + +# Add a second extra plugin sbt file on top of the first one. +> early(addPluginSbtFile=temp/extraB.sbt); reload +> checkPlugins BuildInfoPlugin ScalaJSPlugin +> checkFiles temp/extraA.sbt temp/extraB.sbt + +# The second reboot must keep both of them, and must not add either of them twice. +> reboot +> checkPlugins BuildInfoPlugin ScalaJSPlugin +> checkFiles temp/extraA.sbt temp/extraB.sbt + +# A third file, this one in a directory whose name contains spaces. The path has to be quoted +# for sbt to parse it as one argument, and the reboot has to keep it quoted to replay it. +> addPluginSbtFile "temp with spaces/extraC.sbt" +> reload +> checkPlugins BuildInfoPlugin ScalaJSPlugin ScalaNativePlugin +> checkFiles temp/extraA.sbt temp/extraB.sbt "temp with spaces/extraC.sbt" + +# The third reboot must keep all three, in order, with the spaced path intact. +> reboot +> checkPlugins BuildInfoPlugin ScalaJSPlugin ScalaNativePlugin +> checkFiles temp/extraA.sbt temp/extraB.sbt "temp with spaces/extraC.sbt" + +# Drop the extra files, because the sbt session can be shared with the other scripted tests. +> clearExtraPluginSbtFiles +> reload +> checkPluginsAbsent BuildInfoPlugin ScalaJSPlugin ScalaNativePlugin +> checkFiles