toolkit.plugins.packagemanagers.cocoapods-package-manager.41.0.0.source-code.CocoaPods.kt Maven / Gradle / Ivy
/*
* Copyright (C) 2021 The ORT Project Authors (see )
*
* 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.plugins.packagemanagers.cocoapods
import java.io.File
import java.io.IOException
import org.apache.logging.log4j.kotlin.logger
import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory
import org.ossreviewtoolkit.analyzer.PackageManager
import org.ossreviewtoolkit.downloader.VersionControlSystem
import org.ossreviewtoolkit.model.Hash
import org.ossreviewtoolkit.model.Identifier
import org.ossreviewtoolkit.model.Issue
import org.ossreviewtoolkit.model.Package
import org.ossreviewtoolkit.model.PackageReference
import org.ossreviewtoolkit.model.Project
import org.ossreviewtoolkit.model.ProjectAnalyzerResult
import org.ossreviewtoolkit.model.RemoteArtifact
import org.ossreviewtoolkit.model.Scope
import org.ossreviewtoolkit.model.VcsInfo
import org.ossreviewtoolkit.model.VcsType
import org.ossreviewtoolkit.model.collectDependencies
import org.ossreviewtoolkit.model.config.AnalyzerConfiguration
import org.ossreviewtoolkit.model.config.RepositoryConfiguration
import org.ossreviewtoolkit.model.createAndLogIssue
import org.ossreviewtoolkit.model.orEmpty
import org.ossreviewtoolkit.model.utils.toPurl
import org.ossreviewtoolkit.utils.common.CommandLineTool
import org.ossreviewtoolkit.utils.common.Os
import org.ossreviewtoolkit.utils.common.collectMessages
import org.ossreviewtoolkit.utils.common.stashDirectories
import org.semver4j.RangesList
import org.semver4j.RangesListFactory
/**
* The [CocoaPods](https://cocoapods.org/) package manager for Objective-C.
*
* As pre-condition for the analysis each respective definition file must have a sibling lockfile named 'Podfile.lock'.
* The dependency tree is constructed solely based on parsing that lockfile. So, the dependency tree can be constructed
* on any platform. Note that obtaining the dependency tree from the 'pod' command without a lockfile has Xcode
* dependencies and is not supported by this class.
*
* The only interactions with the 'pod' command happen in order to obtain metadata for dependencies. Therefore,
* 'pod spec which' gets executed, which works also under Linux.
*/
class CocoaPods(
name: String,
analysisRoot: File,
analyzerConfig: AnalyzerConfiguration,
repoConfig: RepositoryConfiguration
) : PackageManager(name, "CocoaPods", analysisRoot, analyzerConfig, repoConfig), CommandLineTool {
class Factory : AbstractPackageManagerFactory("CocoaPods") {
override val globsForDefinitionFiles = listOf("Podfile")
override fun create(
analysisRoot: File,
analyzerConfig: AnalyzerConfiguration,
repoConfig: RepositoryConfiguration
) = CocoaPods(type, analysisRoot, analyzerConfig, repoConfig)
}
private val podspecCache = mutableMapOf()
override fun command(workingDir: File?) = if (Os.isWindows) "pod.bat" else "pod"
override fun getVersionRequirement(): RangesList = RangesListFactory.create(">=1.11.0")
override fun getVersionArguments() = "--version --allow-root"
override fun beforeResolution(definitionFiles: List) = checkVersion()
override fun resolveDependencies(definitionFile: File, labels: Map): List =
stashDirectories(Os.userHomeDirectory.resolve(".cocoapods/repos")).use {
// Ensure to use the CDN instead of the monolithic specs repo.
run("repo", "add-cdn", "trunk", "https://cdn.cocoapods.org", "--allow-root")
try {
resolveDependenciesInternal(definitionFile)
} finally {
// The cache entries are not re-usable across definition files because the keys do not contain the
// dependency version. If non-default Specs repositories were supported, then these would also need to
// be part of the key. As that's more complicated and not giving much performance prefer the more memory
// consumption friendly option of clearing the cache.
podspecCache.clear()
}
}
private fun resolveDependenciesInternal(definitionFile: File): List {
val workingDir = definitionFile.parentFile
val lockfile = workingDir.resolve(LOCKFILE_FILENAME)
val scopes = mutableSetOf()
val packages = mutableSetOf()
val issues = mutableListOf()
if (lockfile.isFile) {
val lockfileData = parseLockfile(lockfile)
scopes += Scope(SCOPE_NAME, lockfileData.dependencies)
packages += scopes.collectDependencies().map {
lockfileData.packagesFromCheckoutOptionsForId[it] ?: getPackage(it, workingDir)
}
} else {
issues += createAndLogIssue(
source = managerName,
message = "Missing lockfile '${lockfile.relativeTo(analysisRoot).invariantSeparatorsPath}' for " +
"definition file '${definitionFile.relativeTo(analysisRoot).invariantSeparatorsPath}'. The " +
"analysis of a Podfile without a lockfile is not supported."
)
}
val projectAnalyzerResult = ProjectAnalyzerResult(
packages = packages,
project = Project(
id = Identifier(
type = managerName,
namespace = "",
name = getFallbackProjectName(analysisRoot, definitionFile),
version = ""
),
definitionFilePath = VersionControlSystem.getPathInfo(definitionFile).path,
authors = emptySet(),
declaredLicenses = emptySet(),
vcs = VcsInfo.EMPTY,
vcsProcessed = processProjectVcs(workingDir),
scopeDependencies = scopes,
homepageUrl = ""
),
issues = issues
)
return listOf(projectAnalyzerResult)
}
private fun getPackage(id: Identifier, workingDir: File): Package {
val podspec = getPodspec(id, workingDir) ?: return Package.EMPTY.copy(id = id, purl = id.toPurl())
val vcs = podspec.source?.git?.let { url ->
VcsInfo(
type = VcsType.GIT,
url = url,
revision = podspec.source.tag.orEmpty()
)
}.orEmpty()
return Package(
id = id,
authors = emptySet(),
declaredLicenses = setOfNotNull(podspec.license.takeUnless { it.isEmpty() }),
description = podspec.summary,
homepageUrl = podspec.homepage,
binaryArtifact = RemoteArtifact.EMPTY,
sourceArtifact = podspec.source?.http?.let { RemoteArtifact(it, Hash.NONE) }.orEmpty(),
vcs = vcs,
vcsProcessed = processPackageVcs(vcs, podspec.homepage)
)
}
private fun getPodspec(id: Identifier, workingDir: File): Podspec? {
podspecCache[id.name]?.let { return it }
val podspecName = id.name.substringBefore("/")
val podspecCommand = runCatching {
run(
"spec", "which", "^$podspecName$",
"--version=${id.version}",
"--allow-root",
"--regex",
workingDir = workingDir
)
}.getOrElse {
val messages = it.collectMessages()
logger.warn {
"Failed to get the '.podspec' file for package '${id.toCoordinates()}': $messages"
}
if ("SSL peer certificate or SSH remote key was not OK" in messages) {
// When running into this error (see e.g. https://github.com/CocoaPods/CocoaPods/issues/11159) abort
// immediately, because connections are retried multiple times for each package's podspec to retrieve
// which would otherwise take a very long time.
throw IOException(messages)
}
return null
}
val podspecFile = File(podspecCommand.stdout.trim())
val podspec = podspecFile.readText().parsePodspec()
podspec.withSubspecs().associateByTo(podspecCache) { it.name }
return podspecCache.getValue(id.name)
}
}
private const val LOCKFILE_FILENAME = "Podfile.lock"
private const val SCOPE_NAME = "dependencies"
private data class LockfileData(
val dependencies: Set,
val packagesFromCheckoutOptionsForId: Map
)
private fun parseLockfile(podfileLock: File): LockfileData {
val lockfile = podfileLock.readText().parseLockfile()
val resolvedVersions = mutableMapOf()
val dependencyConstraints = mutableMapOf>()
// The "PODS" section lists the resolved dependencies and, nested by one level, any version constraints of their
// direct dependencies. That is, the nesting never goes deeper than two levels.
lockfile.pods.map { pod ->
resolvedVersions[pod.name] = checkNotNull(pod.version)
if (pod.dependencies.isNotEmpty()) {
dependencyConstraints[pod.name] = pod.dependencies.mapTo(mutableSetOf()) {
// Discard the version (which is only a constraint in this case) and just take the name.
it.name
}
}
}
val packagesFromCheckoutOptionsForId = lockfile.checkoutOptions.mapNotNull { (name, checkoutOption) ->
val url = checkoutOption.git ?: return@mapNotNull null
val revision = checkoutOption.commit.orEmpty()
// The version written to the lockfile matches the version specified in the project's ".podspec" file at the
// given revision, so the same version might be used in different revisions. To still get a unique identifier,
// append the revision to the version.
val versionFromPodspec = checkNotNull(resolvedVersions[name])
val uniqueVersion = "$versionFromPodspec-$revision"
val id = Identifier("Pod", "", name, uniqueVersion)
// Write the unique version back for correctly associating dependencies below.
resolvedVersions[name] = uniqueVersion
id to Package(
id = id,
declaredLicenses = emptySet(),
description = "",
homepageUrl = url,
binaryArtifact = RemoteArtifact.EMPTY,
sourceArtifact = RemoteArtifact.EMPTY,
vcs = VcsInfo(VcsType.GIT, url, revision)
)
}.toMap()
fun createPackageReference(name: String): PackageReference =
PackageReference(
id = Identifier("Pod", "", name, resolvedVersions.getValue(name)),
dependencies = dependencyConstraints[name].orEmpty().filter {
// Only use a constraint as a dependency if it has a resolved version.
it in resolvedVersions
}.mapTo(mutableSetOf()) {
createPackageReference(it)
}
)
// The "DEPENDENCIES" section lists direct dependencies, but only along with version constraints, not with their
// resolved versions, and eventually additional information about the source.
val dependencies = lockfile.dependencies.mapTo(mutableSetOf()) { dependency ->
// Ignore the version (which is only a constraint in this case) and just take the name.
createPackageReference(dependency.name)
}
return LockfileData(dependencies, packagesFromCheckoutOptionsForId)
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy