Skip to content

Add ability to store prebuilt frameworks in shared cache folder #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ If your `Pods` folder is excluded from git, you may add `keep_source_code_for_pr

If bitcode is needed, add a `enable_bitcode_for_prebuilt_frameworks!` before all targets in Podfile

if you want to share prebuilt frameworks for different projects or for CI builds, you may add `use_shared_cache!` in the head of Podfile to speed up pod install, as it will reuse frameworks from common cache folder(`~Library/Caches/CocoaPods/Prebuilt` by default).


#### Known Issues

Expand All @@ -74,5 +76,3 @@ If bitcode is needed, add a `enable_bitcode_for_prebuilt_frameworks!` before all
MIT

Appreciate a 🌟 if you like it.


1 change: 1 addition & 0 deletions cocoapods-binary.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Gem::Specification.new do |spec|
spec.add_dependency "cocoapods", ">= 1.5.0", "< 2.0"
spec.add_dependency "fourflusher", "~> 2.0"
spec.add_dependency "xcpretty", "~> 0.3.0"
spec.add_dependency "aws-sdk-s3", "~> 1"

spec.add_development_dependency 'bundler', '~> 1.3'
spec.add_development_dependency 'rake'
Expand Down
34 changes: 34 additions & 0 deletions lib/cocoapods-binary/Main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,33 @@ def keep_source_code_for_prebuilt_frameworks!
DSL.dont_remove_source_code = true
end

# Enable shared cache of prebuild frameworks
# Frameworks are stored inside cocoapods cache folder
#
# Location: ~/Library/Caches/CocoaPods/Prebuilt/
# Structure: <xcode-version>/<framework-name>/<framework-version>/<options hash>/
# Options hash depends on:
# - bitcode(enable_bitcode_for_prebuilt_frameworks!);
# - custom options(set_custom_xcodebuild_options_for_prebuilt_frameworks);
# - platform name(ios, osx);
def use_shared_cache!
DSL.shared_cache_enabled = true
end

# Enable s3 shared cache, requires use_shared_cache!
# Frameworks also stored in s3 bucket
# Options hash depends on:
# - login(optional) : if not provided default aws creds strategy will be applied
# - password(optional) : if not provided default aws creds strategy will be applied
# - region(optional): if not provided default aws region strategy will be applied
# - endpoint(optional): if not provided default aws endpoint will be used, supported for custom s3 like implementation, like Ceph
# - bucket(required): s3 bucket name
# - prefix(optional): prefix for object key
def use_s3_cache(options)
DSL.shared_s3_cache_enabled = true
DSL.s3_options = options
end

# Add custom xcodebuild option to the prebuilding action
#
# You may use this for your special demands. For example: the default archs in dSYMs
Expand Down Expand Up @@ -65,6 +92,13 @@ def set_custom_xcodebuild_options_for_prebuilt_frameworks(options)
class_attr_accessor :dont_remove_source_code
dont_remove_source_code = false

class_attr_accessor :shared_cache_enabled
shared_cache_enabled = false
class_attr_accessor :shared_s3_cache_enabled
shared_s3_cache_enabled = false
class_attr_accessor :s3_options
s3_options = {}

class_attr_accessor :custom_build_options
class_attr_accessor :custom_build_options_simulator
self.custom_build_options = []
Expand Down
20 changes: 17 additions & 3 deletions lib/cocoapods-binary/Prebuild.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require_relative 'rome/build_framework'
require_relative 'helper/passer'
require_relative 'helper/target_checker'
require_relative 'helper/shared_cache'


# patch prebuild ability
Expand Down Expand Up @@ -70,7 +71,11 @@ def prebuild_frameworks!
# build options
sandbox_path = sandbox.root
existed_framework_folder = sandbox.generate_framework_path
bitcode_enabled = Pod::Podfile::DSL.bitcode_enabled
options = [
Podfile::DSL.bitcode_enabled,
Podfile::DSL.custom_build_options,
Podfile::DSL.custom_build_options_simulator
]
targets = []

if local_manifest != nil
Expand Down Expand Up @@ -123,7 +128,15 @@ def prebuild_frameworks!

output_path = sandbox.framework_folder_path_for_target_name(target.name)
output_path.mkpath unless output_path.exist?
Pod::Prebuild.build(sandbox_path, target, output_path, bitcode_enabled, Podfile::DSL.custom_build_options, Podfile::DSL.custom_build_options_simulator)

if Prebuild::SharedCache.has?(target, options)
framework_cache_path = Prebuild::SharedCache.local_framework_cache_path_for(target, options)
UI.puts "Using #{target.label} from cache"
FileUtils.cp_r "#{framework_cache_path}/.", output_path
else
Pod::Prebuild.build(sandbox_path, target, output_path, *options)
Prebuild::SharedCache.cache(target, output_path, options)
end

# save the resource paths for later installing
if target.static_framework? and !target.resource_paths.empty?
Expand Down Expand Up @@ -165,6 +178,7 @@ def prebuild_frameworks!
# This is for target with only .a and .h files
if not target.should_build?
Prebuild::Passer.target_names_to_skip_integration_framework << target.name
target_folder.mkpath unless target_folder.exist?
FileUtils.cp_r(root_path, target_folder, :remove_destination => true)
next
end
Expand Down Expand Up @@ -231,4 +245,4 @@ def prebuild_frameworks!


