diff --git a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala index f859c5f581..b1a17a6e95 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/package0/Package.scala @@ -1,6 +1,4 @@ package scala.cli.commands.package0 - -import ai.kien.python.Python import caseapp.* import caseapp.core.help.HelpFormat import coursier.core @@ -34,7 +32,7 @@ import scala.cli.CurrentParams import scala.cli.commands.OptionsHelper.* import scala.cli.commands.doc.Doc import scala.cli.commands.packaging.Spark -import scala.cli.commands.run.Run.orPythonDetectionError +import scala.cli.commands.run.Run.{createPythonInstance, orPythonDetectionError} import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, MainClassOptions, SharedOptions} import scala.cli.commands.util.BuildCommandHelpers import scala.cli.commands.util.BuildCommandHelpers.* @@ -1089,7 +1087,7 @@ object Package extends ScalaCommand[PackageOptions] with BuildCommandHelpers { val pythonLdFlags = if setupPython then value { - val python = Python() + val python = value(createPythonInstance().orPythonDetectionError) val flagsOrError = python.ldflags logger.debug(s"Python ldflags: $flagsOrError") flagsOrError.orPythonDetectionError diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index 676f852566..de45c9c607 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -1,6 +1,4 @@ package scala.cli.commands.repl - -import ai.kien.python.Python import caseapp.* import caseapp.core.help.HelpFormat import coursier.cache.FileCache @@ -23,7 +21,7 @@ import scala.build.internal.{Constants, Runner} import scala.build.options.ScalacOpt.noDashPrefixes import scala.build.options.{BuildOptions, JavaOpt, MaybeScalaVersion, ScalaVersionUtil, Scope} import scala.cli.CurrentParams -import scala.cli.commands.run.Run.{orPythonDetectionError, pythonPathEnv} +import scala.cli.commands.run.Run.{createPythonInstance, orPythonDetectionError, pythonPathEnv} import scala.cli.commands.run.RunMode import scala.cli.commands.shared.{HelpCommandGroup, HelpGroup, ScalacOptions, SharedOptions} import scala.cli.commands.util.BuildCommandHelpers @@ -311,7 +309,7 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { val (scalapyJavaOpts, scalapyExtraEnv) = if (setupPython) { val props = value { - val python = Python() + val python = value(createPythonInstance().orPythonDetectionError) val propsOrError = python.scalapyProperties logger.debug(s"Python Java properties: $propsOrError") propsOrError.orPythonDetectionError diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index bf9077d60e..b74604aaf4 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -504,7 +504,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val (pythonExecutable, pythonLibraryPaths, pythonExtraEnv) = if (setupPython) { val (exec, libPaths) = value { - val python = Python() + val python = value(createPythonInstance().orPythonDetectionError) val pythonPropertiesOrError = for { paths <- python.nativeLibraryPaths executable <- python.executable @@ -586,7 +586,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { val (pythonJavaProps, pythonExtraEnv) = if (setupPython) { val scalapyProps = value { - val python = Python() + val python = value(createPythonInstance().orPythonDetectionError) val propsOrError = python.scalapyProperties logger.debug(s"Python Java properties: $propsOrError") propsOrError.orPythonDetectionError @@ -712,8 +712,64 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { logger = logger ).map(f) + def findHomebrewPython(): Option[os.Path] = + if (Properties.isMac) { + // Try common Homebrew locations + val homebrewPaths = Seq( + "/opt/homebrew/bin/python3", + "/usr/local/bin/python3" + ) + homebrewPaths + .map(os.Path(_, os.pwd)) + .find { path => + os.exists(path) && { + // Verify it has the -config script + val configPath = path / os.up / s"${path.last}-config" + os.exists(configPath) + } + } + } + else None + + def createPythonInstance(): Try[Python] = + // Try default Python detection first + // If it fails on macOS and Homebrew Python is available, the error message will guide the user + Try(Python()) + final class PythonDetectionError(cause: Throwable) extends BuildException( - s"Error detecting Python environment: ${cause.getMessage}", + { + val baseMessage = cause.getMessage + if ( + Properties.isMac && baseMessage != null && baseMessage.contains( + "-config" + ) && baseMessage.contains("does not exist") + ) { + val homebrewPython = findHomebrewPython() + val homebrewHint = homebrewPython match { + case Some(path) => + s""" + |Homebrew Python was found at $path, but it's not being used. + |Ensure Homebrew's bin directory is first in your PATH: + | export PATH="${path / os.up}:$$PATH" + |""".stripMargin + case None => + """ + |Consider installing Python via Homebrew: + | brew install python + | + |Or install Python from https://www.python.org/downloads/ + |""".stripMargin + } + s"""Error detecting Python environment: $baseMessage + | + |The system Python from CommandLineTools may not include the required -config scripts. + |$homebrewHint + | + |Alternatively, you can disable Python setup with --python=false""".stripMargin + } + else + s"Error detecting Python environment: $baseMessage" + }, cause = cause )