From 4976ec7dc7c4e6a25f877659751bfaff7b3f3519 Mon Sep 17 00:00:00 2001 From: Francluob Date: Thu, 8 Jan 2026 17:57:54 +0100 Subject: [PATCH] Fix #8344: Skip interactive prompt in batch mode when project loading fails When project loading fails in batch mode, sbt was showing an interactive prompt asking the user to choose between retry, quit, last, or ignore. However, in batch mode there is no interactive terminal, causing the process to hang waiting for input that will never come. This fix checks if we're in batch mode (Prompt.Batch) and automatically exits with failure (equivalent to 'q' quit option) without prompting the user. This prevents infinite retry loops on persistent errors and allows batch mode scripts to fail fast, which is appropriate for CI/CD environments. The interactive behavior remains unchanged for non-batch mode. --- main/src/main/scala/sbt/Main.scala | 41 +++++++++++++++++------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index dad3b8e04..4db35148f 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -869,25 +869,30 @@ object BuiltinCommands { @tailrec private def doLoadFailed(s: State, loadArg: String): State = { - s.log.warn("Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)") - val result: Int = - try - ITerminal.get.withRawInput(System.in.read) match { - case -1 => 'q'.toInt - case b => b - } - catch { case _: ClosedChannelException => 'q' } - def retry: State = loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog - def ignoreMsg: String = - if (Project.isProjectLoaded(s)) "using previously loaded project" else "no project loaded" + // In batch mode, exit with failure to avoid infinite retry loops on persistent errors + if (ITerminal.console.prompt == Prompt.Batch) { + s.exit(ok = false) + } else { + s.log.warn("Project loading failed: (r)etry, (q)uit, (l)ast, or (i)gnore? (default: r)") + val result: Int = + try + ITerminal.get.withRawInput(System.in.read) match { + case -1 => 'q'.toInt + case b => b + } + catch { case _: ClosedChannelException => 'q' } + def retry: State = loadProjectCommand(LoadProject, loadArg) :: s.clearGlobalLog + def ignoreMsg: String = + if (Project.isProjectLoaded(s)) "using previously loaded project" else "no project loaded" - result.toChar match { - case '\n' | '\r' => retry - case 'r' | 'R' => retry - case 'q' | 'Q' => s.exit(ok = false) - case 'i' | 'I' => s.log.warn(s"Ignoring load failure: $ignoreMsg."); s - case 'l' | 'L' => LastCommand :: loadProjectCommand(LoadFailed, loadArg) :: s - case c => println(s"Invalid response: '$c'"); doLoadFailed(s, loadArg) + result.toChar match { + case '\n' | '\r' => retry + case 'r' | 'R' => retry + case 'q' | 'Q' => s.exit(ok = false) + case 'i' | 'I' => s.log.warn(s"Ignoring load failure: $ignoreMsg."); s + case 'l' | 'L' => LastCommand :: loadProjectCommand(LoadFailed, loadArg) :: s + case c => println(s"Invalid response: '$c'"); doLoadFailed(s, loadArg) + } } }