Ability to install python from PyCharm on macOS (PY-51686)

GitOrigin-RevId: 53ba94d3de3844fb5b58822576e4a54fe060d53b
This commit is contained in:
Semyon Proshev
2021-12-22 10:59:14 +00:00
committed by intellij-monorepo-bot
parent 637aea0ae3
commit aa5acb75c9
4 changed files with 179 additions and 3 deletions
@@ -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=<html><strong>Note:</strong> You'll need admin permissions to install packages for this interpreter. Consider creating a per-project virtual environment instead.</html>
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
@@ -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<PySdkToInstall> {
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>): PyDetectedSdk? {
val project = module?.project
return ProgressManager.getInstance().run(
object : Task.WithResult<PyDetectedSdk?, Exception>(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")
}
}
}
}
@@ -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
}
}
@@ -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();
}
}