Redesign of download python sdk (PY-63083)

+ Installer for pkg (MacOS packages)
+ Installer for exe (Windows executable)
- get rid of XCodeSelect installer (command line tools)
* Split PySdkToInstall

(cherry picked from commit c1a0becc70c6e421c48dd03bb80f31b8b7509dfe)

IJ-MR-120874

GitOrigin-RevId: 4fdcaa18c41bcdd0c004fed76de3054683b9ffbd
This commit is contained in:
Vitaly Legchilkin
2023-11-16 18:30:08 +01:00
committed by intellij-monorepo-bot
parent 56de140cf3
commit 34f3828f5a
12 changed files with 1003 additions and 416 deletions

View File

@@ -28,5 +28,8 @@
<orderEntry type="library" name="jna" level="project" />
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
<orderEntry type="library" name="caffeine" level="project" />
<orderEntry type="library" name="jackson" level="project" />
<orderEntry type="library" name="jackson-databind" level="project" />
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
</component>
</module>

View File

@@ -35,6 +35,12 @@ python.sdk.please.reconfigure.interpreter=Please reconfigure Python interpreter
python.sdk.pipenv.creating.venv.not.supported=Creating a virtual environments based on Pipenv environments is not supported
python.sdk.invalid.remote.sdk=Invalid remote SDK
python.sdk.package.managing.not.supported.for.sdk=Package managing is not supported for SDK {0}
python.sdk.preparation.hint=Preparing for installation
python.sdk.downloading=Downloading {0}
python.sdk.installing.hint.windows=Windows may require your approval. Please check the taskbar.
python.sdk.installing.hint=Installation is in progress. Please check system alerts.
python.sdk.running=Running {0}
python.sdk.running.sudo.prompt=Enter your password to install {0}
# PLEASE add only the keys that are supposed to be re-used by plugins,
# PLEASE keep keys below grouped by the topic,

View File

