Skip to content

Instantly share code, notes, and snippets.

@dominik-hadl
Created August 11, 2016 17:37
Show Gist options
  • Save dominik-hadl/9fe841c0efe19ea7f746fe406561d0e5 to your computer and use it in GitHub Desktop.
Save dominik-hadl/9fe841c0efe19ea7f746fe406561d0e5 to your computer and use it in GitHub Desktop.
Caches build carthage files, for faster bootstrapping.
#!/usr/bin/env ruby
require "fileutils"
require 'digest'
require 'optparse'
require 'xcodeproject'
# Constants
COMPILER_VER = /(?<=\()(.*)(?= )/.match(`xcrun swift -version`)[0]
CARTHAGE_RESOLVED_FILE="Cartfile.resolved"
CARTHAGE_CACHE_DIR="#{ENV['HOME']}/.carthage_cache"
module Platform
IOS = "iOS"
TVOS = "tvOS"
OSX = "OSX"
WATCHOS = "watchOS"
ALL = [IOS, TVOS, OSX, WATCHOS]
end
# Parse args
OPTIONS = {}
opts_parser = OptionParser.new do |opts|
opts.banner = "Usage: carthage_cache.rb [OPTIONS] project_root"
opts.separator ""
opts.separator "Uses cache to load appropriate dependencies. If cached version of the current version is not available,"
opts.separator "bootstraps it and saves back to cache for further use. Also takes into account compiler version."
opts.separator ""
opts.separator "Specific options:"
opts.on("-c", "--[no-]clean", "This option will clean (delete) all cached files for all projects.") do |v|
OPTIONS[:clean] = v
end
opts.on("-w", "--[no-]whole", "Uses MD5 of Cartfile.resolved to check cache and restores the whole image of the folder if available. (worse)") do |v|
OPTIONS[:whole] = v
end
opts.on("-f", "--[no-]force", "Force bootstrap dependencies without using cached versions and save the built products to cache.") do |v|
OPTIONS[:force] = v
end
opts.on_tail("-h", "--help", "Show this message") do
puts opts
exit
end
end
opts_parser.parse!
def get_cache_paths()
base_path = "#{CARTHAGE_CACHE_DIR}/#{COMPILER_VER}"
paths = {}
if OPTIONS[:whole]
paths["WHOLE"] = "#{base_path}/whole"
else
Platform::ALL.each do |platform|
paths[platform] = "#{base_path}/intelligent/#{platform}"
end
end
return paths
end
# Gets the names and hashes from resolved cartfile
def parse_resolved_cartfile(path)
dependencies = {}
# Open file and read line by line
File.open(path) do |file|
file.each_line do |line|
# Run regex on line
result = /^git(?>hub)? ".*(?<=\/)([^"]*)" "(.*)"$/.match(line)
# If we found match (0) and name capture group (1) and hash/version capture group (2)
if !result.nil? && result.length == 3
dependencies[result[2]] = result[1].chomp('.git')
end
end
end
return dependencies
end
def find_xcodeproj_files()
projects = XcodeProject::Project.find("Carthage/Checkouts/**")
# Filter out example, test projects
projects.each do |p|
projects.delete p unless /[Dd]emo$|[Ee]xample$|[Tt]est[s]*$/.match(p.name).nil?
end
return projects
end
def parse_product_name_mappings(xcodeproj_files, dependencies)
names = {}
xcodeproj_files.each do |xp|
data = xp.read
next unless data.targets.first.config("Release").build_settings["DEFINES_MODULE"]
product_name = data.targets.first.config("Release").build_settings["PRODUCT_NAME"]
# If the proj is using target name
if product_name == "$(TARGET_NAME)"
product_name = data.targets.first.name
end
key = nil
file_path = xp.file_path.to_s
dependencies.values.each do |name|
next unless file_path.include? name
key = name
break
end
next unless key.nil? == false
names[key] = product_name unless names.values.include?(product_name)
end
return names
end
def cache_built_dependencies(dependencies)
puts("Caching built dependencies.")
cache_paths = get_cache_paths
if OPTIONS[:whole] == true
cache_path = "#{cache_paths.values.first}/#{Digest::MD5.file CARTHAGE_RESOLVED_FILE}/"
# Get target dir
src_dir = Dir.getwd + "/Carthage/Build/"
# Create the target dir if needed and copy files
FileUtils.mkdir_p cache_path unless File.exists? cache_path
FileUtils.cp_r Dir["#{src_dir}/*"], cache_path
else
xcodeproj_files = find_xcodeproj_files
products_name_mapping = parse_product_name_mappings xcodeproj_files, dependencies
dependencies.each do |d_ver, d_name|
next unless products_name_mapping.keys.include?(d_name)
product_name = products_name_mapping[d_name]
get_cache_paths.each do |platform, path|
target_dir = "#{path}/#{d_name}/#{d_ver}"
src_dir = "Carthage/Build/#{platform}"
fmwk_path = "#{src_dir}/#{product_name}.framework"
next unless fmwk_path.empty? == false
next unless Dir.exist? src_dir
# Create the target dir if needed and copy files
FileUtils.mkdir_p target_dir unless File.exists? target_dir
FileUtils.cp_r fmwk_path, target_dir
FileUtils.cp_r "#{fmwk_path}.dSYM", target_dir
end
end
# Steps
# 1. Find .xcodeproj in Carthage/Checkouts after running carthage checkout
# 2. Parse Product Name to be able to find corresponding .framework
# 3. Copy that from each platform folder to cache
# Question: How to figure out .bcsymbolmap files? Relation to .framework? Metadata? Timestamp?
end
end
def copy_dependencies_from_cache(dependencies)
uncached = {}
cache_paths = get_cache_paths
# Whole cache
if OPTIONS[:whole]
cache_path = "#{cache_paths.values.first}/#{Digest::MD5.file CARTHAGE_RESOLVED_FILE}/"
if Dir.exist? cache_path
found = true
# Get target dir
target_dir = Dir.getwd + "/Carthage/Build"
# Create the target dir if needed and copy files
FileUtils.mkdir_p target_dir unless File.exists? target_dir
FileUtils.cp_r Dir["#{cache_path}/*"], target_dir
else
uncached.replace dependencies
end
else
# Intelligent cache
dependencies.each do |d_ver, d_name|
found = false
cache_paths.each do |platform, path|
dep_cache_path = "#{path}/#{d_name}/#{d_ver}"
if Dir.exist? dep_cache_path
found = true
target_dir = Dir.getwd + "/Carthage/Build/#{platform}"
# Create the target dir if needed and copy files
FileUtils.mkdir_p target_dir unless File.exists? target_dir
FileUtils.cp_r Dir["#{dep_cache_path}/*"], target_dir
end
end
# If not found, put it in uncached
uncached[d_ver] = d_name unless found
end
end
puts "Copied dependencies from cache: #{dependencies.values - uncached.values}"
return uncached
end
def boostrap_and_cache(dependencies = {})
if dependencies.count == 0
success = system("carthage bootstrap")
else
# Pass only some deps to carthage bootstrap, so we don't build everything
success = system("carthage bootstrap #{dependencies.values.join(" ")}")
end
# Check result and cache if appropriate
if success
cache_built_dependencies dependencies
exit 0
else
puts("ERROR: Carthage bootstrap failed, can't cache built dependencies.")
exit 1
end
end
# -----------
# MAIN
# -----------
# Safety check
if ARGV.count != 1
puts "Wrong number of arguments (#{ARGV.count})."
puts opts_parser.help
exit 1
end
# Clean cache
if OPTIONS[:clean]
puts "Cleaning all cached files."
FileUtils.rm_rf(CARTHAGE_CACHE_DIR)
exit 0
end
# Change to project dir
Dir.chdir ARGV[0]
# Get Cartfile.resolved
if !File.exist?(CARTHAGE_RESOLVED_FILE)
puts("No #{CARTHAGE_RESOLVED_FILE} found in specified directory. Bootstrapping instead.")
boostrap_and_cache
end
# Get dependencies
deps = parse_resolved_cartfile CARTHAGE_RESOLVED_FILE
# Force bootstrap or try loading dependencies from cache
if OPTIONS[:force]
puts "Force bootstrapping and caching results."
boostrap_and_cache deps
else
remaining_deps = copy_dependencies_from_cache deps
if remaining_deps.count == 0
puts "Loaded all dependencies from cache."
exit 0
end
end
# Bootstrapping remaining dependencies
puts "Following dependencies not cached, bootstrapping them now: #{remaining_deps.values}"
boostrap_and_cache remaining_deps
@dominik-hadl
Copy link
Author

gem install xcodeproject required

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment