diff --git a/README.md b/README.md index a1aa947..7aa2152 100644 --- a/README.md +++ b/README.md @@ -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 @@ -74,5 +76,3 @@ If bitcode is needed, add a `enable_bitcode_for_prebuilt_frameworks!` before all MIT Appreciate a 🌟 if you like it. - - diff --git a/cocoapods-binary.gemspec b/cocoapods-binary.gemspec index 697aeb2..4a0d9ff 100644 --- a/cocoapods-binary.gemspec +++ b/cocoapods-binary.gemspec @@ -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' diff --git a/lib/cocoapods-binary/Main.rb b/lib/cocoapods-binary/Main.rb index 78fcf41..5908478 100644 --- a/lib/cocoapods-binary/Main.rb +++ b/lib/cocoapods-binary/Main.rb @@ -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: //// + # 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 @@ -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 = [] diff --git a/lib/cocoapods-binary/Prebuild.rb b/lib/cocoapods-binary/Prebuild.rb index 77bc834..59266b0 100644 --- a/lib/cocoapods-binary/Prebuild.rb +++ b/lib/cocoapods-binary/Prebuild.rb @@ -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 @@ -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 @@ -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? @@ -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 @@ -231,4 +245,4 @@ def prebuild_frameworks! end -end \ No newline at end of file +end diff --git a/lib/cocoapods-binary/helper/shared_cache.rb b/lib/cocoapods-binary/helper/shared_cache.rb new file mode 100644 index 0000000..0c5ace7 --- /dev/null +++ b/lib/cocoapods-binary/helper/shared_cache.rb @@ -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 \ No newline at end of file