Skip to content

Commit

Permalink
Merge pull request #61 from fnxpt/dependencies
Browse files Browse the repository at this point in the history
Add support for dependencies.
  • Loading branch information
macblazer authored Jan 1, 2024
2 parents b01ea7a + 493c825 commit 8372b54
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 28 deletions.
23 changes: 20 additions & 3 deletions lib/cyclonedx/cocoapods/bom_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def add_to_bom(xml)
end
end
xml.purl purl
xml.bomRef purl
unless homepage.nil?
xml.externalReferences do
xml.reference(type: HOMEPAGE_REFERENCE_TYPE) do
Expand Down Expand Up @@ -119,10 +120,12 @@ def add_to_bom(xml)
class BOMBuilder
NAMESPACE = 'http://cyclonedx.org/schema/bom/1.4'

attr_reader :component, :pods
attr_reader :component, :pods, :dependencies

def initialize(component: nil, pods:)
@component, @pods = component, pods
def initialize(pods:, component: nil, dependencies: nil)
@pods = pods
@component = component
@dependencies = dependencies
end

def bom(version: 1)
Expand All @@ -136,12 +139,26 @@ def bom(version: 1)
pod.add_to_bom(xml)
end
end

xml.dependencies do
bom_dependencies(xml, dependencies)
end
end
end.to_xml
end

private

def bom_dependencies(xml, dependencies)
dependencies&.each do |key, array|
xml.dependency(ref: key) do
array.each do |value|
xml.dependency(ref: value)
end
end
end
end

def bom_metadata(xml)
xml.metadata do
xml.timestamp Time.now.getutc.strftime('%Y-%m-%dT%H:%M:%SZ')
Expand Down
5 changes: 3 additions & 2 deletions lib/cyclonedx/cocoapods/cli_runner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ def run

analyzer = PodfileAnalyzer.new(logger: @logger, exclude_test_targets: options[:exclude_test_targets])
podfile, lockfile = analyzer.ensure_podfile_and_lock_are_present(options)
pods = analyzer.parse_pods(podfile, lockfile)
pods, dependencies = analyzer.parse_pods(podfile, lockfile)
analyzer.populate_pods_with_additional_info(pods)

bom = BOMBuilder.new(component: component_from_options(options), pods: pods).bom(version: options[:bom_version] || 1)
builder = BOMBuilder.new(pods: pods, component: component_from_options(options), dependencies: dependencies)
bom = builder.bom(version: options[:bom_version] || 1)
write_bom_to_file(bom: bom, options: options)
rescue StandardError => e
@logger.error ([e.message] + e.backtrace).join($/)
Expand Down
42 changes: 33 additions & 9 deletions lib/cyclonedx/cocoapods/podfile_analyzer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def load_plugins(podfile_path)
podfile_contents = File.read(podfile_path)
plugin_syntax = /\s*plugin\s+['"]([^'"]+)['"]/
plugin_names = podfile_contents.scan(plugin_syntax).flatten

plugin_names.each do |plugin_name|
@logger.debug("Loading plugin #{plugin_name}")
begin
Expand All @@ -67,17 +67,32 @@ def ensure_podfile_and_lock_are_present(options)
lockfile = ::Pod::Lockfile.from_file(options[:podfile_lock_path])
verify_synced_sandbox(lockfile)
load_plugins(options[:podfile_path])

return ::Pod::Podfile.from_file(options[:podfile_path]), lockfile
end


def parse_pods(podfile, lockfile)
@logger.debug "Parsing pods from #{podfile.defined_in_file}"
included_pods = create_list_of_included_pods(podfile, lockfile)
return lockfile.pod_names.select { |name| included_pods.include?(name) }.map do |name|
included_pods, dependencies = create_list_of_included_pods(podfile, lockfile)

pods = lockfile.pod_names.select { |name| included_pods.include?(name) }.map do |name|
Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
end

pod_dependencies = { }
dependencies.each {|key, value|
if lockfile.pod_names.include? key
pod = Pod.new(name: key, version: lockfile.version(key), source: source_for_pod(podfile, lockfile, key), checksum: lockfile.checksum(key))

pod_dependencies[pod.purl] = lockfile.pod_names.select { |name| value.include?(name) }.map do |name|
pod = Pod.new(name: name, version: lockfile.version(name), source: source_for_pod(podfile, lockfile, name), checksum: lockfile.checksum(name))
pod.purl
end
end
}