@@ -0,0 +1,317 @@
{
"python": [
{
"version": "3.12.0",
"product": "CPython",
"sources": [
{
"url": "https://www.python.org/ftp/python/3.12.0/Python-3.12.0.tar.xz",
"size": 20575020,
"sha256": "795c34f44df45a0e9b9710c8c71c15c671871524cd412ca14def212e8ccb155d"
},
{
"url": "https://www.python.org/ftp/python/3.12.0/Python-3.12.0.tgz",
"size": 27195214,
"sha256": "51412956d24a1ef7c97f1cb5f70e185c13e3de1f50d131c0aac6338080687afb"
}
],
"binaries": [
{
"os": "Windows",
"cpuArch": "X86_64",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.12.0/python-3.12.0-amd64.exe",
"size": 26507904,
"sha256": "c6bdf93f4b2de6dfa1a3a847e7c24ae10edf7f6318653d452cd4381415700ada"
}
]
},
{
"os": "Windows",
"cpuArch": "X86",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.12.0/python-3.12.0.exe",
"size": 25173976,
"sha256": "78fe137b4b78274e455ce678ba2e296ca7c3c6a0e53806bf09e4f8986b64c632"
}
]
},
{
"os": "Windows",
"cpuArch": "ARM64",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.12.0/python-3.12.0-arm64.exe",
"size": 25742528,
"sha256": "05eb076ce9fe248d4a6295f75be328808b22877f9538cf9effe89938aebc9532"
}
]
},
{
"os": "macOS",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.12.0/python-3.12.0-macos11.pkg",
"size": 45371285,
"sha256": "d18c9ba65137b6f2ef2f4083b647273639f17e390f7439b3c2e35686040745db"
}
]
}
]
},
{
"version": "3.11.6",
"product": "CPython",
"sources": [
{
"url": "https://www.python.org/ftp/python/3.11.6/Python-3.11.6.tar.xz",
"size": 20067204,
"sha256": "0fab78fa7f133f4f38210c6260d90d7c0d5c7198446419ce057ec7ac2e6f5f38"
},
{
"url": "https://www.python.org/ftp/python/3.11.6/Python-3.11.6.tgz",
"size": 26590303,
"sha256": "c049bf317e877cbf9fce8c3af902436774ecef5249a29d10984ca3a37f7f4736"
}
],
"binaries": [
{
"os": "Windows",
"cpuArch": "X86_64",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.11.6/python-3.11.6-amd64.exe",
"size": 25962920,
"sha256": "8d0fd1c7bab34dd26fb89327cf7b7c2c7dc57c4d2a7bea58eae198aa9dd5b4ef"
}
]
},
{
"os": "Windows",
"cpuArch": "X86",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.11.6/python-3.11.6.exe",
"size": 24691136,
"sha256": "d19857c64d2ec2d2db67e308b5a1f87be677e7e8ef870e2271f5c573c7eaf314"
}
]
},
{
"os": "Windows",
"cpuArch": "ARM64",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.11.6/python-3.11.6-arm64.exe",
"size": 25253752,
"sha256": "46c43c1628fb6885e91f937b73510ddf30379fe6bc08f3648cd14962c9408cfc"
}
]
},
{
"os": "macOS",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.11.6/python-3.11.6-macos11.pkg",
"size": 44266709,
"sha256": "c06ff46fa6159da61862ff2b6cde130bee093a5d547281876c9f1f41cc60376d"
}
]
}
]
},
{
"version": "3.10.13",
"product": "CPython",
"sources": [
{
"url": "https://www.python.org/ftp/python/3.10.13/Python-3.10.13.tar.xz",
"size": 19663088,
"sha256": "5c88848668640d3e152b35b4536ef1c23b2ca4bd2c957ef1ecbb053f571dd3f6"
},
{
"url": "https://www.python.org/ftp/python/3.10.13/Python-3.10.13.tgz",
"size": 26111363,
"sha256": "698ec55234c1363bd813b460ed53b0f108877c7a133d48bde9a50a1eb57b7e65"
}
]
},
{
"version": "3.10.11",
"product": "CPython",
"binaries": [
{
"os": "Windows",
"cpuArch": "X86_64",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.10.11/python-3.10.11-amd64.exe",
"size": 29037240,
"sha256": "d8dede5005564b408ba50317108b765ed9c3c510342a598f9fd42681cbe0648b"
}
]
},
{
"os": "Windows",
"cpuArch": "X86",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.10.11/python-3.10.11.exe",
"size": 27865760,
"sha256": "bd115a575e86e61cea9136c5a2c47e090ba484dc2dee8b51a34111bb094266d5"
}
]
},
{
"os": "macOS",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.10.11/python-3.10.11-macos11.pkg",
"size": 41017419,
"sha256": "767ed35ad688d28ea4494081ae96408a0318d0d5bb9ca0139d74d6247b231cfc"
}
]
}
]
},
{
"version": "3.10.7313",
"product": "PyPy",
"sources": [
{
"url": "https://downloads.python.org/pypy/pypy3.10-v7.3.13-src.zip",
"size": 30067315,
"sha256": "828fc66eca1c097e44bc910c78ab773a98747268c7ce264da97022e5aca358dc"
},
{
"url": "https://downloads.python.org/pypy/pypy3.10-v7.3.13-src.tar.bz2",
"size": 23067819,
"sha256": "4ac1733c19d014d3193c804e7f40ffccbf6924bcaaee1b6089b82b9bf9353a6d"
}
],
"binaries": [
{
"os": "Windows",
"cpuArch": "X86_64",
"resources": [
{
"url": "https://downloads.python.org/pypy/pypy3.10-v7.3.13-win64.zip",
"size": 31510169,
"sha256": "5b99422fb8978b2f4bbf97961bca49963a82dc47c2fa51b7d23c493db3a2e0f0"
}
]
}
]
},
{
"version": "3.9.18",
"product": "CPython",
"sources": [
{
"url": "https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tar.xz",
"size": 19673928,
"sha256": "01597db0132c1cf7b331eff68ae09b5a235a3c3caa9c944c29cac7d1c4c4c00a"
},
{
"url": "https://www.python.org/ftp/python/3.9.18/Python-3.9.18.tgz",
"size": 26294072,
"sha256": "504ce8cfd59addc04c22f590377c6be454ae7406cb1ebf6f5a350149225a9354"
}
]
},
{
"version": "3.9.13",
"product": "CPython",
"binaries": [
{
"os": "Windows",
"cpuArch": "X86_64",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe",
"size": 29235432,
"sha256": "fb3d0466f3754752ca7fd839a09ffe53375ff2c981279fd4bc23a005458f7f5d"
}
]
},
{
"os": "Windows",
"cpuArch": "X86",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.9.13/python-3.9.13.exe",
"size": 28096840,
"sha256": "f363935897bf32adf6822ba15ed1bfed7ae2ae96477f0262650055b6e9637c35"
}
]
},
{
"os": "macOS",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.9.13/python-3.9.13-macos11.pkg",
"size": 38821163,
"sha256": "351fe18f4fb03be7afac5e4012fc0a51345f43202af43ef620cf1eee5ee36578"
}
]
}
]
},
{
"version": "3.8.18",
"product": "CPython",
"sources": [
{
"url": "https://www.python.org/ftp/python/3.8.18/Python-3.8.18.tar.xz",
"size": 20696952,
"sha256": "3ffb71cd349a326ba7b2fadc7e7df86ba577dd9c4917e52a8401adbda7405e3f"
},
{
"url": "https://www.python.org/ftp/python/3.8.18/Python-3.8.18.tgz",
"size": 27337549,
"sha256": "7c5df68bab1be81a52dea0cc2e2705ea00553b67107a301188383d7b57320b16"
}
]
},
{
"version": "3.8.10",
"product": "CPython",
"binaries": [
{
"os": "Windows",
"cpuArch": "X86_64",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.8.10/python-3.8.10-amd64.exe",
"size": 28296784,
"sha256": "7628244cb53408b50639d2c1287c659f4e29d3dfdb9084b11aed5870c0c6a48a"
}
]
},
{
"os": "Windows",
"cpuArch": "X86",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.8.10/python-3.8.10.exe",
"size": 27204536,
"sha256": "ad07633a1f0cd795f3bf9da33729f662281df196b4567fa795829f3bb38a30ac"
}
]
},
{
"os": "macOS",
"resources": [
{
"url": "https://www.python.org/ftp/python/3.8.10/python-3.8.10-macosx10.9.pkg",
"size": 29896827,
"sha256": "4c65bc7534d5f07edacbe0fbd609b5734dbf3ac02f5444f9bd97963d589d8afd"
}
]
}
]
}
]
}

