Skip to content

Commit

Permalink
feat(Yarn): Add basic support for Corepack [1].
Browse files Browse the repository at this point in the history
According to the Yarn 2+ documentation, Corepack is the preferred way
to install this package manager. If this method is used, the name of
the executable has to be determined differently.

[1]: https://yarnpkg.com/corepack

Signed-off-by: Oliver Heger <oliver.heger@bosch.io>
  • Loading branch information
oheger-bosch committed May 27, 2024
1 parent 1765a15 commit 2119800
Show file tree
Hide file tree
Showing 2 changed files with 106 additions and 1 deletion.
89 changes: 89 additions & 0 deletions analyzer/src/funTest/kotlin/Yarn2Test.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright (C) 2024 The ORT Project Authors (see <https://github.com/oss-review-toolkit/ort/blob/main/NOTICE>)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
* License-Filename: LICENSE
*/

package org.ossreviewtoolkit.analyzer

import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.WordSpec
import io.kotest.engine.spec.tempdir
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain

import org.ossreviewtoolkit.model.config.AnalyzerConfiguration
import org.ossreviewtoolkit.model.config.PackageManagerConfiguration
import org.ossreviewtoolkit.model.config.RepositoryConfiguration
import org.ossreviewtoolkit.plugins.packagemanagers.node.Yarn2

class Yarn2Test : WordSpec({

Check warning on line 33 in analyzer/src/funTest/kotlin/Yarn2Test.kt

View workflow job for this annotation

GitHub Actions / qodana-scan

Unused symbol

Class "Yarn2Test" is never used
"command" should {
"return the executable defined in yarnrc.yml" {
val workingDir = tempdir()
val executable = "yarn-wrapper.js"
workingDir.resolve(".yarnrc.yml").writeText("yarnPath: $executable")
val executableFile = workingDir.resolve(executable).also {
it.writeText("#!/usr/bin/env node\nconsole.log('yarn')")
}

val yarn = Yarn2("yarn", workingDir, AnalyzerConfiguration(), RepositoryConfiguration())
val command = yarn.command(workingDir)

command shouldBe executableFile.absolutePath
}

"throw if no executable is defined in yarnrc.yml" {
val workingDir = tempdir()
workingDir.resolve(".yarnrc.yml").writeText("someProperty: some-value")

val yarn = Yarn2("yarn", workingDir, AnalyzerConfiguration(), RepositoryConfiguration())

val exception = shouldThrow<IllegalArgumentException> {
yarn.command(workingDir)
}

exception.localizedMessage shouldContain "No Yarn 2+ executable"
}

"throw if the executable defined in yarnrc.yml does not exist" {
val workingDir = tempdir()
val executable = "non-existing-yarn-wrapper.js"
workingDir.resolve(".yarnrc.yml").writeText("yarnPath: $executable")

val yarn = Yarn2("yarn", workingDir, AnalyzerConfiguration(), RepositoryConfiguration())

val exception = shouldThrow<IllegalArgumentException> {
yarn.command(workingDir)
}

exception.localizedMessage shouldContain executable
}

"return the default executable name if corepack is enabled" {
val workingDir = tempdir()
val yarn2Options = mapOf("corepackEnabled" to "true")
val analyzerConfiguration = AnalyzerConfiguration(
packageManagers = mapOf("Yarn2" to PackageManagerConfiguration(options = yarn2Options))
)

val yarn = Yarn2("Yarn2", workingDir, analyzerConfiguration, RepositoryConfiguration())
val command = yarn.command(workingDir)

command shouldBe "yarn"
}
}
})
18 changes: 17 additions & 1 deletion plugins/package-managers/node/src/main/kotlin/Yarn2.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ private enum class YarnDependencyType(val type: String) {
* - *disableRegistryCertificateVerification*: If true, the `yarn npm info` commands called by this package manager will
* not verify the server certificate of the HTTPS connection to the NPM registry. This allows to replace the latter by
* a local one, e.g. for intercepting the requests or replaying them.
* - *corepackEnabled*: If true, this implementation assumes that Yarn has been installed via
* [Corepack](https://yarnpkg.com/corepack). In this case, the default executable name is used, which points to a
* binary proxy that is able to download and install the correct version of Yarn.
*/
class Yarn2(
name: String,
Expand All @@ -100,6 +103,11 @@ class Yarn2(
*/
const val OPTION_DISABLE_REGISTRY_CERTIFICATE_VERIFICATION = "disableRegistryCertificateVerification"

/**
* The name of the option that determines whether Yarn 2+ was installed via Corepack.
*/
const val OPTION_COREPACK_ENABLED = "corepackEnabled"

/**
* The name of Yarn 2+ resource file.
*/
Expand All @@ -119,6 +127,11 @@ class Yarn2(
* The amount of package details to query at once with `yarn npm info`.
*/
private const val BULK_DETAILS_SIZE = 1000

/**
* The name of the default executable. This is used when the [OPTION_COREPACK_ENABLED] option is set.
*/
private const val DEFAULT_EXECUTABLE_NAME = "yarn"
}

class Factory : AbstractPackageManagerFactory<Yarn2>("Yarn2") {
Expand All @@ -135,7 +148,8 @@ class Yarn2(
* The Yarn 2+ executable is not installed globally: The program shipped by the project in `.yarn/releases` is used
* instead. The value of the 'yarnPath' property in the resource file `.yarnrc.yml` defines the path to the
* executable for the current project e.g. `yarnPath: .yarn/releases/yarn-3.2.1.cjs`.
* This map holds the mapping between the directory and their Yarn 2+ executables.
* This map holds the mapping between the directory and their Yarn 2+ executables. It is only used if Yarn has not
* been installed via Corepack; then it is accessed under a default name.
*/
private val yarn2ExecutablesByPath: MutableMap<File, String> = mutableMapOf()

Expand All @@ -157,6 +171,8 @@ class Yarn2(
override fun command(workingDir: File?): String {
if (workingDir == null) return ""

if (options[OPTION_COREPACK_ENABLED].toBoolean()) return DEFAULT_EXECUTABLE_NAME

return yarn2ExecutablesByPath.getOrPut(workingDir) {
val yarnConfig = yamlMapper.readTree(workingDir.resolve(YARN2_RESOURCE_FILE))
val yarnCommand = requireNotNull(yarnConfig[YARN_PATH_PROPERTY_NAME]) {
Expand Down

0 comments on commit 2119800

Please sign in to comment.