return pods, pod_dependencies
end


Expand All @@ -89,7 +104,6 @@ def populate_pods_with_additional_info(pods)
return pods
end


private


Expand Down Expand Up @@ -124,23 +138,33 @@ def simple_hash_of_lockfile_pods(lockfile)
pods_hash
end


def append_all_pod_dependencies(pods_used, pods_cache)
result = pods_used
original_number = 0
dependencies_hash = { }

# Loop adding pod dependencies until we are not adding any more dependencies to the result
# This brings in all the transitive dependencies of every top level pod.
# Note this also handles two edge cases:
# 1. Having a Podfile with no pods used.
# 2. Having a pod that has a platform-specific dependency that is unused for this Podfile.
while result.length != original_number
original_number = result.length

pods_used.each { |pod_name|
result.push(*pods_cache[pod_name]) unless !pods_cache.key?(pod_name) || pods_cache[pod_name].empty?
if pods_cache.key?(pod_name)
result.push(*pods_cache[pod_name])
dependencies_hash[pod_name] = pods_cache[pod_name].empty? ? [] : pods_cache[pod_name]
end
}

result = result.uniq
# maybe additional dependency processing needed here???
pods_used = result
end
result

return result, dependencies_hash
end

def create_list_of_included_pods(podfile, lockfile)
Expand All @@ -152,9 +176,9 @@ def create_list_of_included_pods(podfile, lockfile)

topLevelDeps = includedTargets.map(&:dependencies).flatten.uniq
pods_used = topLevelDeps.map(&:name).uniq
pods_used = append_all_pod_dependencies(pods_used, pods_cache)
pods_used, dependencies = append_all_pod_dependencies(pods_used, pods_cache)

return pods_used.sort
return pods_used.sort, dependencies
end


Expand Down
53 changes: 46 additions & 7 deletions spec/cyclonedx/cocoapods/bom_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,27 @@
end
end


RSpec.describe CycloneDX::CocoaPods::BOMBuilder do
context 'when generating a BOM' do
let(:pods) { {
'Alamofire' => '5.4.2', 'FirebaseAnalytics' => '7.10.0', 'RxSwift' => '5.1.2', 'Realm' => '5.5.1'
}.map { |name, version| CycloneDX::CocoaPods::Pod.new(name: name, version: version) } }
let(:pods) do
{
'Alamofire' => '5.6.2',
'FirebaseAnalytics' => '7.10.0',
'RxSwift' => '5.1.2',
'Realm' => '5.5.1',
'MSAL' => '1.2.1',
'MSAL/app-lib' => '1.2.1'
}.map { |name, version| CycloneDX::CocoaPods::Pod.new(name: name, version: version) }
end
let(:dependencies) do
{
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => ['pkg:cocoapods/[email protected]#app-lib'],
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => []
}
end

shared_examples "bom_generator" do
context 'with an incorrect version' do
Expand Down Expand Up @@ -328,7 +343,7 @@
it 'should generate component metadata when a component is available' do
if bom_builder.component
component_metadata = Nokogiri::XML(Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.metadata('xmlns': described_class::NAMESPACE) do
xml.metadata(xmlns: described_class::NAMESPACE) do
component.add_to_bom(xml)
end
end.to_xml).at('metadata/component')
Expand All @@ -348,25 +363,49 @@
components_generated_from_bom_builder = xml.at('bom/components')

components = Nokogiri::XML(Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
xml.components('xmlns': described_class::NAMESPACE) do
xml.components(xmlns: described_class::NAMESPACE) do
bom_builder.pods.each { |pod| pod.add_to_bom(xml) }
end
end.to_xml).at('components')

expect(components_generated_from_bom_builder).to be_equivalent_to(components)
end

it 'should generate a child dependencies node' do
expect(xml.at('bom/dependencies')).not_to be_nil
end