View File

@@ -0,0 +1,156 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.google.common.io.Resources
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.util.Version
import com.intellij.util.PathUtilRt
import com.intellij.util.Url
import com.intellij.util.Urls
import com.intellij.util.system.CpuArch
import com.intellij.util.system.OS
import com.jetbrains.python.psi.LanguageLevel
import java.net.URL
import java.nio.charset.StandardCharsets
private val LOG: Logger = logger<Sdks>()
/**
* Currently only CPython is supported, PyPy was added to check future structure flexibility.
*/
enum class Product(val title: String) {
CPython("Python"),
PyPy("PyPy");
}
/**
* Resource Type enum with autodetection via file extensions.
*/
enum class ResourceType(vararg val extensions: String) {
MICROSOFT_WINDOWS_EXECUTABLE("exe"),
MICROSOFT_SOFTWARE_INSTALLER("msi"),
APPLE_SOFTWARE_PACKAGE("pkg"),
COMPRESSED("zip", "xz", "tgz", "bz2");
companion object {
fun ofFileName(fileName: String): ResourceType {
val extension = PathUtilRt.getFileExtension(fileName)?.lowercase()
return entries.first { extension in it.extensions }
}
}
}
/**
* Url-specified file resource. FileName and ResourceType values are calculated by the Url provided (might be declared explicitly).
* Downloaded size / sha256 should be verified to prevent consistency leaks.
*/
data class Resource(
val url: Url,
val size: Long,
val sha256: String,
val fileName: String = PathUtilRt.getFileName(url.getPath()),
val type: ResourceType = ResourceType.ofFileName(fileName),
)
/**
* Custom prepared installation packages per OS and ArchType.
* Could contain multiple resources (in case of MSI for example)
*/
data class Binary(
val os: OS,
val cpuArch: CpuArch?,
val resources: List<Resource>,
) {
fun isCompatible(os: OS = OS.CURRENT, cpuArch: CpuArch = CpuArch.CURRENT) = this.os == os && (this.cpuArch?.equals(cpuArch) ?: true)
}
/**
* Bundle with release version of vendor. Might contain sources or any binary packages.
* Vendor + Version is a primary key.
*/
data class Release(
val version: Version,
val product: Product,
val sources: List<Resource>?,
val binaries: List<Binary>?,
val title: String = "${product.title} ${version}"
) : Comparable<Release> {
override fun compareTo(other: Release) = compareValuesBy(this, other, { it.product }, { it.version })
override fun toString(): String {
return title
}
}
/**
* Class represents /sdks.json structure with all available SDK release mappings.
* It has only python section currently.
*/
data class Sdks(
val python: List<Release> = listOf(),
)
fun Version.toLanguageLevel(): LanguageLevel? = LanguageLevel.fromPythonVersion("$major.$minor")
/**
* This class replaces missed String-arg constructor in Version class for jackson deserialization.
*
* @see com.intellij.openapi.util.Version
*/
class VersionDeserializer : JsonDeserializer<Version>() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Version {
return Version.parseVersion(p!!.valueAsString)!!
}
}
/**
* This class replaces missed String-arg constructor in Url class for jackson deserialization.
*
* @see com.intellij.util.Url
*/
class UrlDeserializer : JsonDeserializer<Url>() {
override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Url {
return Urls.parseEncoded(p!!.valueAsString)!!
}
}
object SdksKeeper {
private val configUrl: URL? = Sdks::class.java.getResource("/sdks.json")
val sdks: Sdks by lazy {
deserialize(load())
}
fun pythonReleasesByLanguageLevel(): Map<LanguageLevel, List<Release>> {
return sdks.python.filter { it.version.toLanguageLevel() != null }.groupBy { it.version.toLanguageLevel()!! }
}
private fun deserialize(content: String?): Sdks = try {
jacksonObjectMapper()
.registerModule(
SimpleModule()
.addDeserializer(Version::class.java, VersionDeserializer())
.addDeserializer(Url::class.java, UrlDeserializer())
)
.readValue(content, Sdks::class.java)
}
catch (ex: Exception) {
LOG.error("Json syntax error in the $configUrl", ex)
Sdks()
}
private fun load() = configUrl?.let { Resources.toString(it, StandardCharsets.UTF_8) }
}

