Skip to content

Commit

Permalink
refactor(cocoapods): Separate parsing the lockfile
Browse files Browse the repository at this point in the history
Previously, parsing the lockfile and mapping it to ORT's model has all
been done in the single function `parseLockfile(File)`.

Define a model for the lockfile and a function for its parsing in a
separate dedicated file in order to separate concerns. This makes the
code easier to maintain and simplifies the upcoming porting to KxS.

Signed-off-by: Frank Viernau <[email protected]>
  • Loading branch information
fviernau committed Jul 16, 2024
1 parent 35e048f commit ed9ce11
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 37 deletions.
51 changes: 14 additions & 37 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 @@ -223,50 +219,32 @@ 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>
)

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 { (name, checkout) ->
val url = checkout[":git"]?.textValue() ?: return@mapNotNull null
val revision = checkout[":commit"].textValueOrEmpty()
val externalSources = 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,
Expand All @@ -287,7 +265,7 @@ private fun parseLockfile(podfileLock: File): LockfileData {
sourceArtifact = RemoteArtifact.EMPTY,
vcs = VcsInfo(VcsType.GIT, url, revision)
)
}.orEmpty().toMap()
}.toMap()

fun createPackageReference(name: String): PackageReference =
PackageReference(
Expand All @@ -302,10 +280,9 @@ 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)
Expand Down
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
}

0 comments on commit ed9ce11

Please sign in to comment.