diff --git a/python/pluginResources/messages/PyBundle.properties b/python/pluginResources/messages/PyBundle.properties
index 94e55fc92d52..ea7402aaf269 100644
--- a/python/pluginResources/messages/PyBundle.properties
+++ b/python/pluginResources/messages/PyBundle.properties
@@ -432,8 +432,10 @@ python.sdk.can.t.obtain.python.version=Can't obtain python version
python.sdk.empty.version.string=Python interpreter returned the empty output as a version string
python.sdk.non.zero.exit.code=Python interpreter process exited with a non-zero exit code {0}
python.sdk.executable.not.found.header=Python executable is not found. Choose one of the following options:
-python.sdk.executable.not.found.option.specify.path=Click {0} to specify a path to python.exe in your file system
+python.sdk.executable.not.found.option.specify.path=Click {0} to specify a path to {1} in your file system
python.sdk.executable.not.found.option.download.and.install=Click {0} to download and install Python from python.org ({1})
+python.sdk.executable.not.found.option.install.with.cldt=Click {0} to install Python with the Command-Line Developer Tools: {1}
+python.sdk.executable.not.found.option.install.or.brew=Install Python manually from python.org or via Homebrew
python.sdk.admin.permissions.needed.consider.creating.venv=Note: You'll need admin permissions to install packages for this interpreter. Consider creating a per-project virtual environment instead.
python.sdk.try.to.install.python.manually=Try to install Python from https://www.python.org manually.
python.sdk.consider.installing.python.manually=Consider installing Python from https://www.python.org manually.
@@ -449,6 +451,13 @@ python.sdk.action.add.new.interpreter.text=Add New Interpreter
python.sdk.action.add.local.interpreter.text=Add Local Interpreter...
python.sdk.action.add.interpreter.based.on.target.text=On {0}...
+python.cldt.checking=Checking for Command-Line Developer Tools installation
+python.cldt.required=Selected python requires Command-Line Developer Tools: {0}
+python.cldt.installing.suggestion=Install Python with the Command-Line Developer Tools
+python.cldt.installing.title=Waiting for Python Installation and Interpreter Setup
+python.cldt.installing.indicator=macOS is installing Command-Line Developer Tools
+python.cldt.installing.skip=Skip this step to configure an interpreter later
+
# SDK / skeletons
sdk.some.skeletons.failed=Some skeletons failed to generate
sdk.gen.cleaning.up=Cleaning up skeletons\u2026
diff --git a/python/src/com/jetbrains/python/sdk/PySdkToInstall.kt b/python/src/com/jetbrains/python/sdk/PySdkToInstall.kt
index cede3f058ac6..52808f67dc11 100644
--- a/python/src/com/jetbrains/python/sdk/PySdkToInstall.kt
+++ b/python/src/com/jetbrains/python/sdk/PySdkToInstall.kt
@@ -7,8 +7,11 @@ import com.google.common.io.Files
import com.intellij.execution.ExecutionException
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
+import com.intellij.execution.process.OSProcessUtil
import com.intellij.execution.process.ProcessOutput
+import com.intellij.execution.util.ExecUtil.execAndGetOutput
import com.intellij.icons.AllIcons
+import com.intellij.ide.IdeBundle
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.debug
@@ -38,9 +41,11 @@ import com.jetbrains.python.sdk.PySdkToInstallCollector.Companion.InstallationRe
import com.jetbrains.python.sdk.PySdkToInstallCollector.Companion.LookupResult
import com.jetbrains.python.sdk.PySdkToInstallCollector.Companion.logSdkDownload
import com.jetbrains.python.sdk.PySdkToInstallCollector.Companion.logSdkInstallation
+import com.jetbrains.python.sdk.flavors.MacPythonSdkFlavor
import org.jetbrains.annotations.CalledInAny
import java.io.File
import java.io.IOException
+import java.util.concurrent.TimeUnit
import kotlin.math.absoluteValue
private val LOGGER = Logger.getInstance(PySdkToInstall::class.java)
@@ -48,6 +53,7 @@ private val LOGGER = Logger.getInstance(PySdkToInstall::class.java)
@CalledInAny
internal fun getSdksToInstall(): List {
return if (SystemInfo.isWindows) listOf(getPy39ToInstallOnWindows(), getPy310ToInstallOnWindows())
+ else if (SystemInfo.isMac) listOf(PySdkToInstallViaXCodeSelect())
else emptyList()
}
@@ -127,7 +133,7 @@ private class PySdkToInstallOnWindows(name: String,
return HtmlBuilder()
.append(PyBundle.message("python.sdk.executable.not.found.header"))
.append(tag("ul").children(
- tag("li").children(raw(PyBundle.message("python.sdk.executable.not.found.option.specify.path", text("...").bold()))),
+ tag("li").children(raw(PyBundle.message("python.sdk.executable.not.found.option.specify.path", text("...").bold(), "python.exe"))),
tag("li").children(raw(PyBundle.message("python.sdk.executable.not.found.option.download.and.install",
text(defaultButtonName).bold(), fileSize)))
)).toString()
@@ -331,3 +337,87 @@ private class PySdkToInstallOnWindows(name: String,
private class PyInstallationException(val commandLine: GeneralCommandLine, val output: ProcessOutput) : Exception()
private class PyInstallationExecutionException(val commandLine: GeneralCommandLine, override val cause: ExecutionException) : Exception()
}
+
+private class PySdkToInstallViaXCodeSelect : PySdkToInstall("Python", "") {
+
+ override fun renderInList(renderer: PySdkListCellRenderer) {
+ renderer.append(name)
+ renderer.append(" ")
+ renderer.append(PyBundle.message("python.cldt.installing.suggestion"), SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES)
+ }
+
+ @NlsContexts.DialogMessage
+ override fun getInstallationWarning(defaultButtonName: String): String {
+ val commandChunk = text(MacPythonSdkFlavor.getXCodeSelectInstallCommand().commandLineString)
+
+ return HtmlBuilder()
+ .append(PyBundle.message("python.sdk.executable.not.found.header"))
+ .append(tag("ul").children(
+ tag("li").children(raw(PyBundle.message("python.sdk.executable.not.found.option.specify.path", text("...").bold(), "python"))),
+ tag("li").children(raw(
+ PyBundle.message("python.sdk.executable.not.found.option.install.with.cldt", text(defaultButtonName).bold(), commandChunk.code())
+ )),
+ tag("li").children(text(PyBundle.message("python.sdk.executable.not.found.option.install.or.brew")))
+ )).toString()
+ }
+
+ override fun install(module: Module?, systemWideSdksDetector: () -> List): PyDetectedSdk? {
+ val project = module?.project
+ return ProgressManager.getInstance().run(
+ object : Task.WithResult(project, PyBundle.message("python.cldt.installing.title"), true) {
+ override fun compute(indicator: ProgressIndicator): PyDetectedSdk? {
+ @Suppress("DialogTitleCapitalization")
+ indicator.text = PyBundle.message("python.cldt.installing.indicator")
+ indicator.text2 = PyBundle.message("python.cldt.installing.skip")
+
+ runXCodeSelectInstall()
+
+ while (!MacPythonSdkFlavor.areCommandLineDeveloperToolsAvailable() && isInstallCommandLineDeveloperToolsAppRunning()) {
+ Thread.sleep(TimeUnit.SECONDS.toMillis(5))
+ }
+
+ LOGGER.debug("Resetting system-wide sdks detectors")
+ resetSystemWideSdksDetectors()
+
+ return systemWideSdksDetector()
+ .also { sdks ->
+ LOGGER.debug { sdks.joinToString(prefix = "Detected system-wide sdks: ") { it.homePath ?: it.name } }
+ }
+ .singleOrNull()
+ }
+ }
+ .also {
+ it.cancelText = IdeBundle.message("button.skip")
+ it.cancelTooltipText = IdeBundle.message("button.skip")
+ }
+ )
+ }
+
+ private fun runXCodeSelectInstall() {
+ val commandLine = MacPythonSdkFlavor.getXCodeSelectInstallCommand()
+ try {
+ execAndGetOutput(commandLine)
+ .also {
+ if (LOGGER.isDebugEnabled) {
+ LOGGER.debug("Result of '${commandLine.commandLineString}':\n$it")
+ }
+ }
+ }
+ catch (e: ExecutionException) {
+ LOGGER.warn("Exception during '${commandLine.commandLineString}'", e)
+ }
+ }
+
+ private fun isInstallCommandLineDeveloperToolsAppRunning(): Boolean {
+ val appName = "Install Command Line Developer Tools.app"
+
+ return OSProcessUtil
+ .getProcessList()
+ .any { it.commandLine.contains(appName) }
+ .also {
+ if (LOGGER.isDebugEnabled) {
+ LOGGER.debug("'$appName' is${if (it) "" else " not"} running")
+ }
+ }
+ }
+}
diff --git a/python/src/com/jetbrains/python/sdk/add/PyAddSdkPanel.kt b/python/src/com/jetbrains/python/sdk/add/PyAddSdkPanel.kt
index 7b82a1b36d62..7febce549a33 100644
--- a/python/src/com/jetbrains/python/sdk/add/PyAddSdkPanel.kt
+++ b/python/src/com/jetbrains/python/sdk/add/PyAddSdkPanel.kt
@@ -25,6 +25,7 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.ui.TextFieldWithBrowseButton
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.openapi.util.NlsContexts
+import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.UserDataHolder
import com.intellij.openapi.util.text.StringUtil
import com.jetbrains.python.PySdkBundle
@@ -32,6 +33,7 @@ import com.jetbrains.python.newProject.steps.PyAddNewEnvironmentPanel
import com.jetbrains.python.sdk.*
import com.jetbrains.python.sdk.add.PyAddSdkDialogFlowAction.OK
import com.jetbrains.python.sdk.configuration.PyProjectVirtualEnvConfiguration
+import com.jetbrains.python.sdk.flavors.MacPythonSdkFlavor
import icons.PythonIcons
import java.awt.Component
import java.io.File
@@ -104,6 +106,9 @@ abstract class PyAddSdkPanel : JPanel(), PyAddSdkView {
val message = sdk.getInstallationWarning(defaultButtonName)
ValidationInfo(message).asWarning().withOKEnabled()
}
+ is PyDetectedSdk -> {
+ if (SystemInfo.isMac) MacPythonSdkFlavor.checkDetectedPython(sdk) else null
+ }
else -> null
}
}
diff --git a/python/src/com/jetbrains/python/sdk/flavors/MacPythonSdkFlavor.java b/python/src/com/jetbrains/python/sdk/flavors/MacPythonSdkFlavor.java
index 80ebab22558e..dee0cb505cef 100644
--- a/python/src/com/jetbrains/python/sdk/flavors/MacPythonSdkFlavor.java
+++ b/python/src/com/jetbrains/python/sdk/flavors/MacPythonSdkFlavor.java
@@ -1,12 +1,23 @@
// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.jetbrains.python.sdk.flavors;
+import com.intellij.execution.ExecutionException;
+import com.intellij.execution.configurations.GeneralCommandLine;
+import com.intellij.execution.process.ProcessOutput;
+import com.intellij.execution.util.ExecUtil;
+import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
+import com.intellij.openapi.progress.ProgressManager;
+import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.UserDataHolder;
+import com.intellij.openapi.util.text.HtmlBuilder;
+import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
+import com.jetbrains.python.PyBundle;
+import com.jetbrains.python.sdk.PyDetectedSdk;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -14,8 +25,14 @@ import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
+import static com.intellij.openapi.util.text.HtmlChunk.raw;
+import static com.intellij.openapi.util.text.HtmlChunk.text;
+
public final class MacPythonSdkFlavor extends CPythonSdkFlavor {
+
+ private static final Logger LOGGER = Logger.getInstance(MacPythonSdkFlavor.class);
+
private MacPythonSdkFlavor() {
}
@@ -32,7 +49,9 @@ public final class MacPythonSdkFlavor extends CPythonSdkFlavor {
collectPythonInstallations("/System/Library/Frameworks/Python.framework/Versions", candidates);
collectPythonInstallations("/usr/local/Cellar/python", candidates);
UnixPythonSdkFlavor.collectUnixPythons("/usr/local/bin", candidates);
- UnixPythonSdkFlavor.collectUnixPythons("/usr/bin", candidates);
+ if (areCommandLineDeveloperToolsAvailable()) {
+ UnixPythonSdkFlavor.collectUnixPythons("/usr/bin", candidates);
+ }
return candidates;
}
@@ -56,4 +75,57 @@ public final class MacPythonSdkFlavor extends CPythonSdkFlavor {
}
}
}
+
+ public static @NotNull GeneralCommandLine getXCodeSelectInstallCommand() {
+ return new GeneralCommandLine("xcode-select", "--install");
+ }
+
+ private static @NotNull GeneralCommandLine getXCodeSelectPathCommand() {
+ return new GeneralCommandLine("xcode-select", "-p");
+ }
+
+ public static boolean areCommandLineDeveloperToolsAvailable() {
+ final GeneralCommandLine commandLine = getXCodeSelectPathCommand();
+
+ try {
+ final ProcessOutput output = ExecUtil.execAndGetOutput(commandLine);
+
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Result of '" + commandLine.getCommandLineString() + "':\n" + output);
+ }
+
+ return output.getExitCode() != 2;
+ }
+ catch (ExecutionException e) {
+ LOGGER.warn("Exception during '" + commandLine.getCommandLineString() + "'", e);
+ return true;
+ }
+ }
+
+ public static @Nullable ValidationInfo checkDetectedPython(@NotNull PyDetectedSdk sdk) {
+ if (!"/usr/bin/python3".equals(sdk.getHomePath())) return null;
+
+ //noinspection DialogTitleCapitalization
+ final String progressTitle = PyBundle.message("python.cldt.checking");
+
+ if (ProgressManager
+ .getInstance()
+ .runProcessWithProgressSynchronously(
+ MacPythonSdkFlavor::areCommandLineDeveloperToolsAvailable,
+ progressTitle,
+ true,
+ null
+ )
+ ) {
+ return null;
+ }
+
+ final HtmlChunk commandChunk = text(getXCodeSelectInstallCommand().getCommandLineString());
+
+ final String message = new HtmlBuilder().append(
+ raw(PyBundle.message("python.cldt.required", commandChunk.code()))
+ ).toString();
+
+ return new ValidationInfo(message).asWarning().withOKEnabled();
+ }
}