View File

@@ -0,0 +1,139 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.installer
import com.google.common.hash.Hashing
import com.google.common.io.Files
import com.intellij.openapi.application.PathManager
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.util.io.FileUtil
import com.intellij.util.io.HttpRequests
import com.intellij.util.system.OS
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.sdk.*
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.io.path.fileSize
val LOGGER = logger<ReleaseInstaller>()
data class ReleasePreview(val description: String, val size: Long) {
companion object {
fun of(resource: Resource?) = resource?.let { ReleasePreview(it.url.toString(), it.size) } ?: ReleasePreview("", 0)
}
}
/**
* Software Release Installer.
*
* @see com.jetbrains.python.sdk.Release
*/
interface ReleaseInstaller {
/**
* Verifies if it can install the release (have enough binary/source resources or tools installed).
*/
fun canInstall(release: Release): Boolean
/**
* Installation process of the selected software release.
* Should be checked with canInstall, otherwise the behavior is not predictable.
* @see ReleaseInstaller.canInstall
* @param onPrepareComplete callback function, have to be called right before the installation process.
*/
@Throws(ReleaseInstallerException::class)
fun install(release: Release, indicator: ProgressIndicator, onPrepareComplete: () -> Unit)
/**
* Preview for pre-install messages and warnings.
*/
fun getPreview(release: Release): ReleasePreview
}
/**
* Base Release Installer with additional external resource loading.
* Responsible for loading and checking the checksum/size of additional resources before processing.
*/
abstract class DownloadableReleaseInstaller : ReleaseInstaller {
/**
* Concrete installer should choose which resources it needs.
* Might be any additional resources or tools outside the release scope.
*/
abstract fun getResourcesToDownload(release: Release): List<Resource>
/**
* Concrete installer process entry point.
* There is a guarantee that at this moment all requested resources were downloaded
* and their paths are stored in the resourcePaths.
*
* @see getResourcesToDownload
* @param resourcePaths - requested resources download paths (on the temp path)
*/
@Throws(ProcessException::class)
abstract fun process(release: Release, resourcePaths: Map<Resource, Path>, indicator: ProgressIndicator)
@Throws(ReleaseInstallerException::class)
override fun install(release: Release, indicator: ProgressIndicator, onPrepareComplete: () -> Unit) {
val tempPath = PathManager.getTempPath()
val files = getResourcesToDownload(release).associateWith {
Paths.get(tempPath, "${System.nanoTime()}-${it.fileName}")
}
try {
indicator.text = PySdkBundle.message("python.sdk.preparation.hint")
files.forEach { (resource, path) ->
download(resource, path, indicator)
}
onPrepareComplete()
indicator.text = when(OS.CURRENT) {
OS.Windows -> PySdkBundle.message("python.sdk.installing.hint.windows")
else -> PySdkBundle.message("python.sdk.installing.hint")
}
process(release, files, indicator)
}
finally {
files.values.forEach { runCatching { FileUtil.delete(it) } }
}
}
@Throws(PrepareException::class)
private fun checkConsistency(resource: Resource, target: Path) {
LOGGER.debug("Checking installer size")
val sizeDiff = target.fileSize() - resource.size
if (sizeDiff != 0L) {
throw WrongSizePrepareException(target, sizeDiff)
}
LOGGER.debug("Checking installer checksum")
val actualHashCode = Files.asByteSource(target.toFile()).hash(Hashing.sha256()).toString()
if (!resource.sha256.equals(actualHashCode, ignoreCase = true)) {
throw WrongChecksumPrepareException(target, resource.sha256, actualHashCode)
}
}
@Throws(PrepareException::class)
private fun download(resource: Resource, target: Path, indicator: ProgressIndicator) {
LOGGER.info("Downloading ${resource.url} to $target")
indicator.text2 = PySdkBundle.message("python.sdk.downloading", resource.fileName)
try {
indicator.checkCanceled()
HttpRequests.request(resource.url)
.productNameAsUserAgent()
.saveToFile(target.toFile(), indicator)
indicator.checkCanceled()
checkConsistency(resource, target)
}
catch (e: ProcessCanceledException) {
throw CancelledPrepareException(e)
}
catch (e: Exception) {
throw PrepareException(e)
}
}
override fun getPreview(release: Release): ReleasePreview {
return ReleasePreview.of(getResourcesToDownload(release).firstOrNull())
}
}

