Repository: oss-review-toolkit/ort
Pull request #10293: Cocoapods: Add some support for React Native
Author: login: nnobelis
Created: 2025-05-08T09:32:55Z | Merged: 2025-05-09T10:12:40Z

Please have a look at the individual commit messages for the details.


Diff:
diff --git a/plugins/package-managers/cocoapods/build.gradle.kts b/plugins/package-managers/cocoapods/build.gradle.kts
index a343b29f0f885..9f8d44634b948 100644
--- a/plugins/package-managers/cocoapods/build.gradle.kts
+++ b/plugins/package-managers/cocoapods/build.gradle.kts
@@ -47,4 +47,5 @@ dependencies {
     ksp(projects.analyzer)
 
     funTestImplementation(testFixtures(projects.analyzer))
+    funTestImplementation(projects.plugins.packageManagers.nodePackageManager)
 }
diff --git a/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native-expected-output.yml b/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native-expected-output.yml
new file mode 100644
index 0000000000000..0673280fd891e
--- /dev/null
+++ b/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native-expected-output.yml
@@ -0,0 +1,138 @@
+---
+projects:
+- id: "CocoaPods::ios/Podfile:"
+  definition_file_path: "plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/ios/Podfile"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: "Git"
+    url: "<REPLACE_URL_PROCESSED>"
+    revision: "<REPLACE_REVISION>"
+    path: "<REPLACE_PATH>"
+  homepage_url: ""
+  scopes:
+  - name: "dependencies"
+    dependencies:
+    - id: "Pod::React:0.75.4"
+      dependencies:
+      - id: "Pod::React-Core:0.75.4"
+        dependencies:
+        - id: "Pod::glog:0.3.5"
+    - id: "Pod::React-Core:0.75.4"
+      dependencies:
+      - id: "Pod::glog:0.3.5"
+    - id: "Pod::fmt:9.1.0"
+    - id: "Pod::glog:0.3.5"
+packages:
+- id: "Pod::React:0.75.4"
+  purl: "pkg:cocoapods/React@0.75.4"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  description: ""
+  homepage_url: ""
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+- id: "Pod::React-Core:0.75.4"
+  purl: "pkg:cocoapods/React-Core@0.75.4"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  description: ""
+  homepage_url: ""
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+- id: "Pod::fmt:9.1.0"
+  purl: "pkg:cocoapods/fmt@9.1.0"
+  declared_licenses:
+  - "MIT"
+  declared_licenses_processed:
+    spdx_expression: "MIT"
+  description: "{fmt} is an open-source formatting library for C++. It can be used\
+    \ as a safe and fast alternative to (s)printf and iostreams."
+  homepage_url: "https://github.com/fmtlib/fmt"
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: "Git"
+    url: "https://github.com/fmtlib/fmt.git"
+    revision: "9.1.0"
+    path: ""
+  vcs_processed:
+    type: "Git"
+    url: "https://github.com/fmtlib/fmt.git"
+    revision: "9.1.0"
+    path: ""
+- id: "Pod::glog:0.3.5"
+  purl: "pkg:cocoapods/glog@0.3.5"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  description: ""
+  homepage_url: ""
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
diff --git a/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/ios/Podfile b/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/ios/Podfile
new file mode 100644
index 0000000000000..f59f85f32fd83
--- /dev/null
+++ b/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/ios/Podfile
@@ -0,0 +1,6 @@
+target 'example' do
+    pod 'fmt'
+    pod 'glog'
+    pod 'React'
+    pod 'React-Core'
+end
diff --git a/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/ios/Podfile.lock b/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/ios/Podfile.lock
new file mode 100644
index 0000000000000..c0fad640b96dd
--- /dev/null
+++ b/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/ios/Podfile.lock
@@ -0,0 +1,19 @@
+PODS:
+  - glog (0.3.5)
+  - React (0.75.4):
+    - React-Core (= 0.75.4)
+  - React-Core (0.75.4):
+    - glog
+  - fmt (9.1.0)
+
+DEPENDENCIES:
+  - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
+  - React (from `../node_modules/react-native/`)
+  - React-Core (from `../node_modules/react-native/`)
+  - fmt (from `../node_modules/react-native/third-party-podspecs/fmt.podspec`)
+
+EXTERNAL SOURCES:
+  fmt:
+      :podspec: "../node_modules/react-native/third-party-podspecs/fmt.podspec"
+
+COCOAPODS: 1.13.0
diff --git a/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/package.json b/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/package.json
new file mode 100644
index 0000000000000..822475a59ef0a
--- /dev/null
+++ b/plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/package.json
@@ -0,0 +1,8 @@
+{
+  "name": "test project with react native",
+  "version": "1.0.0",
+  "dependencies": {
+    "react": "18.3.1",
+    "react-native": "0.75.4"
+  }
+}
diff --git a/plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt b/plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
index eab0ddec733b3..b7dc28f4f6785 100644
--- a/plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
+++ b/plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
@@ -22,9 +22,13 @@ package org.ossreviewtoolkit.plugins.packagemanagers.cocoapods
 import io.kotest.core.spec.style.WordSpec
 import io.kotest.matchers.should
 
+import org.ossreviewtoolkit.analyzer.analyze
+import org.ossreviewtoolkit.analyzer.getAnalyzerResult
 import org.ossreviewtoolkit.analyzer.resolveSingleProject
 import org.ossreviewtoolkit.analyzer.withInvariantIssues
+import org.ossreviewtoolkit.model.config.PackageManagerConfiguration
 import org.ossreviewtoolkit.model.toYaml
+import org.ossreviewtoolkit.plugins.packagemanagers.node.npm.NpmFactory
 import org.ossreviewtoolkit.utils.test.getAssetFile
 import org.ossreviewtoolkit.utils.test.matchExpectedResult
 
@@ -74,5 +78,33 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a project which is using React Native" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully. Note that any Node manager can be used as CocoaPods just requires the
+            // 'node_modules' directory to be present.
+            val npmDefinitionFile = getAssetFile("projects/synthetic/react-native/package.json")
+            val definitionFile = getAssetFile("projects/synthetic/react-native/ios/Podfile")
+            val expectedResultFile = getAssetFile("projects/synthetic/react-native-expected-output.yml")
+
+            val result = analyze(
+                projectDir = npmDefinitionFile.parentFile,
+                allowDynamicVersions = true,
+                packageManagers = listOf(NpmFactory(), CocoaPodsFactory()),
+                packageManagerConfiguration = mapOf(
+                    "CocoaPods" to PackageManagerConfiguration(mustRunAfter = listOf("NPM"))
+                )
+            ).getAnalyzerResult()
+
+            // The NPM-related results are not relevant, because this test checks if the Pod packages can be filled with
+            // information coming from the podspec files present in the 'node_modules' directory.
+            val analyzerResult = result.copy(
+                projects = result.projects.filterTo(mutableSetOf()) { it.id.type == "CocoaPods" },
+                packages = result.packages.filterTo(mutableSetOf()) { it.id.type == "Pod" },
+                issues = emptyMap()
+            )
+
+            analyzerResult.toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
+        }
     }
 })
