module XcodeBarber

Constants

VERSION

Public Instance Methods

collect_args(testing) click to toggle source
# File lib/xcode_barber.rb, line 9
def collect_args(testing)
  deps_help = 'The path to the directory containing the .swiftdeps files for this project. To find it, go to your build directory (usually either ~/Library/Developer/Xcode/DerivedData or ./Build) and go to the <app name>-<random letters> subdirectory. From there, run `find . -name \'*.swiftdeps\'` and find the directory containing the swiftdeps files for this target. If this is a big pain, feel free to file an issue.'
  if !testing
    begin
      return Slop.parse do |o|
        o.string '-t', '--target', 'The name of the target', required: true
        o.string '-p', '--project', 'The path to the .xcodeproj', required: true
        o.string '-r', '--root', 'The path to the root that the app starts from. Must be a swift UIApplicationDelegate.', required: true
        o.string '-d', '--deps', deps_help, required: true
        o.separator 'other options:'
        # o.bool '-f', '--force', 'Force the project.pbxproj file to be changed without asking first'
        o.on '--help' do
          puts "Example: barber -t MyAppDebug -p MyApp.xcodeproj -r MyApp/AppDelegate.swift -d ~/.../DerivedData/.../MyAppDebug.build/Objects-normal/x86_64/"
          puts o
          exit
        end
      end
    rescue
      puts "Invalid arguments, try `barber --help` for usage"
      exit
    end
  else
    return {:project => "Beam.xcodeproj", :target => "beam", :root => "Beam/AppDelegate.swift", :deps => "#{ENV['HOME']}/Library/Developer/Xcode/DerivedData/Beam-asdsclslxivvadcbnjflpwajrfmp/Build/Intermediates.noindex/Beam.build/Debug-iphonesimulator/beam.build/Objects-normal/x86_64/"}
  end
end
confirm_overwrite(testing, project_path) click to toggle source
# File lib/xcode_barber.rb, line 45
def confirm_overwrite(testing, project_path)
  return if testing
  puts "This script will make changes to #{project_path}! Are you sure you want to continue? [y/n]"
  continue = yesno()
  exit unless continue
end
rewrite_project(target, project_path, fs_set) click to toggle source
# File lib/xcode_barber.rb, line 176
def rewrite_project(target, project_path, fs_set)
  project = Xcodeproj::Project.open(project_path)
  target = project.targets.detect { |t| t.name == target }
  raise "Couldn't find target #{target} in #{project_path}. Is it capitalized correctly and all that?" unless target

  phase = target.source_build_phase
  phase.files.select do |f|
    path = f.file_ref.path.to_s
    next false unless path.end_with?(".swift")
    next false if fs_set.include?(File.basename(path, ".swift"))
    true
  end.each do |f|
    phase.remove_build_file(f)
  end

  project.save
end
run() click to toggle source
# File lib/xcode_barber.rb, line 194
def run()
  testing = false
  opts = collect_args(testing)
  project_path = opts[:project]
  confirm_overwrite(testing, project_path)
  deps = trimmed_deps(project_path, opts[:deps], opts[:root])
  rewrite_project(opts[:target], project_path, deps)
end
summarize_bottlenecks(fs, file_to_dep, provide_to_file) click to toggle source

def export()

# remove loops
(0...(fs.size)).each { |i| o[i][i] = 0 }

format = "S" * fs.size
out = o.map { |row| row.pack(format) }.join("")
puts fs.size
IO.write("/tmp/deptrim.bin", out)

end

# File lib/xcode_barber.rb, line 64
def summarize_bottlenecks(fs, file_to_dep, provide_to_file)
  f_to_idx = fs.each_with_index.to_h

  dep_hash = fs.map do |file, deps|
    deps = file_to_dep[file]
    files = deps.flat_map do |dep|
      provide_to_file[dep]
    end
    [f_to_idx[file], files.map { |f| f_to_idx[f] }.uniq.sort - [f_to_idx[file]]]
  end

  o = Array.new(fs.size) { Array.new(fs.size) { 0 } }
  dep_hash.each do |i, idxs|
    idxs.each { |idx| o[i][idx] = 1 }
  end

  reverse = Hash.new { |h, k| h[k] = [] }

  dep_hash.each do |d, ds|
    ds.each do |n|
      reverse[n] << d
    end
  end

  n = 10
  summary = `mktemp /tmp/summary_XXXXXX`.chomp
  open(summary, "w") do |file|
    file.puts "Top #{n} with highest ratio dependencies to dependents:"
    top = dep_hash.map do |d, ds|
      [d, ds, ds.size.to_f / reverse[d].size]
    end.sort_by(&:last).reverse.take(n)
    file.puts top.map(&:first).map { |i| fs[i] }
    file.puts ""

    file.puts "Same as above, but with heavier weighting towards those with a low total number of dependents:"
    top = dep_hash.map do |d, ds|
      [d, ds, ds.size.to_f / (reverse[d].size ** 2)]
    end.sort_by(&:last).reverse.take(n)
    file.puts top.map(&:first).map { |i| fs[i] }
    file.puts ""

    file.puts "Still not finding the key bottlenecks in the dependency graph? Feel free to file an issue."
  end

  summary
end
trimmed_deps(project_path, swiftdeps_path, root) click to toggle source
# File lib/xcode_barber.rb, line 111
def trimmed_deps(project_path, swiftdeps_path, root)
  # Dir.chdir(__dir__)
  raise "--project must be a .xcodeproj" unless project_path.end_with?(".xcodeproj")

  raise "pbxproj file not found in #{project_path}" unless File.file?("#{project_path}/project.pbxproj")

  provide_section = false
  depends_section = false

  file_to_dep = Hash.new { |h, k| h[k] = [] }
  provide_to_file = Hash.new { |h, k| h[k] = [] }

  files = `ls #{swiftdeps_path}/*.swiftdeps`.split("\n")
  names = files.map { |file| File.basename(file, ".swiftdeps") }

  files.each do |file|
    name = File.basename(file, ".swiftdeps")
    File.foreach(file) do |line|
      if line.start_with?("provides-top-level:")
        provide_section = true
        depends_section = false
        next
      elsif line.start_with?("depends-top-level:")
        provide_section = false
        depends_section = true
        next
      elsif line.start_with?("depends-member:")
        break
      elsif line.start_with?("depends-") || line.start_with?("provides-")
        provide_section = false
        depends_section = false
        next
      else
        str = line.start_with?("- !pri") ? line[12..-3] : line[3..-3]
        if provide_section
          provide_to_file[str] << name
        elsif depends_section
          file_to_dep[name] << str
        end
      end
    end
  end

  root_name = File.basename(root, ".swift")

  fs = [root_name]
  idx = 0
  files_seen = fs.to_set
  while idx < fs.size
    name = fs[idx]
    deps = file_to_dep[name]
    deps.each do |dep|
      files = provide_to_file[dep]
      fs.concat(files.reject { |f| files_seen.include?(f) })
      files_seen.merge(files)
    end
    idx += 1
  end

  fs_set = fs.to_set
  summary = summarize_bottlenecks(fs, file_to_dep, provide_to_file)
  puts "Trimming complete. Potential dependency bottlenecks summarized at #{summary}"
  return fs_set
end
yesno() click to toggle source
# File lib/xcode_barber.rb, line 35
def yesno
  begin
    system("stty raw -echo")
    str = STDIN.getc
  ensure
    system("stty -raw echo")
  end
  return str.downcase == "y"
end