it 'shoudl properly set dependencies node' do
dependencies_generated_from_bom_builder = xml.at('bom/dependencies')

dependencies = Nokogiri::XML dependencies_result

expect(dependencies_generated_from_bom_builder.to_xml).to be_equivalent_to(dependencies.root.to_xml)
end
end
end

context 'without a component' do
let(:bom_builder) { described_class.new(pods: pods) }
let(:dependencies_result) { '<dependencies/>' }

it_behaves_like "bom_generator"
end

context 'with a component' do
let(:component) { CycloneDX::CocoaPods::Component.new(name: 'Application', version: '1.3.5', type: 'application') }
let(:bom_builder) { described_class.new(component: component, pods: pods) }
let(:bom_builder) { described_class.new(component: component, pods: pods, dependencies: dependencies) }
let(:dependencies_result) do
'<dependencies>
<dependency ref="pkg:cocoapods/[email protected]"/>
<dependency ref="pkg:cocoapods/[email protected]">
<dependency ref="pkg:cocoapods/[email protected]#app-lib"/>
</dependency>
<dependency ref="pkg:cocoapods/[email protected]"/>
<dependency ref="pkg:cocoapods/[email protected]"/>
<dependency ref="pkg:cocoapods/[email protected]"/>
</dependencies>'
end

it_behaves_like "bom_generator"
end
Expand Down
37 changes: 30 additions & 7 deletions spec/cyclonedx/cocoapods/podfile_analyzer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@

require 'cyclonedx/cocoapods/podfile_analyzer'
require 'rspec'

RSpec.describe CycloneDX::CocoaPods::PodfileAnalyzer do
let(:fixtures) { Pathname.new(File.expand_path('../../../fixtures/', __FILE__)) }
let(:empty_podfile) { 'EmptyPodfile/Podfile' }
Expand All @@ -46,10 +45,12 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (empty_podfile + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq([])
expect(dependencies).to eq({})
expect(pod_names.length).to eq(dependencies.length)
end

it 'should find all simple pods' do
Expand All @@ -60,10 +61,16 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (simple_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['Alamofire', 'MSAL', 'MSAL/app-lib'])
expect(dependencies).to eq({
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => ['pkg:cocoapods/[email protected]#app-lib'],
'pkg:cocoapods/[email protected]#app-lib' => []
})
expect(pod_names.length).to eq(dependencies.length)
end

it 'should find all pods actually used' do
Expand All @@ -74,10 +81,12 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (restricted_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['EFQRCode'])
expect(dependencies).to eq({ 'pkg:cocoapods/[email protected]' => [] })
expect(pod_names.length).to eq(dependencies.length)
end

it 'should find all pods' do
Expand All @@ -88,10 +97,16 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (tests_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['Alamofire', 'MSAL', 'MSAL/app-lib'])
expect(dependencies).to eq({
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => ['pkg:cocoapods/[email protected]#app-lib'],
'pkg:cocoapods/[email protected]#app-lib' => []
})
expect(pod_names.length).to eq(dependencies.length)
end
end

Expand All @@ -104,10 +119,16 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (simple_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['Alamofire', 'MSAL', 'MSAL/app-lib'])
expect(dependencies).to eq({
'pkg:cocoapods/[email protected]' => [],
'pkg:cocoapods/[email protected]' => ['pkg:cocoapods/[email protected]#app-lib'],
'pkg:cocoapods/[email protected]#app-lib' => []
})
expect(pod_names.length).to eq(dependencies.length)
end

it 'should not include testing pods' do
Expand All @@ -118,10 +139,12 @@
lock_file = ::Pod::Lockfile.from_file(fixtures + (tests_pod + '.lock'))
expect(lock_file).not_to be_nil

included_pods = analyzer.parse_pods(pod_file, lock_file)
included_pods, dependencies = analyzer.parse_pods(pod_file, lock_file)

pod_names = included_pods.map(&:name)
expect(pod_names).to eq(['Alamofire'])
expect(dependencies).to eq({ 'pkg:cocoapods/[email protected]' => [] })
expect(pod_names.length).to eq(dependencies.length)
end
end
end
Expand Down

0 comments on commit 8372b54

Please sign in to comment.