View File

@@ -0,0 +1,44 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.installer
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.ProcessOutput
import com.intellij.openapi.progress.ProcessCanceledException
import java.io.IOException
import java.nio.file.Path
/**
* Base Exception for the Software Release Installer.
*/
open class ReleaseInstallerException(cause: Throwable) : Exception(cause)
/**
* Base Exception for the release prepare logic (the stage before onPrepareComplete callback).
*/
open class PrepareException(cause: Throwable) : ReleaseInstallerException(cause)
class WrongSizePrepareException(path: Path, sizeDiff: Long) : PrepareException(
IOException("Downloaded $path has incorrect size, difference is $sizeDiff bytes.")
)
class WrongChecksumPrepareException(path: Path, required: String, actual: String) : PrepareException(
IOException("Checksums for $path does not match. Actual value is $actual, expected $required.")
)
class CancelledPrepareException(cause: ProcessCanceledException) : PrepareException(cause)
/**
* Base Exception for the release processing logic (the stage after onPrepareComplete callback).
*/
open class ProcessException(val command: GeneralCommandLine, val output: ProcessOutput?, cause: Throwable) : ReleaseInstallerException(
cause)
class ExecutionProcessException(command: GeneralCommandLine, cause: Exception) : ProcessException(command, null, cause)
class NonZeroExitCodeProcessException(command: GeneralCommandLine, output: ProcessOutput)
: ProcessException(command, output, IOException("Exit code is non-zero: ${output.exitCode}"))
class TimeoutProcessException(command: GeneralCommandLine, output: ProcessOutput)
: ProcessException(command, output, IOException("Runtime timeout reached"))
class CancelledProcessException(command: GeneralCommandLine, output: ProcessOutput)
: ProcessException(command, output, IOException("Installation was canceled"))

View File

