From 9734aff210cc33a7eb7c3d207a4a05f2424c7810 Mon Sep 17 00:00:00 2001 From: Jon Ruskin Date: Sun, 19 Mar 2023 20:12:35 -0700 Subject: [PATCH] rewrite cocoapods source using cocoapods plugin --- Gemfile.lock | 3 + docs/sources/cocoapods.md | 15 ++- lib/licensed/sources/cocoapods.rb | 67 +++++++------ lib/licensed/sources/source.rb | 5 + licensed.gemspec | 2 +- test/sources/cocoapods_test.rb | 153 ++++++++++++++++++++---------- test/test_helper.rb | 1 + 7 files changed, 154 insertions(+), 92 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index b5ac602..6e1f75a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -40,6 +40,8 @@ GEM thor (>= 0.19, < 2.0) mini_portile2 (2.8.1) minitest (5.18.0) + minitest-hooks (1.5.0) + minitest (> 5.3) mocha (2.0.2) ruby2_keywords (>= 0.0.5) nokogiri (1.14.2) @@ -104,6 +106,7 @@ DEPENDENCIES byebug (~> 11.1) licensed! minitest (~> 5.17) + minitest-hooks (~> 1.5) mocha (~> 2.0) rake (~> 13.0) rubocop-github (~> 0.20) diff --git a/docs/sources/cocoapods.md b/docs/sources/cocoapods.md index 5abcc39..ac795df 100644 --- a/docs/sources/cocoapods.md +++ b/docs/sources/cocoapods.md @@ -1,10 +1,8 @@ # CocoaPods -**NOTE!**: Enumerating Cocoapods dependencies is disabled until the cocoapods-core gem is compatible with Rails 7+. See https://github.com/CocoaPods/Core/pull/733 +The cocoapods source will detect dependencies when `Podfile` and `Podfile.lock` are found at an app's `source_path`. The cocoapods source uses the [cocoapods-dependencies-list](https://github.com/jonabc/cocoapods-dependencies-list) plugin to enumerate dependencies and gather metadata on each package. -The cocoapods source will detect dependencies when `Podfile` and `Podfile.lock` are found at an app's `source_path`. - -It uses the `pod` CLI commands to enumerate dependencies and gather metadata on each package. +**NOTE: Licensed does not install the [cocoapods-dependencies-list](https://github.com/jonanc/cocoapods-dependencies-list) plugin. Users must install the gem alongside the cocoapods gem to enumerate cocoapods dependencies.** ## Evaluating dependencies from a specific target @@ -15,3 +13,12 @@ cocoapods: targets: - ios ``` + +## Specifying which pod executable to run + +The cocoapods source will call the `pod` executable to evaluate dependencies by default. If needed, you can override the executable used with the `cocoapods.command` configuration option. This might be useful if the full path to the `pod` executable is needed (e.g. `pod` is not findable from the system `PATH`), or if you need to execute `pod` with `bundle exec`. + +```yml +cocoapods: + command: 'bundle exec pod' +``` diff --git a/lib/licensed/sources/cocoapods.rb b/lib/licensed/sources/cocoapods.rb index bbce4fd..28c3199 100644 --- a/lib/licensed/sources/cocoapods.rb +++ b/lib/licensed/sources/cocoapods.rb @@ -3,32 +3,29 @@ require "json" require "pathname" require "uri" -# **NOTE** Cocoapods is disabled until cocoapods-core supports recent rails versions -# https://github.com/CocoaPods/Core/pull/733 -# require "cocoapods-core" - module Licensed module Sources class Cocoapods < Source + DEFAULT_POD_COMMAND = "pod".freeze + MISSING_PLUGIN_MESSAGE = "Error running `pods dependencies`. Please ensure the cocoapods-dependencies-list gem is installed, it is required for licensed to enumerate dependencies.".freeze + def enabled? - false + return unless Licensed::Shell.tool_available?("pod") - # return unless Licensed::Shell.tool_available?("pod") - - # config.pwd.join("Podfile").exist? && config.pwd.join("Podfile.lock").exist? + config.pwd.join("Podfile").exist? && config.pwd.join("Podfile.lock").exist? end def enumerate_dependencies pods.map do |pod| - name = pod.name - path = dependency_path(pod.root_name) - version = lockfile.version(name).version - Dependency.new( - path: path, - name: name, - version: version, - metadata: { "type" => Cocoapods.type } + name: pod["name"], + version: pod["version"], + path: pod["path"], + metadata: { + "type" => Cocoapods.type, + "summary" => pod["summary"], + "homepage" => pod["homepage"] + } ) end end @@ -36,32 +33,32 @@ module Licensed private def pods - return lockfile.dependencies if targets.nil? + cocoapods_dependencies_json.values.flatten + end - targets_to_validate = podfile.target_definition_list.filter { |t| targets.include?(t.label) } - if targets_to_validate.any? - targets_to_validate.map(&:dependencies).flatten - else - raise Licensed::Sources::Source::Error, "Unable to find any target in the Podfile matching the ones provided in the config." + def cocoapods_dependencies_json + args = ["dependencies", "--include-path=true"] + args << "--targets=#{targets.join(",")}" if targets.any? + + output = Licensed::Shell.execute(*pod_command, *args, allow_failure: true) + if output.include? "Unknown command" + raise Licensed::Sources::Source::Error, MISSING_PLUGIN_MESSAGE end + + JSON.parse(output) + rescue JSON::ParserError => e + message = "Licensed was unable to parse the output from 'pod dependencies'. JSON Error: #{e.message}" + raise Licensed::Sources::Source::Error, message end def targets - @targets ||= config.dig("cocoapods", "targets")&.map { |t| "Pods-#{t}" } + return [] unless [String, Array].any? { |type| source_config["targets"].is_a?(type) } + Array(source_config["targets"]).map { |t| "Pods-#{t}" } end - def lockfile - @lockfile = nil - # @lockfile ||= Pod::Lockfile.from_file(config.pwd.join("Podfile.lock")) - end - - def podfile - @podfile = nil - # @podfile ||= Pod::Podfile.from_file(config.pwd.join("Podfile")) - end - - def dependency_path(name) - config.pwd.join("Pods/#{name}") + def pod_command + return DEFAULT_POD_COMMAND unless source_config["command"].is_a?(String) + source_config["command"].split end end end diff --git a/lib/licensed/sources/source.rb b/lib/licensed/sources/source.rb index 2ac52ae..fafde4d 100644 --- a/lib/licensed/sources/source.rb +++ b/lib/licensed/sources/source.rb @@ -90,6 +90,11 @@ module Licensed config.ignored?(dependency.metadata, require_version: self.class.require_matched_dependency_version) end + # Returns configuration options set for the current source + def source_config + @source_config ||= config[self.class.type].is_a?(Hash) ? config[self.class.type] : {} + end + private # Returns a cached list of dependencies diff --git a/licensed.gemspec b/licensed.gemspec index 376af9e..021e3ca 100644 --- a/licensed.gemspec +++ b/licensed.gemspec @@ -31,10 +31,10 @@ Gem::Specification.new do |spec| spec.add_dependency "parallel", "~> 1.22" spec.add_dependency "reverse_markdown", "~> 2.1" spec.add_dependency "json", "~> 2.6" - # spec.add_dependency "cocoapods-core", "~> 1.11" spec.add_development_dependency "rake", "~> 13.0" spec.add_development_dependency "minitest", "~> 5.17" + spec.add_development_dependency "minitest-hooks", "~> 1.5" spec.add_development_dependency "mocha", "~> 2.0" spec.add_development_dependency "rubocop-github", "~> 0.20" spec.add_development_dependency "byebug", "~> 11.1" diff --git a/test/sources/cocoapods_test.rb b/test/sources/cocoapods_test.rb index 5005a12..3e8b002 100644 --- a/test/sources/cocoapods_test.rb +++ b/test/sources/cocoapods_test.rb @@ -2,62 +2,111 @@ require "test_helper" require "tmpdir" -# Cocoapods is disabled until cocoapods-core supports recent rails versions -# https://github.com/CocoaPods/Core/pull/733 +if Licensed::Shell.tool_available?("pod") + describe Licensed::Sources::Cocoapods do + let(:fixtures) { File.expand_path("../../fixtures/cocoapods", __FILE__) } + let(:config) { Licensed::AppConfiguration.new({ "source_path" => fixtures, "cocoapods" => { "command" => "bundle exec pod" } }) } + let(:source) { Licensed::Sources::Cocoapods.new(config) } -# if Licensed::Shell.tool_available?("pod") -# describe Licensed::Sources::Cocoapods do -# let(:fixtures) { File.expand_path("../../fixtures/cocoapods", __FILE__) } -# let(:config) { Licensed::AppConfiguration.new({ "source_path" => Dir.pwd }) } -# let(:source) { Licensed::Sources::Cocoapods.new(config) } + def with_local_bundler_environment + backup_env = nil -# describe "enabled?" do -# it "is true if Podfiles exist" do -# Dir.chdir(fixtures) do -# assert source.enabled? -# end -# end + ::Bundler.ui.silence do + if ::Bundler.root != config.source_path + backup_env = ENV.to_hash + ENV.replace(::Bundler.original_env) -# it "is false if Podfiles do not exist" do -# Dir.mktmpdir do |dir| -# Dir.chdir(dir) do -# refute source.enabled? -# end -# end -# end -# end + # reset bundler to load from the current app's source path + ::Bundler.reset! + end -# describe "dependencies" do -# it "finds Cocoapods dependencies" do -# Dir.chdir(fixtures) do -# dep = source.dependencies.find { |d| d.name == "Alamofire" } -# assert dep -# assert_equal "5.4.3", dep.version -# end -# end + # ensure the bundler environment is loaded before enumeration + ::Bundler.load -# it "handle multiple subspecs from the same root dependencies" do -# Dir.chdir fixtures do -# assert source.dependencies.detect { |dep| dep.name == "MaterialComponents/Cards" } -# assert source.dependencies.detect { |dep| dep.name == "MaterialComponents/Buttons" } -# end -# end + yield + end + ensure + if backup_env + # restore bundler configuration + ENV.replace(backup_env) + ::Bundler.reset! + end -# it "supports pods from git" do -# Dir.chdir(fixtures) do -# dep = source.dependencies.detect { |d| d.name == "Chatto" } -# end -# end -# end + # reload the bundler environment after enumeration + ::Bundler.load + end -# describe "targets" do -# it "includes only dependencies from target if configured" do -# Dir.chdir fixtures do -# config["cocoapods"] = { "targets" => ["iosTests"] } -# assert source.dependencies.detect { |dep| dep.name == "lottie-ios" } -# assert_nil source.dependencies.detect { |dep| dep.name == "Alamofire" } -# end -# end -# end -# end -# end + around do |&block| + with_local_bundler_environment { block.call } + end + + describe "enabled?" do + it "is true if Podfiles exist" do + Dir.chdir(fixtures) do + assert source.enabled? + end + end + + it "is false if Podfiles do not exist" do + Dir.mktmpdir do |dir| + Dir.chdir(dir) do + refute source.enabled? + end + end + end + end + + describe "dependencies" do + it "finds Cocoapods dependencies" do + Dir.chdir(fixtures) do + dep = source.dependencies.find { |d| d.name == "Alamofire" } + assert dep + assert_equal "5.4.3", dep.version + refute_nil dep.record["summary"] + refute_nil dep.record["homepage"] + refute_nil dep.record["license"] + end + end + + it "handle multiple subspecs from the same root dependencies" do + Dir.chdir fixtures do + assert source.dependencies.detect { |dep| dep.name == "MaterialComponents/Cards" } + assert source.dependencies.detect { |dep| dep.name == "MaterialComponents/Buttons" } + end + end + + it "supports pods from git" do + Dir.chdir(fixtures) do + dep = source.dependencies.detect { |d| d.name == "Chatto" } + end + end + + it "raises an error if cocoapods-dependencies-list isn't available" do + Dir.mktmpdir do |dir| + FileUtils.cp_r(fixtures, dir) + Dir.chdir(File.join(dir, "cocoapods")) do + with_local_bundler_environment do + Licensed::Shell.execute(*%w{bundle config without plugins}) + Licensed::Shell.execute(*%w{bundle install}) + error = assert_raises Licensed::Sources::Source::Error do + source.dependencies.find { |d| d.name == "Alamofire" } + end + + assert_equal Licensed::Sources::Cocoapods::MISSING_PLUGIN_MESSAGE, error.message + end + end + end + end + end + + describe "targets" do + it "includes only dependencies from target if configured" do + Dir.chdir fixtures do + config["cocoapods"]["targets"] = ["iosTests"] + assert source.dependencies.detect { |dep| dep.name == "lottie-ios" } + assert_nil source.dependencies.detect { |dep| dep.name == "Alamofire" } + end + end + end + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb index ac48f89..6f791c6 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "bundler/setup" require "minitest/autorun" +require "minitest/hooks/default" require "mocha/minitest" require "byebug" require "licensed"