end
end
end
147 changes: 147 additions & 0 deletions lib/cocoapods-binary/helper/shared_cache.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
require 'aws-sdk-s3'
require 'digest'
require_relative '../tool/tool'
require 'zip'

module Pod
class Prebuild
class SharedCache
extend Config::Mixin

# `true` if there is cache for the target
# `false` otherwise
#
# @return [Boolean]
def self.has?(target, options)
has_local_cache_for(target, options) || has_s3_cache_for(target, options)
end

# `true` if there is local cache for the target
# `false` otherwise
#
# @return [Boolean]
def self.has_local_cache_for?(target, options)
if Podfile::DSL.shared_cache_enabled
path = local_framework_cache_path_for(target, options)
path.exist?
else
false
end
end

# `true` if there is s3 cache for the target
# `false` otherwise
#
# @return [Boolean]
def has_s3_cache_for?(target, options)
result = false
if Podfile::DSL.shared_s3_cache_enabled
s3_cache_path = s3_framework_cache_path_for(target, options)
s3_cache_path = Podfile::DSL.s3_options[:prefix] + s3_cache_path unless Podfile::DSL.s3_options[:prefix].nil?
s3 = Aws::S3::Resource.new(create_s3_options)
if s3.bucket(Podfile::DSL.s3_options[:bucket]).object("#{s3_cache_path}").exists?
Dir.mktmpdir {|dir|
s3.bucket(Podfile::DSL.s3_options[:bucket]).object("#{s3_cache_path}").get(response_target: "#{dir}/framework.zip")
unzip("#{dir}/framework.zip", path)
result = true
}
end
end
result
end

# @return [{}] AWS connection options
def self.create_s3_options
options = {}
creds = Aws::Credentials.new(Podfile::DSL.s3_options[:login], Podfile::DSL.s3_options[:password]) unless Podfile::DSL.s3_options[:login].nil? and Podfile::DSL.s3_options[:password].nil?
options[:credentials] = creds unless creds.nil?
options[:region] = Podfile::DSL.s3_options[:region] unless Podfile::DSL.s3_options[:region].nil?
options[:endpoint] = Podfile::DSL.s3_options[:endpoint] unless Podfile::DSL.s3_options[:endpoint].nil?

options
end

def self.zip(dir, zip_dir)
Zip::File.open(zip_dir, Zip::File::CREATE)do |zipfile|
Find.find(dir) do |path|
Find.prune if File.basename(path)[0] == ?.
dest = /#{dir}\/(\w.*)/.match(path)
# Skip files if they exists
begin
zipfile.add(dest[1],path) if dest
rescue Zip::ZipEntryExistsError
end
end
end
end

def self.unzip(zip, unzip_dir, remove_after = false)
Zip::File.open(zip) do |zip_file|
zip_file.each do |f|
f_path=File.join(unzip_dir, f.name)
FileUtils.mkdir_p(File.dirname(f_path))
zip_file.extract(f, f_path) unless File.exist?(f_path)
end
end
FileUtils.rm(zip) if remove_after
end

# Copies input_path to target's cache and save to s3 if applicable
def self.cache(target, input_path, options)
if not Podfile::DSL.shared_cache_enabled
return
end
cache_path = local_framework_cache_path_for(target, options)
cache_path.mkpath unless cache_path.exist?
FileUtils.cp_r "#{input_path}/.", cache_path
if Podfile::DSL.shared_s3_cache_enabled
s3_cache_path = s3_framework_cache_path_for(target, options)
s3 = Aws::S3::Resource.new(create_s3_options)
Dir.mktmpdir {|dir|
zip(cache_path, "#{dir}/framework.zip")
s3.bucket(Podfile::DSL.s3_options[:bucket]).object("#{Podfile::DSL.s3_options[:prefix]}/#{s3_cache_path}").upload_file("#{dir}/framework.zip")
}
end
end

# Path of the target's local cache
#
# @return [Pathname]
def self.local_framework_cache_path_for(target, options)
framework_cache_path = cache_root + xcode_version
framework_cache_path = framework_cache_path + target.name
framework_cache_path = framework_cache_path + target.version
options_with_platform = options + [target.platform.name]
framework_cache_path = framework_cache_path + Digest::MD5.hexdigest(options_with_platform.to_s).to_s
end

# Path of the target's s3 cache
#
# @return [Pathname]
def self.s3_framework_cache_path_for(target, options)
framework_cache_path = Pathname.new('') + xcode_version
framework_cache_path = framework_cache_path + target.name
framework_cache_path = framework_cache_path + target.version
options_with_platform = options + [target.platform.name]
framework_cache_path = framework_cache_path + Digest::MD5.hexdigest(options_with_platform.to_s).to_s
end

# Current xcode version.
#
# @return [String]
private
class_attr_accessor :xcode_version
# Converts from "Xcode 10.2.1\nBuild version 10E1001\n" to "10.2.1".
self.xcode_version = `xcodebuild -version`.split("\n").first.split().last || "Unkwown"

# Path of the cache folder
# Reusing cache_root from cocoapods's config
# `~Library/Caches/CocoaPods` is default value
#
# @return [Pathname]
private
class_attr_accessor :cache_root
self.cache_root = config.cache_root + 'Prebuilt'
end
end
end