@@ -0,0 +1,90 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.jetbrains.python.sdk.installer
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.process.CapturingProcessHandler
import com.intellij.execution.process.ProcessOutput
import com.intellij.execution.util.ExecUtil
import com.intellij.openapi.progress.ProgressIndicator
import com.jetbrains.python.PySdkBundle
import com.jetbrains.python.sdk.Release
import com.jetbrains.python.sdk.Resource
import com.jetbrains.python.sdk.ResourceType
import java.nio.file.Path
import kotlin.io.path.absolutePathString
/**
* Software Release Installer for Apple Software Package (pkg) files
*/
class PkgReleaseInstaller : ResourceTypeReleaseInstaller(ResourceType.APPLE_SOFTWARE_PACKAGE) {
override fun buildCommandLine(resource: Resource, path: Path): GeneralCommandLine {
return ExecUtil.sudoCommand(
GeneralCommandLine("installer", "-pkg", path.absolutePathString(), "-target", "/"),
PySdkBundle.message("python.sdk.running.sudo.prompt", resource.fileName)
)
}
}
/**
* Software Release Installer for Microsoft Window Executable (exe) files
*/
class ExeReleaseInstaller : ResourceTypeReleaseInstaller(ResourceType.MICROSOFT_WINDOWS_EXECUTABLE) {
override fun buildCommandLine(resource: Resource, path: Path): GeneralCommandLine {
return GeneralCommandLine(path.absolutePathString(), "/quiet")
}
}
/**
* Base Release Installer with resource type specific filtering (like exe, pkg, ...)
*/
abstract class ResourceTypeReleaseInstaller(private val resourceType: ResourceType) : DownloadableReleaseInstaller() {
abstract fun buildCommandLine(resource: Resource, path: Path): GeneralCommandLine
@Throws(ProcessException::class)
override fun process(release: Release, resourcePaths: Map<Resource, Path>, indicator: ProgressIndicator) {
indicator.isIndeterminate = true
resourcePaths.forEach { (resource, path) ->
processResource(resource, path, indicator)
}
}
@Throws(ProcessException::class)
private fun processResource(resource: Resource, path: Path, indicator: ProgressIndicator) {
indicator.text2 = PySdkBundle.message("python.sdk.running", resource.fileName)
val commandLine = buildCommandLine(resource, path)
LOGGER.info("Running ${commandLine.commandLineString}")
val processOutput: ProcessOutput
try {
processOutput = CapturingProcessHandler(commandLine).runProcessWithProgressIndicator(indicator)
}
catch (e: Exception) {
throw ExecutionProcessException(commandLine, e)
}
processOutput.isCancelled.takeIf { it }?.let { throw CancelledProcessException(commandLine, processOutput) }
processOutput.exitCode.takeIf { it != 0 }?.let { throw NonZeroExitCodeProcessException(commandLine, processOutput) }
processOutput.isTimeout.takeIf { it }?.let { throw TimeoutProcessException(commandLine, processOutput) }
}
/**
* It can install a release only if the release contains at least single resource with corresponding resource type in the binaries.
*/
override fun canInstall(release: Release): Boolean {
return release.binaries?.any {
it.isCompatible() &&
it.resources.any { r -> r.type == resourceType }
} ?: false
}
/**
* @returns first non-empty list with resources of the selected type in any compatible binary package or empty list if nothing is found.
*/
override fun getResourcesToDownload(release: Release): List<Resource> {
return release.binaries
?.filter { it.isCompatible() }
?.firstNotNullOfOrNull {
it.resources.filter { r -> r.type == resourceType }.ifEmpty { null }
}
?: listOf()
}
}

View File

@@ -0,0 +1,28 @@
package com.jetbrains.python.tests
import com.jetbrains.python.psi.LanguageLevel
import com.jetbrains.python.sdk.SdksKeeper
import com.jetbrains.python.sdk.Product
import junit.framework.TestCase.assertEquals
import org.junit.Test
class SdksTest {
@Test
fun testLoading() {
assert(SdksKeeper.pythonReleasesByLanguageLevel().size == 5)
}
@Test
fun testPyPy() {
val releases = SdksKeeper.pythonReleasesByLanguageLevel()[LanguageLevel.PYTHON310]
assert(releases!!.any { it.product == Product.PyPy })
}
@Test
fun testAvailableSorted() {
assertEquals(
setOf("3.8", "3.9", "3.10", "3.11", "3.12"),
SdksKeeper.pythonReleasesByLanguageLevel().keys.map { it.toString() }.toSet()
)
}
}