module Pod class Command class Dependsay < Command include Command::ProjectDirectory self.summary = "Visualize the project's dependencies." self.description = <<-DESC Visualize the project's dependencies. DESC def self.options [ ['--ignore-lockfile', 'Whether the lockfile should be ignored when calculating the dependency graph'], ['--repo-update', 'Fetch external podspecs and run `pod repo update` before calculating the dependency graph'] ].concat(super) end def self.arguments [ CLAide::Argument.new('PODSPEC', false) ].concat(super) end attr_reader :output def initialize(argv) @output = output_file @podspec_name = argv.shift_argument @ignore_lockfile = argv.flag?('ignore-lockfile', false) @repo_update = argv.flag?('repo-update', false) super end def output_file(path = nil) File.open(path.nil? ? 'output.html' : path, 'w') end def validate! super if @podspec_name require 'pathname' path = Pathname.new(@podspec_name) if path.file? @podspec = Specification.from_file(path) else sets = Config .instance .sources_manager .search(Dependency.new(@podspec_name)) spec = sets && sets.specification @podspec = spec && spec.subspec_by_name(@podspec_name) raise Informative, "Cannot find `#{@podspec_name}`." unless @podspec end end end def run UI.title 'Calculating dependencies' do dependencies end output.write(erb(:index)) end def toolbox? false end def layout 'null' end def file(path) File.read(resolve_file_path("/public/#{path}")) end def erb(template) path = resolve_file_path("/views/#{template}.erb") file = File.open(path).read ERB.new(file).result binding end def resolve_file_path(path) File.expand_path("../../../..#{path}", __FILE__) end def dependencies @dependencies ||= begin analyzer = Installer::Analyzer.new( sandbox, podfile, @ignore_lockfile || @podspec ? nil : config.lockfile ) specs = config.with_changes(skip_repo_update: !@repo_update) do analyzer.analyze(@repo_update || @podspec).specs_by_target.values.flatten(1) end lockfile = Lockfile.generate(podfile, specs, {}) lockfile.to_hash['PODS'] end end def podfile @podfile ||= begin if podspec = @podspec platform = podspec.available_platforms.first platform_name = platform.name platform_version = platform.deployment_target.to_s source_urls = Config.instance.sources_manager.all.map(&:url).compact Podfile.new do install! 'cocoapods', integrate_targets: false, warn_for_multiple_pod_sources: false source_urls.each { |u| source(u) } platform platform_name, platform_version pod podspec.name, podspec: podspec.defined_in_file target 'Dependencies' end else verify_podfile_exists! config.podfile end end end def sandbox if @podspec require 'tmpdir' Sandbox.new(Dir.mktmpdir) else config.sandbox end end def data @data ||= begin definitions = [] relations = [] pod_to_dependencies.each do |key, value| definitions << { 'type' => 'Class', 'namespace' => key, 'circular' => false, 'line' => 0, 'lines' => 0 } end pod_to_dependencies.each do |key, value| value.each do |v| relations << { 'type' => 'Base', 'resolved_namespace' => pick(v, definitions), 'caller' => key, 'circular' => false, 'lines' => 0 } end end definitions.each do |value| ns = value['namespace'] relations.each do |v| value['lines'] += 1 if [v['caller'], v['resolved_namespace']].include? ns end end definitions.each do |value| value['namespace'] = name_transform(value['namespace']) end relations.each do |value| value['resolved_namespace'] = name_transform(value['resolved_namespace']) value['caller'] = name_transform(value['caller']) end data = { 'definitions' => definitions, 'relations' => relations } data.to_s.gsub(/=>/, ':') end end def name_transform(name) name.instance_of?(String) ? name.gsub(/ \(/,"@").gsub(/\./, "-").gsub(/\)/, "") : name end def pick(key, pool) pool.each do |v| return v['namespace'] if sanitized_pod_name(key) == sanitized_pod_name(v['namespace']) end end # Truncates the input string after a pod's name removing version requirements, etc. def sanitized_pod_name(name) Pod::Dependency.from_string(name).name end # Returns a Set of Strings of the names of dependencies specified in the Podfile. def podfile_dependencies Set.new(podfile.target_definitions.values.map { |t| t.dependencies.map { |d| d.name } }.flatten) end # Returns a [String: [String]] containing resolved mappings from the name of a pod to an array of the names of its dependencies. def pod_to_dependencies dependencies.map { |d| d.is_a?(Hash) ? d : { d => [] } }.reduce({}) { |combined, individual| combined.merge!(individual) } end # Basename to use for output files. def output_file_basename return 'Podfile' unless @podspec_name File.basename(@podspec_name, File.extname(@podspec_name)) end end end end