Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cocoapods: Separate lockfile deserialization and its model #8890

Merged
merged 5 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 22 additions & 47 deletions plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@

package org.ossreviewtoolkit.plugins.packagemanagers.cocoapods

import com.fasterxml.jackson.databind.node.ObjectNode

import java.io.File
import java.io.IOException

Expand All @@ -45,12 +43,10 @@ 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.model.yamlMapper
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.ossreviewtoolkit.utils.common.textValueOrEmpty

import org.semver4j.RangesList
import org.semver4j.RangesListFactory
Expand Down Expand Up @@ -92,8 +88,8 @@ class CocoaPods(

override fun beforeResolution(definitionFiles: List<File>) = checkVersion()

override fun resolveDependencies(definitionFile: File, labels: Map<String, String>): List<ProjectAnalyzerResult> {
return stashDirectories(Os.userHomeDirectory.resolve(".cocoapods/repos")).use {
override fun resolveDependencies(definitionFile: File, labels: Map<String, String>): List<ProjectAnalyzerResult> =
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")

Expand All @@ -107,7 +103,6 @@ class CocoaPods(
podspecCache.clear()
}
}
}

private fun resolveDependenciesInternal(definitionFile: File): List<ProjectAnalyzerResult> {
val workingDir = definitionFile.parentFile
Expand All @@ -122,7 +117,7 @@ class CocoaPods(

scopes += Scope(SCOPE_NAME, lockfileData.dependencies)
packages += scopes.flatMap { it.collectDependencies() }.map {
lockfileData.externalSources[it] ?: getPackage(it, workingDir)
lockfileData.packagesFromCheckoutOptionsForId[it] ?: getPackage(it, workingDir)
}
} else {
issues += createAndLogIssue(
Expand Down Expand Up @@ -223,61 +218,42 @@ private const val LOCKFILE_FILENAME = "Podfile.lock"

private const val SCOPE_NAME = "dependencies"

private fun parseNameAndVersion(entry: String): Pair<String, String?> {
val info = entry.split(' ', limit = 2)
val name = info[0]

// A version entry could look something like "(6.3.0)", "(= 2021.06.28.00-v2)", "(~> 8.15.0)", etc. Also see
// https://guides.cocoapods.org/syntax/podfile.html#pod.
val version = info.getOrNull(1)?.removeSurrounding("(", ")")

return name to version
}

private data class LockfileData(
val dependencies: Set<PackageReference>,
val externalSources: Map<Identifier, Package>
val packagesFromCheckoutOptionsForId: Map<Identifier, Package>
)

private fun parseLockfile(podfileLock: File): LockfileData {
val lockfile = podfileLock.readText().parseLockfile()
val resolvedVersions = mutableMapOf<String, String>()
val dependencyConstraints = mutableMapOf<String, MutableSet<String>>()
val root = yamlMapper.readTree(podfileLock)

// 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.
root.get("PODS").forEach { node ->
when (node) {
is ObjectNode -> {
val (name, version) = parseNameAndVersion(node.fieldNames().asSequence().single())
resolvedVersions[name] = checkNotNull(version)
dependencyConstraints[name] = node.single().mapTo(mutableSetOf()) {
// Discard the version (which is only a constraint in this case) and just take the name.
parseNameAndVersion(it.textValue()).first
}
}
lockfile.pods.map { pod ->
resolvedVersions[pod.name] = checkNotNull(pod.version)

else -> {
val (name, version) = parseNameAndVersion(node.textValue())
resolvedVersions[name] = checkNotNull(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 externalSources = root.get("CHECKOUT OPTIONS")?.fields()?.asSequence()?.mapNotNull {
val checkout = it.value as ObjectNode
val url = checkout[":git"]?.textValue() ?: return@mapNotNull null
val revision = checkout[":commit"].textValueOrEmpty()
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[it.key])
val versionFromPodspec = checkNotNull(resolvedVersions[name])
val uniqueVersion = "$versionFromPodspec-$revision"
val id = Identifier("Pod", "", it.key, uniqueVersion)
val id = Identifier("Pod", "", name, uniqueVersion)

// Write the unique version back for correctly associating dependencies below.
resolvedVersions[it.key] = uniqueVersion
resolvedVersions[name] = uniqueVersion

id to Package(
id = id,
Expand All @@ -288,7 +264,7 @@ private fun parseLockfile(podfileLock: File): LockfileData {
sourceArtifact = RemoteArtifact.EMPTY,
vcs = VcsInfo(VcsType.GIT, url, revision)
)
}?.toMap()
}.toMap()

fun createPackageReference(name: String): PackageReference =
PackageReference(
Expand All @@ -303,11 +279,10 @@ private fun parseLockfile(podfileLock: File): LockfileData {

// 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 = root.get("DEPENDENCIES").mapTo(mutableSetOf()) { node ->
// Discard the version (which is only a constraint in this case) and just take the name.
val (name, _) = parseNameAndVersion(node.textValue())
createPackageReference(name)
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, externalSources.orEmpty())
return LockfileData(dependencies, packagesFromCheckoutOptionsForId)
}
100 changes: 100 additions & 0 deletions plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.plugins.packagemanagers.cocoapods

import com.fasterxml.jackson.databind.node.ObjectNode

import org.ossreviewtoolkit.model.yamlMapper
import org.ossreviewtoolkit.plugins.packagemanagers.cocoapods.Lockfile.CheckoutOption
import org.ossreviewtoolkit.plugins.packagemanagers.cocoapods.Lockfile.Dependency

internal data class Lockfile(
val pods: List<Pod>,
val checkoutOptions: Map<String, CheckoutOption>,
val dependencies: List<Dependency>
) {
data class Pod(
val name: String,
val version: String? = null,
val dependencies: List<Pod> = emptyList()
)

data class CheckoutOption(
val git: String?,
val commit: String?
)

data class Dependency(
val name: String,
val versionConstraint: String?
)
}

internal fun String.parseLockfile(): Lockfile {
val root = yamlMapper.readTree(this)

val pods = root.get("PODS").map { node ->
when (node) {
is ObjectNode -> {
val (name, version) = parseNameAndVersion(node.fieldNames().asSequence().single())
Lockfile.Pod(
name = name,
version = version,
dependencies = node.single().map {
val (depName, depVersion) = parseNameAndVersion(it.textValue())
Lockfile.Pod(depName, depVersion)
}
)
}

else -> {
val (name, version) = parseNameAndVersion(node.textValue())
Lockfile.Pod(name, version)
}
}
}

val checkoutOptions = root.get("CHECKOUT OPTIONS")?.fields()?.asSequence()?.mapNotNull { (name, node) ->
val checkoutOption = CheckoutOption(
git = node[":git"]?.textValue(),
commit = node[":commit"].textValue()
)

name to checkoutOption
}.orEmpty().toMap()

val dependencies = root.get("DEPENDENCIES").map { node ->
val (name, version) = parseNameAndVersion(node.textValue())
Dependency(name, version)
}

return Lockfile(pods, checkoutOptions, dependencies)
}

private fun parseNameAndVersion(entry: String): Pair<String, String?> {
val info = entry.split(' ', limit = 2)
val name = info[0]

// A version entry could look something like "(6.3.0)", "(= 2021.06.28.00-v2)", "(~> 8.15.0)", etc. Also see
// https://guides.cocoapods.org/syntax/podfile.html#pod.
val version = info.getOrNull(1)?.removeSurrounding("(", ")")

return name to version
}
Loading