Created
August 11, 2016 17:37
-
-
Save dominik-hadl/9fe841c0efe19ea7f746fe406561d0e5 to your computer and use it in GitHub Desktop.
Caches build carthage files, for faster bootstrapping.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
gem install xcodeproject
required