diff --git a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerEngine.kt b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerEngine.kt index 61c65a9b4ce0..b21b987bf94f 100644 --- a/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerEngine.kt +++ b/python/src/com/jetbrains/python/packaging/conda/CondaPackageManagerEngine.kt @@ -35,7 +35,7 @@ internal class CondaPackageManagerEngine(private val sdk: Sdk) : PythonPackageMa } override suspend fun installPackageCommand(installRequest: PythonPackageInstallRequest, options: List): PyResult { - val installationArgs = installRequest.buildInstallationArguments().getOr { return it } + val installationArgs = installRequest.buildCondaInstallationArguments().getOr { return it } val env = getEnvData() return CondaExecutor.installPackages(sdk.getCondaBinToExecute(), env.envIdentity, installationArgs, options) } @@ -61,20 +61,18 @@ internal class CondaPackageManagerEngine(private val sdk: Sdk) : PythonPackageMa private fun getEnvData(): PyCondaEnv = (sdk.pySdkAdditionalData.flavorAndData.data as PyCondaFlavorData).env +} - private fun PythonPackageInstallRequest.buildInstallationArguments(): PyResult> = when (this) { - is PythonPackageInstallRequest.ByLocation -> PyResult.localizedError(PyBundle.message("python.packaging.conda.does.not.support.location.uri")) - is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> { - val condaSpecs = specifications.filter { it.repository is CondaPackageRepository } - val specs = condaSpecs.map { it.nameWithVersionSpec } +internal fun PythonPackageInstallRequest.buildCondaInstallationArguments(): PyResult> = when (this) { + is PythonPackageInstallRequest.ByLocation -> PyResult.localizedError(PyBundle.message("python.packaging.conda.does.not.support.location.uri")) + is PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications -> { + val condaSpecs = specifications.filter { it.repository is CondaPackageRepository } + val specs = condaSpecs.map { it.nameWithVersionSpec } - //https://docs.conda.io/projects/conda/en/latest/user-guide/concepts/pkg-specs.html#package-match-specifications - //When using the command line, put double quotes around any package version specification that - // contains the space character or any of the following characters: <, >, *, or |. - //Ido not know why we need put single prefix quota but it does not work in EEL with suffix quota - val quoted = specs.map { "\"$it" } - - PyResult.success(quoted) - } + // Each spec is passed to conda as a single argv element (no shell), so it must not be quoted. + // conda's docs recommend double quotes only for shell command lines to protect characters like <, >, *, |. + // Wrapping the spec in a quote here glues a literal '"' onto the package name, which conda 26.x rejects + // with InvalidMatchSpec (PY-91412). + PyResult.success(specs) } } \ No newline at end of file diff --git a/python/testSrc/com/jetbrains/python/packaging/conda/CondaInstallationArgumentsTest.kt b/python/testSrc/com/jetbrains/python/packaging/conda/CondaInstallationArgumentsTest.kt new file mode 100644 index 000000000000..f4baa1c7d19a --- /dev/null +++ b/python/testSrc/com/jetbrains/python/packaging/conda/CondaInstallationArgumentsTest.kt @@ -0,0 +1,29 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.packaging.conda + +import com.intellij.testFramework.junit5.TestApplication +import com.jetbrains.python.getOrThrow +import com.jetbrains.python.packaging.common.PythonRepositoryPackageSpecification +import com.jetbrains.python.packaging.management.PythonPackageInstallRequest +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +@TestApplication +class CondaInstallationArgumentsTest { + // PY-91412: specs are passed to conda as individual argv elements (no shell), so they must reach conda verbatim. + // A stray leading double-quote used to be glued onto every spec, which conda 26.x rejects with InvalidMatchSpec. + @Test + fun testCondaSpecsAreNotQuoted() { + val specs = listOf( + PythonRepositoryPackageSpecification(CondaPackageRepository, "seaborn"), + PythonRepositoryPackageSpecification(CondaPackageRepository, "seaborn", "0.13.2"), + ) + val request = PythonPackageInstallRequest.ByRepositoryPythonPackageSpecifications(specs) + + val args = request.buildCondaInstallationArguments().getOrThrow() + + assertEquals(specs.map { it.nameWithVersionSpec }, args) + assertTrue(args.none { it.contains('"') }, "Conda specs must not contain quote characters: $args") + } +}