diff --git a/plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt b/plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
index 417e074b01f3f..c058b777e7964 100644
--- a/plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
+++ b/plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
@@ -21,6 +21,8 @@ package org.ossreviewtoolkit.plugins.packagemanagers.cocoapods
 
 import java.io.File
 
+import kotlin.io.resolveSibling
+
 import org.ossreviewtoolkit.analyzer.PackageManager
 import org.ossreviewtoolkit.analyzer.PackageManagerFactory
 import org.ossreviewtoolkit.analyzer.PackageManagerResult
@@ -131,8 +133,26 @@ class CocoaPods(override val descriptor: PluginDescriptor = CocoaPodsFactory.des
         if (lockfile.isFile) {
             val lockfileData = lockfile.readText().parseLockfile()
 
+            // Resolve paths of external sources relative to the lockfile.
+            val lockfileWithResolvedPaths = lockfileData.copy(
+                externalSources = lockfileData.externalSources.mapValues { entry ->
+                    Lockfile.ExternalSource(
+                        entry.value.path?.let { lockfile.resolveSibling(it).path },
+                        entry.value.podspec?.let { lockfile.resolveSibling(it).path }
+                    )
+                }
+            )
+
             // Convert direct dependencies with version constraints to pods with resolved versions.
-            val dependencies = lockfileData.dependencies.mapNotNull { it.resolvedPod }
+            val dependencies = lockfileData.dependencies.mapNotNull {
+                it.resolvedPod?.run {
+                    lockfileWithResolvedPaths.Pod(
+                        name,
+                        version,
+                        dependencies
+                    )
+                }
+            }
 
             graphBuilder.addDependencies(projectId, SCOPE_NAME, dependencies)
         } else {
diff --git a/plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt b/plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
index be202ffd45087..2bf35c4c28a32 100644
--- a/plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
+++ b/plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
@@ -22,6 +22,10 @@ package org.ossreviewtoolkit.plugins.packagemanagers.cocoapods
 import java.io.File
 import java.io.IOException
 
+import kotlin.io.path.createTempFile
+import kotlin.io.path.pathString
+import kotlin.io.path.writeText
+
 import org.apache.logging.log4j.kotlin.logger
 
 import org.ossreviewtoolkit.analyzer.PackageManager.Companion.processPackageVcs
@@ -36,6 +40,7 @@ import org.ossreviewtoolkit.model.VcsType
 import org.ossreviewtoolkit.model.orEmpty
 import org.ossreviewtoolkit.model.utils.DependencyHandler
 import org.ossreviewtoolkit.model.utils.toPurl
+import org.ossreviewtoolkit.utils.common.searchUpwardsForSubdirectory
 
 internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
     private val podspecCache = mutableMapOf<String, Podspec>()
@@ -86,7 +91,7 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
                     path?.let { File(it) }?.takeIf { it.isFile }
                 }
 
-                podspecFile?.readText()?.parsePodspec() ?: return Package.EMPTY.copy(id = id, purl = id.toPurl())
+                podspecFile?.parsePodspecFile() ?: return Package.EMPTY.copy(id = id, purl = id.toPurl())
             }
 
             val vcs = podspec.source?.git?.let { url ->
@@ -111,6 +116,43 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            convertRubyPodspecFile(content)
+        } else {
+            content
+        }?.parsePodspec()
+    }
+
+    private fun File.convertRubyPodspecFile(content: String): String? {
+        // The podspec is in Ruby format.
+        // Because it may depend on React Native functions, an extra require may have to be injected.
+        val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+            val reactNativePath =
+                nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+            if (reactNativePath.isFile) {
+                "require '$reactNativePath'\n$content"
+            } else {
+                null
+            }
+        } ?: content
+
+        val patchedPodspecFile = createTempFile("ruby_podspec", ".podspec").apply { writeText(rubyContent) }
+
+        return runCatching {
+            // Convert the Ruby podspec file to JSON.
+            CocoaPodsCommand.run(parentFile, "ipc", "spec", patchedPodspecFile.pathString)
+                .requireSuccess()
+                .stdout
+        }.onFailure { e ->
+            logger.warn {
+                "Failed to process the '.podspec' file in Ruby format at '$canonicalPath': ${e.message.orEmpty()}"
+            }
+        }.getOrNull()
+    }
+
     private fun getPodspecPath(name: String, version: String): String? {
         val podspecProcess = CocoaPodsCommand.run(
             "spec", "which", "^$name$",


Reviews:
user: login: fviernau
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T09:44:34Z
user: login: sschuberth
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-08T09:50:08Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T10:05:26Z
user: login: fviernau
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-08T10:16:43Z
user: login: fviernau
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T10:20:11Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T11:18:54Z
user: login: fviernau
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T11:55:26Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T12:44:36Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T12:53:37Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:00:04Z
user: login: sschuberth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:11:16Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:11:59Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:12:53Z
user: login: sschuberth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-08T13:39:36Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T06:34:03Z
user: login: sschuberth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T07:02:03Z
user: login: sschuberth
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-05-09T07:15:30Z
user: login: fviernau
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-05-09T07:17:00Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T07:30:41Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T07:37:23Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T07:40:00Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T07:44:12Z
user: login: fviernau
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T07:45:34Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T08:01:12Z
user: login: fviernau
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T08:24:24Z
user: login: sschuberth
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T08:42:12Z
user: login: nnobelis
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-09T08:48:47Z
user: login: fviernau
state: APPROVED
author_association: MEMBER
submitted_at: 2025-05-09T09:13:15Z

Review comments:
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: sschuberth
body: You should be able to omit `this.`.
created_at: 2025-05-08T09:39:19Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: sschuberth
body: Let's use `?.let { resolveSibling(it).path }` here and below.
created_at: 2025-05-08T09:42:08Z
author_association: MEMBER
diff_hunk: @@ -74,5 +78,29 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a Podfile located in a subdirectory referring to npm packages" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully.
+
path: plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
user: login: sschuberth
body: Nit: Drop this empty line.
created_at: 2025-05-08T09:43:26Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: fviernau
body: I believe the new functionality goes beyond just parsing the lockfile and therefore should not be part of this file.
Can path resolution be done outside of this file, e.g. in CocoaPods?



created_at: 2025-05-08T09:44:27Z
author_association: MEMBER
diff_hunk: @@ -74,5 +78,29 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a Podfile located in a subdirectory referring to npm packages" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully.
+
+            val npmDefinitionFile = getAssetFile("projects/synthetic/subdirectory/package.json")
+            val result = analyze(
+                npmDefinitionFile.parentFile,
+                true,
+                packageManagers = listOf(NpmFactory(), CocoaPodsFactory()),
+                packageManagerConfiguration = mapOf("CocoaPods" to PackageManagerConfiguration(listOf("NPM")))
+            ).getAnalyzerResult()
+
+            // The NPM-related results are not relevant for this test so they can be discarded.
+            val analyzerResult = result.copy(
+                projects = result.projects.filter { it.id.type == "CocoaPods" }.toSet(),
+                packages = result.packages.filter { it.id.type == "Pod" }.toSet(),
path: plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
user: login: sschuberth
body: Use `.filterTo(mutableSetOf())` instead of the final `.toSet()`.
created_at: 2025-05-08T09:46:05Z
author_association: MEMBER
diff_hunk: @@ -111,6 +115,43 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            // The podspec is in Ruby format.
+            // Because it may depend on React Native functions, an extra require my have to be injected.
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: sschuberth
body: Typo: s/my/may/? Or better "might"?
created_at: 2025-05-08T09:46:54Z
author_association: MEMBER
diff_hunk: @@ -111,6 +115,43 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            // The podspec is in Ruby format.
+            // Because it may depend on React Native functions, an extra require my have to be injected.
+            val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+                val reactNativePath =
+                    nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+                if (reactNativePath.isFile) {
+                    "require '$reactNativePath'\n$content"
+                } else {
+                    null
+                }
+            } ?: content
+
+            val tempRubyPath = createTempFile("ruby_podspec", ".podspec").also { it.writeText(rubyContent) }
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: sschuberth
body: Here, using `apply` is slightly slicker than `also`.
created_at: 2025-05-08T09:48:02Z
author_association: MEMBER
diff_hunk: @@ -111,6 +115,43 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            // The podspec is in Ruby format.
+            // Because it may depend on React Native functions, an extra require my have to be injected.
+            val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+                val reactNativePath =
+                    nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+                if (reactNativePath.isFile) {
+                    "require '$reactNativePath'\n$content"
+                } else {
+                    null
+                }
+            } ?: content
+
+            val tempRubyPath = createTempFile("ruby_podspec", ".podspec").also { it.writeText(rubyContent) }
+
+            val podspecProcess = CocoaPodsCommand.run(
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: sschuberth
body: Please add a code comment what this does, even if mentioned in the commit message.
created_at: 2025-05-08T09:48:34Z
author_association: MEMBER
diff_hunk: @@ -111,6 +115,43 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            // The podspec is in Ruby format.
+            // Because it may depend on React Native functions, an extra require my have to be injected.
+            val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+                val reactNativePath =
+                    nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+                if (reactNativePath.isFile) {
+                    "require '$reactNativePath'\n$content"
+                } else {
+                    null
+                }
+            } ?: content
+
+            val tempRubyPath = createTempFile("ruby_podspec", ".podspec").also { it.writeText(rubyContent) }
+
+            val podspecProcess = CocoaPodsCommand.run(
+                parentFile, "ipc", "spec", tempRubyPath.toString()
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: sschuberth
body: Maybe: `.path` instead of `.toString()` for clarity?
created_at: 2025-05-08T09:48:58Z
author_association: MEMBER
diff_hunk: @@ -111,6 +115,43 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            // The podspec is in Ruby format.
+            // Because it may depend on React Native functions, an extra require my have to be injected.
+            val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+                val reactNativePath =
+                    nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+                if (reactNativePath.isFile) {
+                    "require '$reactNativePath'\n$content"
+                } else {
+                    null
+                }
+            } ?: content
+
+            val tempRubyPath = createTempFile("ruby_podspec", ".podspec").also { it.writeText(rubyContent) }
+
+            val podspecProcess = CocoaPodsCommand.run(
+                parentFile, "ipc", "spec", tempRubyPath.toString()
+            )
+
+            if (podspecProcess.isError) {
+                logger.warn {
+                    "Failed to process the '.podspec' file in Ruby format at '$canonicalPath': " +
+                        podspecProcess.errorMessage
+                }
+
+                null
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: sschuberth
body: This is unused. I guess you forgot an `else` for the code below?
created_at: 2025-05-08T09:50:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+---
path: plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/subdirectory-expected-output.yml
user: login: fviernau
body: The test project name is `subdirectory`, but is this the primary concern? Would it make sense to have some `npm` mention instead?
created_at: 2025-05-08T10:04:02Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: nnobelis
body: The problem is that the `path` and `podspec` properties are used by the `PodDependencyHandler`. I cannot add a reference to the lock file to the handler because it is build during the initialization of the `CocoaPods` class (I guess because the handler maintains a cache of the `Podspec`s).
Additionally, I cannot pass the lock file to any of the functions of the handler because they are fixed by the interface `DependencyHandler`.

Therefore, the only solution I found was to put the resolution in the `Lockfile` class. If you have any other idea...
created_at: 2025-05-08T10:05:26Z
author_association: MEMBER
diff_hunk: @@ -74,5 +78,29 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a Podfile located in a subdirectory referring to npm packages" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully.
+
+            val npmDefinitionFile = getAssetFile("projects/synthetic/subdirectory/package.json")
+            val result = analyze(
+                npmDefinitionFile.parentFile,
+                true,
+                packageManagers = listOf(NpmFactory(), CocoaPodsFactory()),
path: plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
user: login: fviernau
body: Is NPM always the package manager used in this scenario, or could it be other node mgr?
created_at: 2025-05-08T10:07:11Z
author_association: MEMBER
diff_hunk: @@ -74,5 +78,29 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a Podfile located in a subdirectory referring to npm packages" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully.
+
+            val npmDefinitionFile = getAssetFile("projects/synthetic/subdirectory/package.json")
+            val result = analyze(
+                npmDefinitionFile.parentFile,
+                true,
+                packageManagers = listOf(NpmFactory(), CocoaPodsFactory()),
+                packageManagerConfiguration = mapOf("CocoaPods" to PackageManagerConfiguration(listOf("NPM")))
path: plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
user: login: fviernau
body: Let's used named parameters with `PackageManagerConfiguration` so that it's more readable.
created_at: 2025-05-08T10:09:08Z
author_association: MEMBER
diff_hunk: @@ -74,5 +78,29 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a Podfile located in a subdirectory referring to npm packages" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully.
+
+            val npmDefinitionFile = getAssetFile("projects/synthetic/subdirectory/package.json")
+            val result = analyze(
+                npmDefinitionFile.parentFile,
+                true,
+                packageManagers = listOf(NpmFactory(), CocoaPodsFactory()),
+                packageManagerConfiguration = mapOf("CocoaPods" to PackageManagerConfiguration(listOf("NPM")))
+            ).getAnalyzerResult()
+
+            // The NPM-related results are not relevant for this test so they can be discarded.
path: plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
user: login: fviernau
body: Propose to change the comment to: "The NPM-related results are not relevant, because ..."

(To me it's not obvious why these are not relevant)
created_at: 2025-05-08T10:09:56Z
author_association: MEMBER
diff_hunk: @@ -74,5 +78,29 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a Podfile located in a subdirectory referring to npm packages" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully.
+
+            val npmDefinitionFile = getAssetFile("projects/synthetic/subdirectory/package.json")
+            val result = analyze(
+                npmDefinitionFile.parentFile,
+                true,
+                packageManagers = listOf(NpmFactory(), CocoaPodsFactory()),
+                packageManagerConfiguration = mapOf("CocoaPods" to PackageManagerConfiguration(listOf("NPM")))
+            ).getAnalyzerResult()
+
+            // The NPM-related results are not relevant for this test so they can be discarded.
+            val analyzerResult = result.copy(
+                projects = result.projects.filter { it.id.type == "CocoaPods" }.toSet(),
+                packages = result.packages.filter { it.id.type == "Pod" }.toSet(),
+                issues = emptyMap()
+            )
+            val definitionFile = getAssetFile("projects/synthetic/subdirectory/ios/Podfile")
+            val expectedResultFile = getAssetFile("projects/synthetic/subdirectory-expected-output.yml")
path: plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
user: login: fviernau
body: Line 100, 101 we typically have defined before the `act` phase of the test (arrange, act, asset). 
created_at: 2025-05-08T10:11:11Z
author_association: MEMBER
diff_hunk: @@ -111,6 +115,43 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: fviernau
body: Please extract the content of this if block to a separate function for readability.
created_at: 2025-05-08T10:12:06Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: fviernau
body: I see. Would it be an option to just define `private fun Podspec.resolvedRelativePaths(base: File): Podspec`inside the `handler`?

This way we keep that majority of the logic file agnostic, so that you can run it given any string, as this can be convenient.
created_at: 2025-05-08T10:20:11Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: nnobelis
body: But the handler does not know anything about the lockfile nor its path. So I wouldn't know which value to pass to this `base` parameter.
created_at: 2025-05-08T11:18:53Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: fviernau
body: I see. (I misunderstood your above comment and assumed the handler would be the one calling the parse function). However, would it work if the function was in `CocoaPods.kt`, as the only caller seems to be here ? https://github.com/oss-review-toolkit/ort/blob/737038d25ed9fa426e6f27e98fa14b80b1180036/plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt#L132
created_at: 2025-05-08T11:55:26Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: nnobelis
body: So to be clear, you suggest something like `lockfile.readText().parseLockfile().resolveRelativePaths(lockfile)` were you postprocess the parsed `Lockfile` to resolve all the paths, at the cost of copying the `Lockfile` object ?

Or maybe more elegant:
```
val dependencies = lockfileData.dependencies.mapNotNull {
  it.resolvedPod.withResolvesRelativePaths(lockfile)
}
```
created_at: 2025-05-08T12:44:36Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: nnobelis
body: Ok but without the `it`, as it shadows the one of the enclosing `.map`.
created_at: 2025-05-08T12:53:37Z
author_association: MEMBER
diff_hunk: @@ -111,6 +115,43 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            // The podspec is in Ruby format.
+            // Because it may depend on React Native functions, an extra require my have to be injected.
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: nnobelis
body: I think _may_ to keep the tense consistency.
created_at: 2025-05-08T13:00:04Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: sschuberth
body: Ok, but maybe call the lamda arguments `path` and `podspec`, after their fields, instead of `rawPath`?
created_at: 2025-05-08T13:11:16Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+---
path: plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/subdirectory-expected-output.yml
user: login: nnobelis
body: Since this test covers both commits, I think react-native is a better name (I didn't find another `node_modules` usage with Cocoapods nor podspec files in Ruby ).
created_at: 2025-05-08T13:11:59Z
author_association: MEMBER
diff_hunk: @@ -74,5 +78,29 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a Podfile located in a subdirectory referring to npm packages" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully.
+
+            val npmDefinitionFile = getAssetFile("projects/synthetic/subdirectory/package.json")
+            val result = analyze(
+                npmDefinitionFile.parentFile,
+                true,
+                packageManagers = listOf(NpmFactory(), CocoaPodsFactory()),
path: plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
user: login: nnobelis
body: No any node manager works. Our customer uses Yarn.

I wrote the test with NPM for simplicity.
created_at: 2025-05-08T13:12:53Z
author_association: MEMBER
diff_hunk: @@ -74,5 +78,29 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a Podfile located in a subdirectory referring to npm packages" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully.
+
+            val npmDefinitionFile = getAssetFile("projects/synthetic/subdirectory/package.json")
+            val result = analyze(
+                npmDefinitionFile.parentFile,
+                true,
+                packageManagers = listOf(NpmFactory(), CocoaPodsFactory()),
+                packageManagerConfiguration = mapOf("CocoaPods" to PackageManagerConfiguration(listOf("NPM")))
path: plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
user: login: sschuberth
body: While at it, you should do the same esp. for the boolean argument in line 89.
created_at: 2025-05-08T13:39:36Z
author_association: MEMBER
diff_hunk: @@ -95,16 +97,16 @@ internal data class Lockfile(
     )
 }
 
-internal fun String.parseLockfile(): Lockfile {
-    val root = Yaml.default.parseToYamlNode(this).yamlMap
+internal fun File.parseLockfile(): Lockfile {
+    val root = Yaml.default.parseToYamlNode(this.readText()).yamlMap
 
     val externalSources = root.get<YamlMap>("EXTERNAL SOURCES")?.entries.orEmpty().map {
         val name = it.key.content
         val node = it.value.yamlMap
 
         val externalSource = ExternalSource(
-            path = node.get<YamlScalar>(":path")?.content,
-            podspec = node.get<YamlScalar>(":podspec")?.content
+            path = node.get<YamlScalar>(":path")?.content?.let { rawPath -> parentFile.resolve(rawPath).path },
+            podspec = node.get<YamlScalar>(":podspec")?.content?.let { rawPath -> parentFile.resolve(rawPath).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/Lockfile.kt
user: login: nnobelis
body: @fviernau  Please have a look. I think I fixed it.
created_at: 2025-05-09T06:34:03Z
author_association: MEMBER
diff_hunk: @@ -131,8 +133,26 @@ class CocoaPods(override val descriptor: PluginDescriptor = CocoaPodsFactory.des
         if (lockfile.isFile) {
             val lockfileData = lockfile.readText().parseLockfile()
 
+            // Resolve paths of external sources relative to the lockfile.
+            val lockfileWithResolvedPaths = lockfileData.copy(
+                externalSources = lockfileData.externalSources.mapValues {
+                    Lockfile.ExternalSource(
+                        it.value.path?.let { path -> lockfile.resolveSibling(path).path },
+                        it.value.podspec?.let { podspec -> lockfile.resolveSibling(podspec).path }
path: plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
user: login: sschuberth
body: To follow up on the `it` shadowing topic: I wonder if it makes more sense then to name only the outer lambda parameter like `.mapValues { source ->` and stick to `it` for both inner `let` calls.
created_at: 2025-05-09T06:44:16Z
author_association: MEMBER
diff_hunk: @@ -131,8 +133,26 @@ class CocoaPods(override val descriptor: PluginDescriptor = CocoaPodsFactory.des
         if (lockfile.isFile) {
             val lockfileData = lockfile.readText().parseLockfile()
 
+            // Resolve paths of external sources relative to the lockfile.
+            val lockfileWithResolvedPaths = lockfileData.copy(
+                externalSources = lockfileData.externalSources.mapValues {
+                    Lockfile.ExternalSource(
+                        it.value.path?.let { path -> lockfile.resolveSibling(path).path },
+                        it.value.podspec?.let { podspec -> lockfile.resolveSibling(podspec).path }
+                    )
+                }
+            )
+
             // Convert direct dependencies with version constraints to pods with resolved versions.
-            val dependencies = lockfileData.dependencies.mapNotNull { it.resolvedPod }
+            val dependencies = lockfileData.dependencies.mapNotNull {
+                it.resolvedPod?.run {
+                    lockfileWithResolvedPaths.Pod(
path: plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
user: login: fviernau
body: Should above line actually be `Lockfile.Pod(` instead?
created_at: 2025-05-09T06:44:56Z
author_association: MEMBER
diff_hunk: @@ -131,8 +133,26 @@ class CocoaPods(override val descriptor: PluginDescriptor = CocoaPodsFactory.des
         if (lockfile.isFile) {
             val lockfileData = lockfile.readText().parseLockfile()
 
+            // Resolve paths of external sources relative to the lockfile.
+            val lockfileWithResolvedPaths = lockfileData.copy(
+                externalSources = lockfileData.externalSources.mapValues {
+                    Lockfile.ExternalSource(
+                        it.value.path?.let { path -> lockfile.resolveSibling(path).path },
+                        it.value.podspec?.let { podspec -> lockfile.resolveSibling(podspec).path }
+                    )
+                }
+            )
+
             // Convert direct dependencies with version constraints to pods with resolved versions.
-            val dependencies = lockfileData.dependencies.mapNotNull { it.resolvedPod }
+            val dependencies = lockfileData.dependencies.mapNotNull {
path: plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
user: login: fviernau
body: Not sure if I understand this correctly, but should this now operate on `lockfileWithResolvedPaths` instead of `lockfileData` ? (And would this make below modifications unnecessary?
created_at: 2025-05-09T06:47:42Z
author_association: MEMBER
diff_hunk: @@ -131,8 +133,26 @@ class CocoaPods(override val descriptor: PluginDescriptor = CocoaPodsFactory.des
         if (lockfile.isFile) {
             val lockfileData = lockfile.readText().parseLockfile()
 
+            // Resolve paths of external sources relative to the lockfile.
+            val lockfileWithResolvedPaths = lockfileData.copy(
+                externalSources = lockfileData.externalSources.mapValues {
+                    Lockfile.ExternalSource(
+                        it.value.path?.let { path -> lockfile.resolveSibling(path).path },
+                        it.value.podspec?.let { podspec -> lockfile.resolveSibling(podspec).path }
+                    )
+                }
+            )
+
             // Convert direct dependencies with version constraints to pods with resolved versions.
-            val dependencies = lockfileData.dependencies.mapNotNull { it.resolvedPod }
+            val dependencies = lockfileData.dependencies.mapNotNull {
+                it.resolvedPod?.run {
+                    lockfileWithResolvedPaths.Pod(
+                        name,
+                        version,
+                        dependencies
+                    )
path: plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
user: login: fviernau
body: Does the above change actually have impact on the functionality? (Can you give a hint)
created_at: 2025-05-09T06:48:44Z
author_association: MEMBER
diff_hunk: @@ -74,5 +78,29 @@ class CocoaPodsFunTest : WordSpec({
 
             result.withInvariantIssues().toYaml() should matchExpectedResult(expectedResultFile, definitionFile)
         }
+
+        "determine dependencies from a Podfile located in a subdirectory referring to npm packages" {
+            // The NPM package manager is run first to initialize the node_modules directory, which is required to
+            // parse the Podfile successfully.
+
+            val npmDefinitionFile = getAssetFile("projects/synthetic/subdirectory/package.json")
+            val result = analyze(
+                npmDefinitionFile.parentFile,
+                true,
+                packageManagers = listOf(NpmFactory(), CocoaPodsFactory()),
path: plugins/package-managers/cocoapods/src/funTest/kotlin/CocoaPodsFunTest.kt
user: login: fviernau
body: Consider adding this info to the code comment on lines 83 / 84.
created_at: 2025-05-09T06:50:16Z
author_association: MEMBER
diff_hunk: @@ -111,6 +116,48 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            convertRubyPodspecFile(content)
+        } else {
+            content
+        }?.parsePodspec()
+    }
+
+    private fun File.convertRubyPodspecFile(content: String): String? {
+        // The podspec is in Ruby format.
+        // Because it may depend on React Native functions, an extra require may have to be injected.
+        val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+            val reactNativePath =
+                nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+            if (reactNativePath.isFile) {
+                "require '$reactNativePath'\n$content"
+            } else {
+                null
+            }
+        } ?: content
+
+        val tempRubyPath = createTempFile("ruby_podspec", ".podspec").apply { writeText(rubyContent) }
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: fviernau
body: Consider renming `tempRubyPath` -> `patchedPodspecFile`
created_at: 2025-05-09T07:03:05Z
author_association: MEMBER
diff_hunk: @@ -111,6 +116,48 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            convertRubyPodspecFile(content)
+        } else {
+            content
+        }?.parsePodspec()
+    }
+
+    private fun File.convertRubyPodspecFile(content: String): String? {
+        // The podspec is in Ruby format.
+        // Because it may depend on React Native functions, an extra require may have to be injected.
+        val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+            val reactNativePath =
+                nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+            if (reactNativePath.isFile) {
+                "require '$reactNativePath'\n$content"
+            } else {
+                null
+            }
+        } ?: content
+
+        val tempRubyPath = createTempFile("ruby_podspec", ".podspec").apply { writeText(rubyContent) }
+
+        // Convert the Ruby podspec file to JSON.
+        val podspecProcess = CocoaPodsCommand.run(
+            parentFile, "ipc", "spec", tempRubyPath.pathString
+        )
+
+        return if (podspecProcess.isError) {
+            logger.warn {
+                "Failed to process the '.podspec' file in Ruby format at '$canonicalPath': " +
+                    podspecProcess.errorMessage
+            }
+
+            null
+        } else {
+            podspecProcess.stdout
+        }
+    }
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: fviernau
body: Maybe this can be a bit more readable?
```
return runCatching {
   // Convert the Ruby podspec file to JSON.
   val process = CocoaPodsCommand.run(parentFile, "ipc", "spec", tempRubyPath.pathString)
          .requireSuccess()
          .stdout
}.onFailure { e ->
    logger.warn {
        "Failed to process the '.podspec' file in Ruby format at '$canonicalPath': ${e.message.orEmpty()}"
    
    }
}.getOrNull()
```
created_at: 2025-05-09T07:08:17Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+---
+projects:
+- id: "CocoaPods::ios/Podfile:"
+  definition_file_path: "plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/ios/Podfile"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: "Git"
+    url: "<REPLACE_URL_PROCESSED>"
+    revision: "<REPLACE_REVISION>"
+    path: "<REPLACE_PATH>"
+  homepage_url: ""
+  scopes:
+  - name: "dependencies"
+    dependencies:
+    - id: "Pod::React:0.75.4"
+      dependencies:
+      - id: "Pod::React-Core:0.75.4"
+        dependencies:
+        - id: "Pod::glog:0.3.5"
+    - id: "Pod::React-Core:0.75.4"
+      dependencies:
+      - id: "Pod::glog:0.3.5"
+    - id: "Pod::fmt:9.1.0"
+    - id: "Pod::glog:0.3.5"
+packages:
+- id: "Pod::React:0.75.4"
+  purl: "pkg:cocoapods/React@0.75.4"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  description: ""
+  homepage_url: ""
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+- id: "Pod::React-Core:0.75.4"
+  purl: "pkg:cocoapods/React-Core@0.75.4"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  description: ""
+  homepage_url: ""
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+- id: "Pod::fmt:9.1.0"
+  purl: "pkg:cocoapods/fmt@9.1.0"
+  declared_licenses:
+  - "MIT"
+  declared_licenses_processed:
+    spdx_expression: "MIT"
+  description: "{fmt} is an open-source formatting library for C++. It can be used\
+    \ as a safe and fast alternative to (s)printf and iostreams."
+  homepage_url: "https://github.com/fmtlib/fmt"
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: "Git"
+    url: "https://github.com/fmtlib/fmt.git"
+    revision: "9.1.0"
+    path: ""
+  vcs_processed:
+    type: "Git"
+    url: "https://github.com/fmtlib/fmt.git"
+    revision: "9.1.0"
+    path: ""
+- id: "Pod::glog:0.3.5"
+  purl: "pkg:cocoapods/glog@0.3.5"
+  declared_licenses: []
path: plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native-expected-output.yml
user: login: fviernau
body: IIUC set of packages which get parsed via the new ruby logic are: glog, React, React Core, fmt.

Looking at this file, `fmt` seems to be the only package which has metadata. 
Can you give a hint _why_ the other don't have any metadata?
created_at: 2025-05-09T07:16:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,138 @@
+---
+projects:
+- id: "CocoaPods::ios/Podfile:"
+  definition_file_path: "plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native/ios/Podfile"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: "Git"
+    url: "<REPLACE_URL_PROCESSED>"
+    revision: "<REPLACE_REVISION>"
+    path: "<REPLACE_PATH>"
+  homepage_url: ""
+  scopes:
+  - name: "dependencies"
+    dependencies:
+    - id: "Pod::React:0.75.4"
+      dependencies:
+      - id: "Pod::React-Core:0.75.4"
+        dependencies:
+        - id: "Pod::glog:0.3.5"
+    - id: "Pod::React-Core:0.75.4"
+      dependencies:
+      - id: "Pod::glog:0.3.5"
+    - id: "Pod::fmt:9.1.0"
+    - id: "Pod::glog:0.3.5"
+packages:
+- id: "Pod::React:0.75.4"
+  purl: "pkg:cocoapods/React@0.75.4"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  description: ""
+  homepage_url: ""
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+- id: "Pod::React-Core:0.75.4"
+  purl: "pkg:cocoapods/React-Core@0.75.4"
+  declared_licenses: []
+  declared_licenses_processed: {}
+  description: ""
+  homepage_url: ""
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+  vcs_processed:
+    type: ""
+    url: ""
+    revision: ""
+    path: ""
+- id: "Pod::fmt:9.1.0"
+  purl: "pkg:cocoapods/fmt@9.1.0"
+  declared_licenses:
+  - "MIT"
+  declared_licenses_processed:
+    spdx_expression: "MIT"
+  description: "{fmt} is an open-source formatting library for C++. It can be used\
+    \ as a safe and fast alternative to (s)printf and iostreams."
+  homepage_url: "https://github.com/fmtlib/fmt"
+  binary_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  source_artifact:
+    url: ""
+    hash:
+      value: ""
+      algorithm: ""
+  vcs:
+    type: "Git"
+    url: "https://github.com/fmtlib/fmt.git"
+    revision: "9.1.0"
+    path: ""
+  vcs_processed:
+    type: "Git"
+    url: "https://github.com/fmtlib/fmt.git"
+    revision: "9.1.0"
+    path: ""
+- id: "Pod::glog:0.3.5"
+  purl: "pkg:cocoapods/glog@0.3.5"
+  declared_licenses: []
path: plugins/package-managers/cocoapods/src/funTest/assets/projects/synthetic/react-native-expected-output.yml
user: login: nnobelis
body: Because `fmt` is the only one with an entry in `EXTERNAL SOURCES` in the `Podfile.lock`.

All the React Native pods' podspec files are not coming from the Cocoapod cache as it is usual, but rather from the external source section.
I plan in a future commit to add support for `EXTERNAL SOURCES/<podName>/:path:` and will then complete the test.
created_at: 2025-05-09T07:30:41Z
author_association: MEMBER
diff_hunk: @@ -131,8 +133,26 @@ class CocoaPods(override val descriptor: PluginDescriptor = CocoaPodsFactory.des
         if (lockfile.isFile) {
             val lockfileData = lockfile.readText().parseLockfile()
 
+            // Resolve paths of external sources relative to the lockfile.
+            val lockfileWithResolvedPaths = lockfileData.copy(
+                externalSources = lockfileData.externalSources.mapValues {
+                    Lockfile.ExternalSource(
+                        it.value.path?.let { path -> lockfile.resolveSibling(path).path },
+                        it.value.podspec?.let { podspec -> lockfile.resolveSibling(podspec).path }
+                    )
+                }
+            )
+
             // Convert direct dependencies with version constraints to pods with resolved versions.
-            val dependencies = lockfileData.dependencies.mapNotNull { it.resolvedPod }
+            val dependencies = lockfileData.dependencies.mapNotNull {
+                it.resolvedPod?.run {
+                    lockfileWithResolvedPaths.Pod(
path: plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
user: login: nnobelis
body: `Pod` is an _inner_ class of `Lockfile`, hence to build an instance of it you need an instance of `Lockfile`.

Therefore the code clones the `Lockfile` and resolves the paths it contains, this is `lockfileWithResolvedPaths`.
Then all `Pod`s are "rebuilt" using  `lockfileWithResolvedPaths`'s `Pod` constructor to have the updated paths.

So no, the code is correct.
created_at: 2025-05-09T07:37:23Z
author_association: MEMBER
diff_hunk: @@ -131,8 +133,26 @@ class CocoaPods(override val descriptor: PluginDescriptor = CocoaPodsFactory.des
         if (lockfile.isFile) {
             val lockfileData = lockfile.readText().parseLockfile()
 
+            // Resolve paths of external sources relative to the lockfile.
+            val lockfileWithResolvedPaths = lockfileData.copy(
+                externalSources = lockfileData.externalSources.mapValues {
+                    Lockfile.ExternalSource(
+                        it.value.path?.let { path -> lockfile.resolveSibling(path).path },
+                        it.value.podspec?.let { podspec -> lockfile.resolveSibling(podspec).path }
+                    )
+                }
+            )
+
             // Convert direct dependencies with version constraints to pods with resolved versions.
-            val dependencies = lockfileData.dependencies.mapNotNull { it.resolvedPod }
+            val dependencies = lockfileData.dependencies.mapNotNull {
path: plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
user: login: nnobelis
body: At this point in the code, `lockfileWithResolvedPaths` and `lockfileData` contains the same `dependencies`. Their only difference is `Lockfile.externalSources`.

So the code can work on either, I prefer "the old one" for clarity.

created_at: 2025-05-09T07:40:00Z
author_association: MEMBER
diff_hunk: @@ -131,8 +133,26 @@ class CocoaPods(override val descriptor: PluginDescriptor = CocoaPodsFactory.des
         if (lockfile.isFile) {
             val lockfileData = lockfile.readText().parseLockfile()
 
+            // Resolve paths of external sources relative to the lockfile.
+            val lockfileWithResolvedPaths = lockfileData.copy(
+                externalSources = lockfileData.externalSources.mapValues {
+                    Lockfile.ExternalSource(
+                        it.value.path?.let { path -> lockfile.resolveSibling(path).path },
+                        it.value.podspec?.let { podspec -> lockfile.resolveSibling(podspec).path }
+                    )
+                }
+            )
+
             // Convert direct dependencies with version constraints to pods with resolved versions.
-            val dependencies = lockfileData.dependencies.mapNotNull { it.resolvedPod }
+            val dependencies = lockfileData.dependencies.mapNotNull {
+                it.resolvedPod?.run {
+                    lockfileWithResolvedPaths.Pod(
+                        name,
+                        version,
+                        dependencies
+                    )
path: plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
user: login: nnobelis
body: The change just rewrites the paths.

Their was some tests with external source configured in the module, but they were only with `:git:` and `:branch' `.

I am not sure projects with `:path:` or `:podspec:` even worked before this PR.
created_at: 2025-05-09T07:44:12Z
author_association: MEMBER
diff_hunk: @@ -131,8 +133,26 @@ class CocoaPods(override val descriptor: PluginDescriptor = CocoaPodsFactory.des
         if (lockfile.isFile) {
             val lockfileData = lockfile.readText().parseLockfile()
 
+            // Resolve paths of external sources relative to the lockfile.
+            val lockfileWithResolvedPaths = lockfileData.copy(
+                externalSources = lockfileData.externalSources.mapValues {
+                    Lockfile.ExternalSource(
+                        it.value.path?.let { path -> lockfile.resolveSibling(path).path },
+                        it.value.podspec?.let { podspec -> lockfile.resolveSibling(podspec).path }
+                    )
+                }
+            )
+
             // Convert direct dependencies with version constraints to pods with resolved versions.
-            val dependencies = lockfileData.dependencies.mapNotNull { it.resolvedPod }
+            val dependencies = lockfileData.dependencies.mapNotNull {
+                it.resolvedPod?.run {
+                    lockfileWithResolvedPaths.Pod(
path: plugins/package-managers/cocoapods/src/main/kotlin/CocoaPods.kt
user: login: fviernau
body: Ah, hadn't noticed it became an `inner` class, here: https://github.com/oss-review-toolkit/ort/pull/9692.

Then let's leave the code as is, I'll take a look after this PR is merged, if this can be made a non inner class again, to make above code simpler and more importantly to clearly separate upstream data model / parsing, from mapping things to ORT's model. It's become mixed again.
created_at: 2025-05-09T07:45:33Z
author_association: MEMBER
diff_hunk: @@ -111,6 +116,48 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            convertRubyPodspecFile(content)
+        } else {
+            content
+        }?.parsePodspec()
+    }
+
+    private fun File.convertRubyPodspecFile(content: String): String? {
+        // The podspec is in Ruby format.
+        // Because it may depend on React Native functions, an extra require may have to be injected.
+        val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+            val reactNativePath =
+                nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+            if (reactNativePath.isFile) {
+                "require '$reactNativePath'\n$content"
+            } else {
+                null
+            }
+        } ?: content
+
+        val tempRubyPath = createTempFile("ruby_podspec", ".podspec").apply { writeText(rubyContent) }
+
+        // Convert the Ruby podspec file to JSON.
+        val podspecProcess = CocoaPodsCommand.run(
+            parentFile, "ipc", "spec", tempRubyPath.pathString
+        )
+
+        return if (podspecProcess.isError) {
+            logger.warn {
+                "Failed to process the '.podspec' file in Ruby format at '$canonicalPath': " +
+                    podspecProcess.errorMessage
+            }
+
+            null
+        } else {
+            podspecProcess.stdout
+        }
+    }
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: nnobelis
body: You cannot access `process.errorMessage` in `onFailure`, as `process` is scoped to `runCatching`.
created_at: 2025-05-09T08:01:12Z
author_association: MEMBER
diff_hunk: @@ -111,6 +116,48 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            convertRubyPodspecFile(content)
+        } else {
+            content
+        }?.parsePodspec()
+    }
+
+    private fun File.convertRubyPodspecFile(content: String): String? {
+        // The podspec is in Ruby format.
+        // Because it may depend on React Native functions, an extra require may have to be injected.
+        val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+            val reactNativePath =
+                nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+            if (reactNativePath.isFile) {
+                "require '$reactNativePath'\n$content"
+            } else {
+                null
+            }
+        } ?: content
+
+        val tempRubyPath = createTempFile("ruby_podspec", ".podspec").apply { writeText(rubyContent) }
+
+        // Convert the Ruby podspec file to JSON.
+        val podspecProcess = CocoaPodsCommand.run(
+            parentFile, "ipc", "spec", tempRubyPath.pathString
+        )
+
+        return if (podspecProcess.isError) {
+            logger.warn {
+                "Failed to process the '.podspec' file in Ruby format at '$canonicalPath': " +
+                    podspecProcess.errorMessage
+            }
+
+            null
+        } else {
+            podspecProcess.stdout
+        }
+    }
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: fviernau
body: > You cannot access `process.errorMessage` in `onFailure`, as `process` is scoped to `runCatching`.

This was a mistake, updated above code.
created_at: 2025-05-09T08:24:23Z
author_association: MEMBER
diff_hunk: @@ -111,6 +116,48 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            convertRubyPodspecFile(content)
+        } else {
+            content
+        }?.parsePodspec()
+    }
+
+    private fun File.convertRubyPodspecFile(content: String): String? {
+        // The podspec is in Ruby format.
+        // Because it may depend on React Native functions, an extra require may have to be injected.
+        val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+            val reactNativePath =
+                nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+            if (reactNativePath.isFile) {
+                "require '$reactNativePath'\n$content"
+            } else {
+                null
+            }
+        } ?: content
+
+        val tempRubyPath = createTempFile("ruby_podspec", ".podspec").apply { writeText(rubyContent) }
+
+        // Convert the Ruby podspec file to JSON.
+        val podspecProcess = CocoaPodsCommand.run(
+            parentFile, "ipc", "spec", tempRubyPath.pathString
+        )
+
+        return if (podspecProcess.isError) {
+            logger.warn {
+                "Failed to process the '.podspec' file in Ruby format at '$canonicalPath': " +
+                    podspecProcess.errorMessage
+            }
+
+            null
+        } else {
+            podspecProcess.stdout
+        }
+    }
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: sschuberth
body: However, if using that code, `process` should probably be renamed to `spec` or `json`, as it does not contain the process object, but the JSON string.
created_at: 2025-05-09T08:42:12Z
author_association: MEMBER
diff_hunk: @@ -111,6 +116,48 @@ internal class PodDependencyHandler : DependencyHandler<Lockfile.Pod> {
         }
     }
 
+    private fun File.parsePodspecFile(): Podspec? {
+        val content = readText()
+
+        return if ("Pod::Spec.new" in content) {
+            convertRubyPodspecFile(content)
+        } else {
+            content
+        }?.parsePodspec()
+    }
+
+    private fun File.convertRubyPodspecFile(content: String): String? {
+        // The podspec is in Ruby format.
+        // Because it may depend on React Native functions, an extra require may have to be injected.
+        val rubyContent = parentFile.searchUpwardsForSubdirectory("node_modules")?.let { nodeModulesParentDir ->
+            val reactNativePath =
+                nodeModulesParentDir.resolve("node_modules/react-native/scripts/react_native_pods.rb")
+            if (reactNativePath.isFile) {
+                "require '$reactNativePath'\n$content"
+            } else {
+                null
+            }
+        } ?: content
+
+        val tempRubyPath = createTempFile("ruby_podspec", ".podspec").apply { writeText(rubyContent) }
+
+        // Convert the Ruby podspec file to JSON.
+        val podspecProcess = CocoaPodsCommand.run(
+            parentFile, "ipc", "spec", tempRubyPath.pathString
+        )
+
+        return if (podspecProcess.isError) {
+            logger.warn {
+                "Failed to process the '.podspec' file in Ruby format at '$canonicalPath': " +
+                    podspecProcess.errorMessage
+            }
+
+            null
+        } else {
+            podspecProcess.stdout
+        }
+    }
path: plugins/package-managers/cocoapods/src/main/kotlin/PodDependencyHandler.kt
user: login: nnobelis
body: I removed the `process` variable as it was unused.
created_at: 2025-05-09T08:48:47Z
author_association: MEMBER

PR comments:
user: login: codecov[bot]
created_at: 2025-05-08T09:48:27Z
body: ## [Codecov](https://app.codecov.io/gh/oss-review-toolkit/ort/pull/10293?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 56.43%. Comparing base [(`7d2e86a`)](https://app.codecov.io/gh/oss-review-toolkit/ort/commit/7d2e86adeef7ff6973e24275000c18a5446a72f0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit) to head [(`73a12e5`)](https://app.codecov.io/gh/oss-review-toolkit/ort/commit/73a12e5c529d011a81a37534ce6df064568b2b7d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit).
> Report is 18 commits behind head on main.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff            @@
##               main   #10293   +/-   ##
=========================================
  Coverage     56.43%   56.43%           
  Complexity     1604     1604           
=========================================
  Files           331      331           
  Lines         12243    12243           
  Branches       1135     1135           
=========================================
  Hits           6909     6909           
  Misses         4887     4887           
  Partials        447      447           
```

| [Flag](https://app.codecov.io/gh/oss-review-toolkit/ort/pull/10293/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit) | Coverage Δ | |
|---|---|---|
| [funTest-docker](https://app.codecov.io/gh/oss-review-toolkit/ort/pull/10293/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit) | `70.59% <ø> (ø)` | |
| [funTest-non-docker](https://app.codecov.io/gh/oss-review-toolkit/ort/pull/10293/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit) | `33.42% <ø> (ø)` | |
| [test-ubuntu-24.04](https://app.codecov.io/gh/oss-review-toolkit/ort/pull/10293/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit) | `40.45% <ø> (ø)` | |
| [test-windows-2022](https://app.codecov.io/gh/oss-review-toolkit/ort/pull/10293/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit) | `40.43% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/oss-review-toolkit/ort/pull/10293?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=oss-review-toolkit).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: nnobelis
created_at: 2025-05-09T08:01:46Z
body: @fviernau 

> Consider adding this info to the code comment on lines 83 / 84.

Which info are you referring to ?

EDIT: I think this is an old comment that is not relevant anymore right ?
author_association: MEMBER
user: login: fviernau
created_at: 2025-05-09T08:25:21Z
body: > Which info are you referring to ?
> 
> EDIT: I think this is an old comment that is not relevant anymore right ?

This was referring to the discussion whether it has to be NPM or could be any other package manager.
And it was referring to the topmost code comment in the test.
author_association: MEMBER