├── VERSION ├── integration_tests ├── .gitignore ├── LocalPods │ ├── UICommonsStatic │ │ ├── Resources │ │ │ └── static.json │ │ ├── UICommonsStatic.podspec │ │ └── Classes │ │ │ └── UICommons.swift │ └── UICommonsDynamic │ │ ├── Resources │ │ └── dynamic.json │ │ ├── UICommonsDynamic.podspec │ │ └── Classes │ │ └── UICommons.swift ├── PrebuiltPodIntegration │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── ViewController.swift │ ├── AppDelegate.swift │ ├── Info.plist │ └── Base.lproj │ │ └── LaunchScreen.storyboard ├── PrebuiltPodIntegrationTests │ ├── Tests │ │ ├── UICommonsStaticTests.swift │ │ ├── UICommonsDynamicTests.swift │ │ └── StaticFrameworkResourcesTests.swift │ └── Info.plist ├── Podfile ├── Podfile.lock ├── prebuilt_cache │ └── default │ │ └── Manifest.lock └── PrebuiltPodIntegration.xcodeproj │ └── xcshareddata │ └── xcschemes │ └── PrebuiltPodIntegration.xcscheme ├── .rspec ├── spec ├── command │ ├── config_spec.rb │ ├── binary_command_spec.rb │ └── prebuild_command_spec.rb ├── spec_helper │ ├── tempdir.rb │ ├── tempfile.rb │ └── lockfile.rb ├── spec_helper.rb ├── integration │ ├── prebuilt_source_installer_spec.rb │ └── installer_spec.rb ├── cahe_validator │ ├── validator_with_podfile_spec.rb │ ├── validator_with_dev_pods_spec.rb │ └── validation_result_spec.rb ├── env │ └── env_spec.rb ├── helper │ ├── json_spec.rb │ ├── lockfile_spec.rb │ └── podspec_spec.rb └── prebuild │ └── prebuild_installer_spec.rb ├── resources ├── benchmark.png └── realproj_buildtime_trend.png ├── docs ├── resources │ ├── deps_graph.png │ └── pods_cache_flow.png ├── best_practices.md ├── troubleshooting_guidelines.md ├── how_it_works.md └── configure_cocoapods_binary_cache.md ├── PodBinaryCacheExample ├── Gemfile ├── PodBinCacheExample │ ├── Assets.xcassets │ │ ├── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── ViewController.swift │ ├── Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── AppDelegate.swift ├── .gitignore ├── rebuild.sh ├── PodBinCacheExample.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── PodBinCacheExample.xcscheme ├── PodBinCacheExample.xcworkspace │ ├── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ └── contents.xcworkspacedata ├── DevPods │ ├── ConfigService │ │ ├── Classes │ │ │ └── ServiceVariable.swift │ │ └── ConfigService.podspec │ └── ConfigSDK │ │ ├── ConfigSDK.podspec │ │ └── Classes │ │ └── ConfigVar.swift ├── BuildBenchMark.sh ├── Podfile ├── Gemfile.lock └── Podfile.lock ├── .gitlab-ci.yml ├── lib ├── cocoapods-binary-cache │ ├── cache │ │ ├── validator_accumulated.rb │ │ ├── validator_with_podfile.rb │ │ ├── all.rb │ │ ├── validator_non_dev_pods.rb │ │ ├── validator_exclusion.rb │ │ ├── validator.rb │ │ ├── validator_dev_pods.rb │ │ ├── validator_dependencies_graph.rb │ │ ├── validation_result.rb │ │ └── validator_base.rb │ ├── pod-binary │ │ ├── helper │ │ │ ├── podfile_options.rb │ │ │ ├── sandbox.rb │ │ │ ├── names.rb │ │ │ ├── detected_prebuilt_pods │ │ │ │ ├── installer.rb │ │ │ │ └── target_definition.rb │ │ │ ├── build.rb │ │ │ ├── target_checker.rb │ │ │ └── prebuild_sandbox.rb │ │ ├── prebuild_dsl.rb │ │ ├── prebuild_hook.rb │ │ ├── integration.rb │ │ ├── integration │ │ │ ├── patch │ │ │ │ ├── resolve_dependencies.rb │ │ │ │ ├── sandbox_analyzer_state.rb │ │ │ │ ├── embed_framework_script.rb │ │ │ │ └── source_installation.rb │ │ │ ├── validation.rb │ │ │ └── source_installer.rb │ │ └── LICENSE.txt │ ├── ui.rb │ ├── helper │ │ ├── path_utils.rb │ │ ├── benchmark_show.rb │ │ ├── json.rb │ │ ├── podspec.rb │ │ ├── checksum.rb │ │ └── lockfile.rb │ ├── diagnosis │ │ ├── base.rb │ │ ├── diagnosis.rb │ │ └── integration.rb │ ├── state_store.rb │ ├── hooks │ │ ├── post_install.rb │ │ └── pre_install.rb │ ├── env.rb │ ├── main.rb │ ├── pod-rome │ │ ├── LICENSE.txt │ │ └── xcodebuild_raw.rb │ ├── prebuild_output │ │ ├── output.rb │ │ └── metadata.rb │ └── dependencies_graph │ │ ├── graph_visualizer.rb │ │ └── dependencies_graph.rb ├── cocoapods-binary-cache.rb ├── cocoapods_plugin.rb └── command │ ├── push.rb │ ├── fetch.rb │ ├── helper │ └── zip.rb │ ├── executor │ ├── visualizer.rb │ ├── pusher.rb │ ├── base.rb │ ├── prebuilder.rb │ └── fetcher.rb │ ├── binary.rb │ ├── visualize.rb │ ├── prebuild.rb │ └── config.rb ├── Dangerfile ├── Rakefile ├── .gitignore ├── .flake8 ├── Gemfile ├── .github ├── workflows │ ├── lint.yml │ └── test.yml ├── ISSUE_TEMPLATE │ ├── FEATURE_REQUEST.md │ └── BUG_REPORT.md └── PULL_REQUEST_TEMPLATE.md ├── LICENSE ├── cocoapods-binary-cache.gemspec ├── CONTRIBUTING.md ├── scripts └── integration_test.sh ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile.lock └── README.md /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.14 -------------------------------------------------------------------------------- /integration_tests/.gitignore: -------------------------------------------------------------------------------- 1 | _Prebuilt/ 2 | _Prebuild/ 3 | _Prebuild_delta/ 4 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --require ./spec/spec_helper.rb 2 | --color 3 | --force-color 4 | --format documentation 5 | -------------------------------------------------------------------------------- /spec/command/config_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Pod::Config" do 2 | # TODO (thuyen): Write tests 3 | end 4 | -------------------------------------------------------------------------------- /integration_tests/LocalPods/UICommonsStatic/Resources/static.json: -------------------------------------------------------------------------------- 1 | { 2 | "one": 1, 3 | "two": 2 4 | } 5 | -------------------------------------------------------------------------------- /integration_tests/LocalPods/UICommonsDynamic/Resources/dynamic.json: -------------------------------------------------------------------------------- 1 | { 2 | "one": 1, 3 | "two": 2 4 | } 5 | -------------------------------------------------------------------------------- /resources/benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grab/cocoapods-binary-cache/HEAD/resources/benchmark.png -------------------------------------------------------------------------------- /docs/resources/deps_graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grab/cocoapods-binary-cache/HEAD/docs/resources/deps_graph.png -------------------------------------------------------------------------------- /docs/best_practices.md: -------------------------------------------------------------------------------- 1 | # Best practices 2 | 3 | 🚧 This documentation is under construction. Come back later to check it out. 4 | -------------------------------------------------------------------------------- /docs/resources/pods_cache_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grab/cocoapods-binary-cache/HEAD/docs/resources/pods_cache_flow.png -------------------------------------------------------------------------------- /PodBinaryCacheExample/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'cocoapods', '1.8.4' 3 | gem "cocoapods-binary-cache", :path => "../" -------------------------------------------------------------------------------- /resources/realproj_buildtime_trend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grab/cocoapods-binary-cache/HEAD/resources/realproj_buildtime_trend.png -------------------------------------------------------------------------------- /spec/spec_helper/tempdir.rb: -------------------------------------------------------------------------------- 1 | require "tmpdir" 2 | 3 | def create_tempdir(prefix_suffix = nil) 4 | Dir.mktmpdir(prefix_suffix) 5 | end 6 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | include: 2 | - project: 'mobile/dax-ios/cicd' 3 | ref: master 4 | file: '/config/cocoapods_binary_cache/.gitlab-ci.yml' 5 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegration/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /docs/troubleshooting_guidelines.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting guidelines 2 | 3 | 🚧 This documentation is under construction. Come back later to check it out. 4 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/validator_accumulated.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class AccumulatedCacheValidator < BaseCacheValidator 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | rubocop.lint( 2 | force_exclusion: true, 3 | inline_comment: true, 4 | only_report_new_offenses: true, 5 | include_cop_names: true 6 | ) 7 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /DerivedData 3 | /Pods 4 | /_Prebuild 5 | /_Prebuild_delta 6 | /.bundle 7 | __pycache__/ 8 | xcshareddata/ 9 | xcuserdata/ -------------------------------------------------------------------------------- /PodBinaryCacheExample/rebuild.sh: -------------------------------------------------------------------------------- 1 | find ./ -type l -delete 2 | rm -rf DerivedData 3 | rm -rf Pods/* 4 | bundle exec pod binary-cache --cmd=fetch 5 | bundle exec pod install 6 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/helper/podfile_options.rb: -------------------------------------------------------------------------------- 1 | require_relative "detected_prebuilt_pods/target_definition" 2 | require_relative "detected_prebuilt_pods/installer" 3 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | -------------------------------------------------------------------------------- /spec/spec_helper/tempfile.rb: -------------------------------------------------------------------------------- 1 | require "tempfile" 2 | 3 | def create_tempfile(basename = "", content: nil) 4 | Tempfile.new(basename).tap do |f| 5 | f << content unless content.nil? 6 | f.close 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/ui.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | module UI 3 | class << self 4 | def step(message) 5 | section("❯❯❯ Step: #{message}".magenta) { yield if block_given? } 6 | end 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/cocoapods_plugin.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | 4 | require "cocoapods-binary-cache/main" 5 | require "command/binary" 6 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | def specs(dir) 4 | FileList["spec/#{dir}/*_spec.rb"].shuffle.join(' ') 5 | end 6 | 7 | desc 'Runs all the specs' 8 | task :specs do 9 | sh "bundle exec bacon #{specs('**')}" 10 | end 11 | 12 | task :default => :specs 13 | 14 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/helper/sandbox.rb: -------------------------------------------------------------------------------- 1 | require_relative "prebuild_sandbox" 2 | 3 | module Pod 4 | class Sandbox 5 | def prebuild_sandbox 6 | @prebuild_sandbox ||= Pod::PrebuildSandbox.from_standard_sandbox(self) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | **/*/__pycache__ 3 | 4 | DerivedData/ 5 | *.xcworkspacedata 6 | *.xcworkspace 7 | *.xcodeproj/project.xcworkspace/xcshareddata 8 | UserInterfaceState.xcuserstate 9 | xcuserdata/ 10 | 11 | Pods/ 12 | build/ 13 | 14 | .bundle/ 15 | 16 | .stats/ 17 | .logs/ 18 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/prebuild_dsl.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Podfile 3 | module DSL 4 | def config_cocoapods_binary_cache(options) 5 | PodPrebuild.config.dsl_config = options 6 | PodPrebuild.config.validate_dsl_config 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/validator_with_podfile.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class PodfileChangesCacheValidator < BaseCacheValidator 3 | def validate(*) 4 | return PodPrebuild::CacheValidationResult.new if @prebuilt_lockfile.nil? || @podfile.nil? 5 | 6 | validate_with_podfile 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/helper/path_utils.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | 4 | class PathUtils 5 | def self.remove_last_path_component(path, num_components = 1) 6 | path.split("/")[0...-num_components].join("/") 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/diagnosis/base.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class BaseDiagnosis 3 | def initialize(options) 4 | @cache_validation = options[:cache_validation] 5 | @standard_sandbox = options[:standard_sandbox] 6 | @specs = (options[:specs] || []).map { |s| [s.name, s] }.to_h 7 | end 8 | 9 | def spec(name) 10 | @specs[name] 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/all.rb: -------------------------------------------------------------------------------- 1 | require_relative "validation_result" 2 | require_relative "validator_base" 3 | require_relative "validator_accumulated" 4 | require_relative "validator_with_podfile" 5 | require_relative "validator_non_dev_pods" 6 | require_relative "validator_dev_pods" 7 | require_relative "validator_dependencies_graph" 8 | require_relative "validator_exclusion" 9 | require_relative "validator" 10 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/helper/benchmark_show.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | 4 | require "benchmark" 5 | 6 | class BenchmarkShow 7 | def self.benchmark 8 | time = Benchmark.measure { yield } 9 | Pod::UI.puts "🕛 Time elapsed: #{time}" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/state_store.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | def self.state 3 | @state ||= State.new 4 | end 5 | 6 | class State 7 | def initialize 8 | @store = { 9 | :cache_validation => CacheValidationResult.new 10 | } 11 | end 12 | 13 | def update(data) 14 | @store.merge!(data) 15 | end 16 | 17 | def cache_validation 18 | @store[:cache_validation] 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegration/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PrebuiltPodIntegration 4 | // 5 | // Created by Ngoc Thuyen Trinh on 11/05/2020. 6 | // Copyright © 2020 Grab. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | final class ViewController: UIViewController { 12 | 13 | override func viewDidLoad() { 14 | super.viewDidLoad() 15 | view.backgroundColor = .white 16 | } 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/prebuild_hook.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper/podfile_options" 2 | require_relative "helper/prebuild_sandbox" 3 | 4 | Pod::HooksManager.register("cocoapods-binary-cache", :pre_install) do |installer_context| 5 | PodPrebuild::PreInstallHook.new(installer_context).run 6 | end 7 | 8 | Pod::HooksManager.register("cocoapods-binary-cache", :post_install) do |installer_context| 9 | PodPrebuild::PostInstallHook.new(installer_context).run 10 | end 11 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | 3 | ignore = 4 | # See the rules at: https://lintlyci.github.io/Flake8Rules/rules/.html 5 | E111, # indentation is not a multiple of four 6 | F403, # unable to detect undefined names 7 | E121, # continuation line under-indented for hanging indent 8 | E114, # indentation is not a multiple of four (comment) 9 | E124, # closing bracket does not match visual indentation 10 | 11 | exclude = 12 | .git, 13 | __pycache__, 14 | __init__.py, 15 | 16 | max-line-length = 120 -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegrationTests/Tests/UICommonsStaticTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICommonsStaticTests.swift 3 | // PrebuiltPodIntegrationTests 4 | // 5 | // Created by Ngoc Thuyen Trinh on 09/10/2020. 6 | // Copyright © 2020 Grab. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UICommonsStatic 11 | 12 | final class UICommonsStaticTests: XCTestCase { 13 | func testUICommonsStaticTests() { 14 | XCTAssertNotNil(UICommonsStatic.jsonString(from: "static")) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegrationTests/Tests/UICommonsDynamicTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UICommonsDynamicTests.swift 3 | // PrebuiltPodIntegrationTests 4 | // 5 | // Created by Ngoc Thuyen Trinh on 09/10/2020. 6 | // Copyright © 2020 Grab. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import UICommonsDynamic 11 | 12 | final class UICommonsDynamicTests: XCTestCase { 13 | func testUICommonsDynamicTests() { 14 | XCTAssertNotNil(UICommonsDynamic.jsonString(from: "dynamic")) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/validator_non_dev_pods.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class NonDevPodsCacheValidator < BaseCacheValidator 3 | def validate(*) 4 | return PodPrebuild::CacheValidationResult.new if @pod_lockfile.nil? 5 | 6 | validate_pods( 7 | pods: @pod_lockfile.non_dev_pods, 8 | subspec_pods: @pod_lockfile.subspec_vendor_pods, 9 | prebuilt_pods: @prebuilt_lockfile.nil? ? {} : @prebuilt_lockfile.non_dev_pods 10 | ) 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/validator_exclusion.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class ExclusionCacheValidator < AccumulatedCacheValidator 3 | def initialize(options) 4 | super(options) 5 | @ignored_pods = options[:ignored_pods] || Set.new 6 | @prebuilt_pod_names = options[:prebuilt_pod_names] 7 | end 8 | 9 | def validate(accumulated) 10 | validation = @prebuilt_pod_names.nil? ? accumulated : accumulated.keep(@prebuilt_pod_names) 11 | validation.discard(@ignored_pods) 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | # Specify your gem"s dependencies in cocoapods-binary-cache.gemspec 4 | gemspec 5 | 6 | group :development do 7 | gem "bacon" 8 | gem "cocoapods" 9 | gem "mocha" 10 | gem "mocha-on-bacon" 11 | gem "prettybacon" 12 | end 13 | 14 | group :test do 15 | gem "rspec" 16 | gem "xcpretty" 17 | end 18 | 19 | group :lint do 20 | gem "danger" 21 | gem "danger-rubocop" 22 | gem "rubocop", "0.84.0" 23 | end 24 | 25 | group :debug do 26 | gem "pry" 27 | gem "pry-nav" 28 | gem "pry-rescue" 29 | end 30 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "cocoapods-core" 2 | require "cocoapods" 3 | require "cocoapods-binary-cache" 4 | require "cocoapods_plugin" 5 | 6 | require_relative "spec_helper/lockfile" 7 | require_relative "spec_helper/tempfile" 8 | require_relative "spec_helper/tempdir" 9 | 10 | module Pod 11 | UI.disable_wrap = true 12 | module UI 13 | class << self 14 | def puts(message = "") 15 | end 16 | 17 | def warn(message = "", actions = []) 18 | end 19 | 20 | def print(message) 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample/ViewController.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 3 | Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 4 | */ 5 | 6 | import UIKit 7 | import Alamofire 8 | import ConfigService 9 | 10 | class ViewController: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | let serviceVar = ServiceVariable() 16 | serviceVar.save() 17 | serviceVar.reload() 18 | serviceVar.reload2() 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /lib/command/push.rb: -------------------------------------------------------------------------------- 1 | require_relative "executor/pusher" 2 | 3 | module Pod 4 | class Command 5 | class Binary < Command 6 | class Push < Binary 7 | self.arguments = [CLAide::Argument.new("CACHE-BRANCH", false)] 8 | def initialize(argv) 9 | super 10 | @pusher = PodPrebuild::CachePusher.new( 11 | config: prebuild_config, 12 | cache_branch: argv.shift_argument || "master" 13 | ) 14 | end 15 | 16 | def run 17 | @pusher.run 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/command/fetch.rb: -------------------------------------------------------------------------------- 1 | require_relative "executor/fetcher" 2 | 3 | module Pod 4 | class Command 5 | class Binary < Command 6 | class Fetch < Binary 7 | self.arguments = [CLAide::Argument.new("CACHE-BRANCH", false)] 8 | def initialize(argv) 9 | super 10 | @fetcher = PodPrebuild::CacheFetcher.new( 11 | config: prebuild_config, 12 | cache_branch: argv.shift_argument || "master" 13 | ) 14 | end 15 | 16 | def run 17 | @fetcher.run 18 | end 19 | end 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/integration.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper/podfile_options" 2 | require_relative "helper/prebuild_sandbox" 3 | require_relative "helper/sandbox" 4 | require_relative "helper/names" 5 | require_relative "helper/target_checker" 6 | require_relative "integration/alter_specs" 7 | require_relative "integration/validation" 8 | require_relative "integration/patch/embed_framework_script" 9 | require_relative "integration/patch/sandbox_analyzer_state" 10 | require_relative "integration/patch/resolve_dependencies" 11 | require_relative "integration/patch/source_installation" 12 | -------------------------------------------------------------------------------- /lib/command/helper/zip.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | module ZipUtils 3 | def self.zip(path, to_dir: nil) 4 | basename = File.basename(path) 5 | out_path = to_dir.nil? ? "#{basename}.zip" : "#{to_dir}/#{basename}.zip" 6 | cmd = [] 7 | cmd << "cd" << File.dirname(path) 8 | cmd << "&& zip -r --symlinks" << out_path << basename 9 | cmd << "&& cd -" 10 | `#{cmd.join(" ")}` 11 | end 12 | 13 | def self.unzip(path, to_dir: nil) 14 | cmd = [] 15 | cmd << "unzip -nq" << path 16 | cmd << "-d" << to_dir unless to_dir.nil? 17 | `#{cmd.join(" ")}` 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/DevPods/ConfigService/Classes/ServiceVariable.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 3 | Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 4 | */ 5 | 6 | import ConfigSDK 7 | 8 | public final class ServiceVariable { 9 | private let raw: ConfigVar 10 | 11 | public init() { 12 | raw = ConfigVar() 13 | } 14 | 15 | public func reload() { 16 | raw.reload() 17 | } 18 | 19 | public func reload2() { 20 | raw.reload2() 21 | } 22 | 23 | public func save() { 24 | raw.saveToPersistent() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spec/command/binary_command_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Pod::Command::Binary" do 2 | describe "#initialize" do 3 | let(:args) { [] } 4 | before do 5 | PodPrebuild.config.reset! 6 | allow_any_instance_of(PodPrebuild::CachePrebuilder).to receive(:run) 7 | @command = Pod::Command::Binary.new(CLAide::ARGV.new(args)) 8 | end 9 | after do 10 | PodPrebuild.config.reset! 11 | end 12 | 13 | context "option --repo is specified" do 14 | let(:args) { ["--repo=custom"] } 15 | it "updates :repo to CLI config" do 16 | expect(PodPrebuild.config.cli_config[:repo]).to eq("custom") 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/DevPods/ConfigService/ConfigService.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'ConfigService' 3 | s.version = '0.0.1' 4 | s.summary = 'Config Service' 5 | 6 | s.description = 'ConfigService description' 7 | 8 | s.homepage = 'https://github.com/example' 9 | s.license = 'Grab' 10 | s.author = 'GrabTaxi Holdings Pte Ltd' 11 | s.source = { :path => "ExperimentService" } 12 | 13 | s.ios.deployment_target = '10.0' 14 | s.static_framework = false 15 | 16 | s.source_files = 'Classes/**/*' 17 | 18 | s.dependency 'RxSwift' 19 | s.dependency 'ConfigSDK' 20 | end 21 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/DevPods/ConfigSDK/ConfigSDK.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'ConfigSDK' 3 | s.version = '0.0.1' 4 | s.summary = 'A short description' 5 | 6 | s.description = 'ConfigSDK description' 7 | 8 | s.homepage = 'https://github.com/example' 9 | s.license = 'Grab' 10 | s.author = { "Bang" => "bang@grabtaxi.com" } 11 | s.source = { :git => 'https://github.com', :tag => s.version.to_s } 12 | 13 | s.ios.deployment_target = '10.0' 14 | s.static_framework = false 15 | 16 | s.source_files = 'Classes/**/*' 17 | 18 | s.dependency 'SQLite.swift' 19 | s.dependency 'ProtocolBuffers-Swift' 20 | end 21 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/hooks/post_install.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class PostInstallHook 3 | def initialize(installer_context) 4 | @installer_context = installer_context 5 | end 6 | 7 | def run 8 | diagnose if PodPrebuild::Env.integration_stage? 9 | end 10 | 11 | private 12 | 13 | def diagnose 14 | Pod::UI.title("Diagnosing cocoapods-binary-cache") do 15 | PodPrebuild::Diagnosis.new( 16 | cache_validation: PodPrebuild.state.cache_validation, 17 | standard_sandbox: @installer_context.sandbox, 18 | specs: @installer_context.umbrella_targets.map(&:specs).flatten 19 | ).run 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegration/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PrebuiltPodIntegration 4 | // 5 | // Created by Ngoc Thuyen Trinh on 11/05/2020. 6 | // Copyright © 2020 Grab. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | @UIApplicationMain 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | window = UIWindow(frame: UIScreen.main.bounds) 18 | window?.rootViewController = ViewController() 19 | window?.makeKeyAndVisible() 20 | return true 21 | } 22 | } 23 | 24 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/helper/json.rb: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | module PodPrebuild 4 | class JSONFile 5 | attr_reader :path 6 | attr_reader :data 7 | 8 | def initialize(path) 9 | @path = path 10 | @data = load_json 11 | end 12 | 13 | def empty? 14 | @data.empty? 15 | end 16 | 17 | def [](key) 18 | @data[key] 19 | end 20 | 21 | def []=(key, value) 22 | @data[key] = value 23 | end 24 | 25 | def save! 26 | File.open(@path, "w") { |f| f.write(JSON.pretty_generate(@data)) } 27 | end 28 | 29 | private 30 | 31 | def load_json 32 | File.open(@path) { |f| JSON.parse(f.read) } 33 | rescue 34 | {} 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | danger: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - name: Ruby setup 15 | uses: actions/setup-ruby@v1 16 | with: 17 | ruby-version: 2.7 18 | - name: Bundle installation 19 | run: | 20 | gem install bundler:2.1.2 21 | bundle install 22 | - name: Rubocop 23 | run: | 24 | export DANGER_GITHUB_API_TOKEN="${{ secrets.GITHUB_TOKEN }}" 25 | bundle exec danger --verbose || echo "Danger comments might not work with forked-repos" 26 | -------------------------------------------------------------------------------- /integration_tests/LocalPods/UICommonsStatic/UICommonsStatic.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "UICommonsStatic" 3 | s.version = "0.0.1" 4 | s.summary = "UICommonsStatic" 5 | s.description = "UICommonsStatic" 6 | s.homepage = "https://github.com/grab/cocoapods-binary-cache" 7 | s.license = "Grab" 8 | s.author = "GrabTaxi Holdings Pte Ltd" 9 | s.source = { :git => "https://github.com/grab/cocoapods-binary-cache.git", :tag => s.version.to_s } 10 | 11 | s.ios.deployment_target = "10.0" 12 | 13 | s.source_files = "Classes/**/*" 14 | s.resource_bundle = { 15 | "UICommonsStatic" => ["Resources/**/*.json"] 16 | } 17 | 18 | s.frameworks = "UIKit" 19 | end 20 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/env.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class Env 3 | @stage_idx = 0 4 | 5 | class << self 6 | def reset! 7 | @stage_idx = 0 8 | @stages = nil 9 | end 10 | 11 | def next_stage! 12 | @stage_idx += 1 if @stage_idx < stages.count - 1 13 | end 14 | 15 | def stages 16 | @stages ||= PodPrebuild.config.prebuild_job? ? [:prebuild, :integration] : [:integration] 17 | end 18 | 19 | def current_stage 20 | stages[@stage_idx] 21 | end 22 | 23 | def prebuild_stage? 24 | current_stage == :prebuild 25 | end 26 | 27 | def integration_stage? 28 | current_stage == :integration 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/helper/podspec.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Specification 3 | def empty_source_files? 4 | unless subspecs.empty? 5 | # return early if there are some files in subpec(s) but process the spec itself 6 | return false unless subspecs.all?(&:empty_source_files?) 7 | end 8 | 9 | check = lambda do |patterns| 10 | patterns = [patterns] if patterns.is_a?(String) 11 | patterns.reject(&:empty?).all? do |pattern| 12 | Xcodeproj::Constants::HEADER_FILES_EXTENSIONS.any? { |ext| pattern.end_with?(ext) } 13 | end 14 | end 15 | available_platforms.all? do |platform| 16 | check.call(consumer(platform).source_files) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /integration_tests/LocalPods/UICommonsDynamic/UICommonsDynamic.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = "UICommonsDynamic" 3 | s.version = "0.0.1" 4 | s.summary = "UICommonsDynamic" 5 | s.description = "UICommonsDynamic" 6 | s.homepage = "https://github.com/grab/cocoapods-binary-cache" 7 | s.license = "Grab" 8 | s.author = "GrabTaxi Holdings Pte Ltd" 9 | s.source = { :git => "https://github.com/grab/cocoapods-binary-cache.git", :tag => s.version.to_s } 10 | 11 | s.ios.deployment_target = "10.0" 12 | 13 | s.source_files = "Classes/**/*" 14 | s.resource_bundle = { 15 | "UICommonsDynamic" => ["Resources/**/*.json"] 16 | } 17 | 18 | s.frameworks = "UIKit" 19 | end 20 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/helper/checksum.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | 4 | require "digest/md5" 5 | 6 | class FolderChecksum 7 | def self.git_checksum(dir) 8 | checksum_of_files(`git ls-files #{File.realdirpath(dir).shellescape}`.split("\n")) 9 | rescue => e 10 | Pod::UI.warn "Cannot get checksum of tracked files under #{dir}: #{e}" 11 | checksum_of_files(Dir["#{dir}/**/*"].reject { |f| File.directory?(f) }) 12 | end 13 | 14 | def self.checksum_of_files(files) 15 | checksums = files.sort.map { |f| Digest::MD5.hexdigest(File.read(f)) } 16 | Digest::MD5.hexdigest(checksums.join) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/diagnosis/diagnosis.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "integration" 3 | 4 | module PodPrebuild 5 | class Diagnosis 6 | def initialize(options) 7 | @diagnosers = [ 8 | IntegrationDiagnosis 9 | ].map { |klazz| klazz.new(options) } 10 | end 11 | 12 | def run 13 | diagnosis = @diagnosers.map(&:run) 14 | errors = diagnosis.select { |d| d[0] == :error }.map { |d| d[1] } 15 | warnings = diagnosis.select { |d| d[0] == :error }.map { |d| d[1] } 16 | 17 | warnings.each { |d| Pod::UI.puts "⚠️ #{d[1]}" } 18 | errors.each { |d| Pod::UI.puts "🚩 #{d[1]}" } 19 | return if errors.empty? || !PodPrebuild.config.strict_diagnosis? 20 | 21 | raise "There are #{errors.count} error(s) spotted after the diagnosis" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/command/executor/visualizer.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "../../cocoapods-binary-cache/dependencies_graph/dependencies_graph" 3 | 4 | module PodPrebuild 5 | class Visualizer < CommandExecutor 6 | def initialize(options) 7 | super(options) 8 | @lockfile = options[:lockfile] 9 | @open = options[:open] 10 | @output_dir = options[:output_dir] 11 | @devpod_only = options[:devpod_only] 12 | @max_deps = options[:max_deps] 13 | end 14 | 15 | def run 16 | FileUtils.mkdir_p(@output_dir) 17 | graph = DependenciesGraph.new(lockfile: @lockfile, devpod_only: @devpod_only, max_deps: @max_deps) 18 | output_path = "#{@output_dir}/graph.png" 19 | graph.write_graphic_file(output_path: output_path) 20 | system("open #{@output_path}") if @open 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /integration_tests/LocalPods/UICommonsDynamic/Classes/UICommons.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | public struct UICommonsDynamic { 5 | private class DummyDynamic { } 6 | 7 | public static func jsonString(from fileName: String, fileExtension: String = ".json") -> String? { 8 | return dataFromFile(fileName, fileExtension: fileExtension) 9 | .flatMap { String(data: $0, encoding: .utf8) } 10 | } 11 | 12 | private static func dataFromFile(_ fileName: String, fileExtension: String) -> Data? { 13 | guard 14 | let bundlePath = Bundle(for: DummyDynamic.self).path(forResource: "UICommonsDynamic", ofType: "bundle"), 15 | let bundle = Bundle(path: bundlePath), 16 | let pathUrl = bundle.url(forResource: fileName, withExtension: fileExtension) 17 | else { return nil } 18 | return try? Data(contentsOf: pathUrl) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Request a feature 3 | about: If something can be improved, to make the plugin better 4 | --- 5 | 6 | ### Checklist 7 | 8 | - [ ] I've read the [Contribution Guidelines](https://github.com/grab/cocoapods-binary-cache/blob/master/CONTRIBUTING.md) 9 | - [ ] I've searched for [existing GitHub issues](https://github.com/grab/cocoapods-binary-cache/issues) if there was such a request before. 10 | 11 | ### Description 12 | #### Motivation 13 | 14 |
[Details go here]
15 | 16 | #### Summary 17 | 18 |
[Details go here]
19 | -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegrationTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/validator.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class CacheValidator 3 | def initialize(options) 4 | @validators = [ 5 | PodPrebuild::PodfileChangesCacheValidator.new(options), 6 | PodPrebuild::NonDevPodsCacheValidator.new(options) 7 | ] 8 | @validators << PodPrebuild::DevPodsCacheValidator.new(options) if PodPrebuild.config.dev_pods_enabled? 9 | @validators << PodPrebuild::DependenciesGraphCacheValidator.new(options) 10 | @validators << PodPrebuild::ExclusionCacheValidator.new(options) 11 | end 12 | 13 | def validate(*) 14 | @validators.reduce(PodPrebuild::CacheValidationResult.new) do |acc, validator| 15 | validation = validator.validate(acc) 16 | validator.is_a?(AccumulatedCacheValidator) ? validation : acc.merge(validation) 17 | end 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /integration_tests/LocalPods/UICommonsStatic/Classes/UICommons.swift: -------------------------------------------------------------------------------- 1 | 2 | import Foundation 3 | 4 | public struct UICommonsStatic { 5 | private class DummyStatic { } 6 | 7 | public static func jsonString(from fileName: String, fileExtension: String = ".json") -> String? { 8 | return dataFromFile(fileName, fileExtension: fileExtension) 9 | .flatMap { String(data: $0, encoding: .utf8) } 10 | } 11 | 12 | private static func dataFromFile(_ fileName: String, fileExtension: String) -> Data? { 13 | guard 14 | let bundlePath = Bundle(for: DummyStatic.self).path(forResource: "UICommonsStatic", ofType: "bundle"), 15 | let bundle = Bundle(path: bundlePath), 16 | let pathUrl = bundle.url(forResource: fileName, withExtension: fileExtension), 17 | let fileData = try? Data(contentsOf: pathUrl) 18 | else { return nil } 19 | return fileData 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Checklist 10 | 11 | - [ ] I've read the [Contribution Guidelines](https://github.com/grab/cocoapods-binary-cache/blob/master/CONTRIBUTING.md) 12 | - [ ] I've run `bundle exec rspec` from the root directory to run my changes against rspec tests 13 | - [ ] I've run `bundle exec rubocop` to check code style 14 | 15 | ### Description 16 | 17 |
[Details go here]
18 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/integration/patch/resolve_dependencies.rb: -------------------------------------------------------------------------------- 1 | # Let cocoapods use the prebuild framework files in install process. 2 | # 3 | # the code only effect the second pod install process. 4 | # 5 | module Pod 6 | class Installer 7 | # Modify specification to use only the prebuild framework after analyzing 8 | original_resolve_dependencies = instance_method(:resolve_dependencies) 9 | define_method(:resolve_dependencies) do 10 | original_resolve_dependencies.bind(self).call 11 | 12 | # check the pods 13 | # Although we have did it in prebuild stage, it's not sufficient. 14 | # Same pod may appear in another target in form of source code. 15 | # Prebuild.check_one_pod_should_have_only_one_target(prebuilt_pod_targets) 16 | validate_every_pod_only_have_one_form 17 | alter_specs_for_prebuilt_pods 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/integration/validation.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Installer 3 | def validate_every_pod_only_have_one_form 4 | multi_targets_pods = pod_targets 5 | .group_by(&:pod_name) 6 | .select do |_, targets| 7 | is_multi_targets = targets.map { |t| t.platform.name }.uniq.count > 1 8 | is_multi_forms = targets.map { |t| prebuilt_pod_targets.include?(t) }.uniq.count > 1 9 | is_multi_targets && is_multi_forms 10 | end 11 | return if multi_targets_pods.empty? 12 | 13 | warnings = "One pod can only be prebuilt or not prebuilt. These pod have different forms in multiple targets:\n" 14 | warnings += multi_targets_pods 15 | .map { |name, targets| " #{name}: #{targets.map { |t| t.platform.name }}" } 16 | .join("\n") 17 | raise Informative, warnings 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/command/executor/pusher.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | module PodPrebuild 4 | class CachePusher < CommandExecutor 5 | attr_reader :cache_branch 6 | 7 | def initialize(options) 8 | super(options) 9 | @cache_branch = options[:cache_branch] 10 | end 11 | 12 | def run 13 | Pod::UI.step("Pushing cache") do 14 | if @config.local_cache? 15 | print_message_for_local_cache 16 | else 17 | commit_and_push_cache 18 | end 19 | end 20 | end 21 | 22 | private 23 | 24 | def print_message_for_local_cache 25 | Pod::UI.puts "Skip pushing cache as you're using local cache".yellow 26 | end 27 | 28 | def commit_and_push_cache 29 | commit_message = "Update prebuilt cache" 30 | git("add .") 31 | git("commit -m '#{commit_message}'") 32 | git("push origin #{@cache_branch}") 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/main.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | 4 | require_relative "ui" 5 | require_relative "dependencies_graph/dependencies_graph" 6 | require_relative "cache/all" 7 | require_relative "helper/benchmark_show" 8 | require_relative "helper/json" 9 | require_relative "helper/lockfile" 10 | require_relative "helper/path_utils" 11 | require_relative "helper/podspec" 12 | require_relative "env" 13 | require_relative "state_store" 14 | require_relative "hooks/post_install" 15 | require_relative "hooks/pre_install" 16 | require_relative "pod-binary/prebuild_dsl" 17 | require_relative "pod-binary/prebuild_hook" 18 | require_relative "pod-binary/prebuild" 19 | require_relative "prebuild_output/metadata" 20 | require_relative "prebuild_output/output" 21 | require_relative "diagnosis/diagnosis" 22 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/validator_dev_pods.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class DevPodsCacheValidator < BaseCacheValidator 3 | def validate(*) 4 | return PodPrebuild::CacheValidationResult.new if @pod_lockfile.nil? 5 | 6 | validate_pods( 7 | pods: @pod_lockfile.dev_pods, 8 | subspec_pods: [], 9 | prebuilt_pods: @prebuilt_lockfile.nil? ? {} : @prebuilt_lockfile.dev_pods 10 | ) 11 | end 12 | 13 | def incompatible_pod(name) 14 | diff = super(name) 15 | return diff unless diff.empty? 16 | 17 | incompatible_source(name) 18 | end 19 | 20 | def incompatible_source(name) 21 | diff = {} 22 | prebuilt_hash = read_source_hash(name) 23 | expected_hash = pod_lockfile.dev_pod_hash(name) 24 | unless prebuilt_hash == expected_hash 25 | diff[name] = { :prebuilt_hash => prebuilt_hash, :expected_hash => expected_hash} 26 | end 27 | diff 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/command/binary.rb: -------------------------------------------------------------------------------- 1 | require "fileutils" 2 | require_relative "config" 3 | require_relative "fetch" 4 | require_relative "prebuild" 5 | require_relative "push" 6 | require_relative "visualize" 7 | 8 | module Pod 9 | class Command 10 | class Binary < Command 11 | self.abstract_command = true 12 | def self.options 13 | [ 14 | ["--repo", "Cache repo (in accordance with `cache_repo` in `config_cocoapods_binary_cache`)"] 15 | ] 16 | end 17 | 18 | def initialize(argv) 19 | super 20 | load_podfile 21 | update_cli_config(:repo => argv.option("repo")) 22 | end 23 | 24 | def prebuild_config 25 | @prebuild_config ||= PodPrebuild.config 26 | end 27 | 28 | def load_podfile 29 | Pod::Config.instance.podfile 30 | end 31 | 32 | def update_cli_config(options) 33 | PodPrebuild.config.cli_config.merge!(options) 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/diagnosis/integration.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | 3 | module PodPrebuild 4 | class IntegrationDiagnosis < BaseDiagnosis 5 | def run 6 | should_be_integrated = if PodPrebuild.config.prebuild_job? \ 7 | then @cache_validation.hit + @cache_validation.missed \ 8 | else @cache_validation.hit \ 9 | end 10 | should_be_integrated = should_be_integrated.map { |name| name.split("/")[0] }.to_set 11 | unintegrated = should_be_integrated.reject do |name| 12 | module_name = spec(name)&.module_name || name 13 | framework_path = \ 14 | @standard_sandbox.pod_dir(name) + \ 15 | PodPrebuild.config.prebuilt_path(path: "#{module_name}.framework") 16 | framework_path.exist? 17 | end 18 | return [] if unintegrated.empty? 19 | 20 | [[:error, "Unintegrated frameworks: #{unintegrated}"]] 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/validator_dependencies_graph.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class DependenciesGraphCacheValidator < AccumulatedCacheValidator 3 | def initialize(options) 4 | super(options) 5 | @ignored_pods = options[:ignored_pods] || Set.new 6 | end 7 | 8 | def validate(accumulated) 9 | return accumulated if library_evolution_supported? || @pod_lockfile.nil? 10 | 11 | dependencies_graph = DependenciesGraph.new(lockfile: @pod_lockfile.lockfile, invert_edge: true) 12 | clients = dependencies_graph.get_clients(accumulated.discard(@ignored_pods).missed.to_a) 13 | unless PodPrebuild.config.dev_pods_enabled? 14 | clients = clients.reject { |client| @pod_lockfile.dev_pods.keys.include?(client) } 15 | end 16 | 17 | missed = clients.map { |client| [client, "Dependencies were missed"] }.to_h 18 | accumulated.merge(PodPrebuild::CacheValidationResult.new(missed, Set.new)) 19 | end 20 | 21 | def library_evolution_supported? 22 | false 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/integration/patch/sandbox_analyzer_state.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Installer 3 | class Analyzer 4 | class SandboxAnalyzer 5 | original_analyze = instance_method(:analyze) 6 | define_method(:analyze) do 7 | state = original_analyze.bind(self).call 8 | state = alter_state(state) 9 | state 10 | end 11 | 12 | private 13 | 14 | def alter_state(state) 15 | return state if PodPrebuild.config.tracked_prebuilt_pod_names.empty? 16 | 17 | prebuilt = PodPrebuild.config.tracked_prebuilt_pod_names 18 | Pod::UI.message "Alter sandbox state: treat prebuilt frameworks as added: #{prebuilt.to_a}" 19 | SpecsState.new( 20 | :added => (state.added + prebuilt).uniq, 21 | :changed => state.changed - prebuilt, 22 | :removed => state.deleted - prebuilt, 23 | :unchanged => state.unchanged - prebuilt 24 | ) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/command/visualize.rb: -------------------------------------------------------------------------------- 1 | require_relative "executor/visualizer" 2 | 3 | module Pod 4 | class Command 5 | class Binary < Command 6 | class Viz < Binary 7 | self.arguments = [CLAide::Argument.new("OUTPUT-DIR", false)] 8 | def self.options 9 | [ 10 | ["--open", "Open the graph upon completion"], 11 | ["--devpod_only", "Only include development pod"], 12 | ["--max_deps", "Only include pod with number of dependencies <= max_deps"] 13 | ] 14 | end 15 | 16 | def initialize(argv) 17 | super 18 | @visualizer = PodPrebuild::Visualizer.new( 19 | config: prebuild_config, 20 | lockfile: config.lockfile, 21 | output_dir: argv.shift_argument || ".", 22 | open: argv.flag?("open"), 23 | devpod_only: argv.flag?("devpod_only"), 24 | max_deps: argv.option("max_deps") 25 | ) 26 | end 27 | 28 | def run 29 | @visualizer.run 30 | end 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/helper/names.rb: -------------------------------------------------------------------------------- 1 | # ABOUT NAMES 2 | # 3 | # There are many kinds of name in cocoapods. Two main names are widely used in this plugin. 4 | # - root_spec.name (spec.root_name, targe.pod_name): 5 | # aka "pod_name" 6 | # the name we use in podfile. the concept. 7 | # 8 | # - target.name: 9 | # aka "target_name" 10 | # the name of the final target in xcode project. the final real thing. 11 | # 12 | # One pod may have multiple targets in xcode project, due to one pod can be used in mutiple 13 | # platform simultaneously. So one `root_spec.name` may have multiple coresponding `target.name`s. 14 | # Therefore, map a spec to/from targets is a little complecated. It's one to many. 15 | # 16 | 17 | # Tool to transform Pod_name to target efficiently 18 | module Pod 19 | def self.fast_get_targets_for_pod_name(pod_name, targets, cache) 20 | pod_name = pod_name.split("/")[0] # Look for parent spec instead of subspecs 21 | if cache.empty? 22 | targets.select { |target| target.name == pod_name } 23 | else 24 | cache.first[pod_name] || [] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/integration/prebuilt_source_installer_spec.rb: -------------------------------------------------------------------------------- 1 | require "cocoapods-binary-cache/pod-binary/integration/patch/source_installation" 2 | 3 | describe "Pod::Installer::PrebuiltSourceInstaller" do 4 | describe "#install!" do 5 | let(:podfile) { Pod::Podfile.new } 6 | let(:tmp_dir) { create_tempdir } 7 | let(:sandbox) { Pod::Sandbox.new(tmp_dir) } 8 | let(:name) { "A" } 9 | before do 10 | @source_installer = Pod::Installer::PodSourceInstaller.new(sandbox, podfile, []) 11 | @installer = Pod::Installer::PrebuiltSourceInstaller.new( 12 | sandbox, 13 | podfile, 14 | [], 15 | source_installer: @source_installer 16 | ) 17 | allow(@installer).to receive(:name).and_return(name) 18 | allow(@installer).to receive(:install_prebuilt_framework!) 19 | end 20 | 21 | after do 22 | FileUtils.remove_entry tmp_dir 23 | end 24 | 25 | describe "download sources" do 26 | it "downloads sources by default" do 27 | expect(@source_installer).to receive(:install!) 28 | @installer.install! 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Grabtaxi Holdings PTE LTE (GRAB) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /lib/command/executor/base.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class CommandExecutor 3 | def initialize(options) 4 | @config = options[:config] 5 | prepare_cache_dir 6 | end 7 | 8 | def installer 9 | @installer ||= begin 10 | pod_config = Pod::Config.instance 11 | Pod::Installer.new(pod_config.sandbox, pod_config.podfile, pod_config.lockfile) 12 | end 13 | end 14 | 15 | def use_local_cache? 16 | @config.cache_repo.nil? 17 | end 18 | 19 | def prepare_cache_dir 20 | FileUtils.mkdir_p(@config.cache_path) if @config.cache_path 21 | end 22 | 23 | def git(cmd, options = {}) 24 | comps = ["git"] 25 | comps << "-C" << @config.cache_path unless options[:cache_repo] == false 26 | comps << cmd 27 | comps << "&> /dev/null" if options[:ignore_output] 28 | comps << "|| true" if options[:can_fail] 29 | cmd = comps.join(" ") 30 | raise "Fail to run command '#{cmd}'" unless system(cmd) 31 | end 32 | 33 | def git_clone(cmd, options = {}) 34 | git("clone #{cmd}", options.merge(:cache_repo => false)) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegrationTests/Tests/StaticFrameworkResourcesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StaticFrameworkResourcesTests.swift 3 | // PrebuiltPodIntegrationTests 4 | // 5 | // Created by Ngoc Thuyen Trinh on 11/05/2020. 6 | // Copyright © 2020 Grab. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import BKMoneyKit 11 | 12 | final class StaticFrameworkResourcesTests: XCTestCase { 13 | func testResourcesCopiedToMainBundle() { 14 | expectFiles(ofType: "png", inDir: "BKMoneyKit.bundle/CardLogo") 15 | expectFiles(ofType: "png", inDir: "GoogleMaps.bundle") 16 | expectFiles(ofType: "png", inDir: "GoogleSignIn.bundle") 17 | expectFiles(ofType: "png", inDir: "IQKeyboardManager.bundle") 18 | } 19 | 20 | private func expectFiles( 21 | ofType resourceType: String, 22 | inDir: String? = nil, 23 | _ file: StaticString = #file, 24 | _ line: UInt = #line 25 | ) { 26 | let paths = Bundle.main.paths(forResourcesOfType: resourceType, inDirectory: inDir) 27 | if paths.isEmpty { 28 | XCTFail("No resources of type \(resourceType) in dir: \(inDir ?? "nil")", file: file, line: line) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | rspec: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | - name: Ruby setup 16 | uses: actions/setup-ruby@v1 17 | with: 18 | ruby-version: 2.7 19 | - name: Bundle installation 20 | run: | 21 | gem install bundler:2.1.2 22 | bundle install 23 | - name: RSpec 24 | run: | 25 | bundle exec rspec 26 | 27 | integration_test: 28 | runs-on: macOS-latest 29 | strategy: 30 | matrix: 31 | mode: [non-prebuild, prebuild-changes, prebuild-all] 32 | xcframework: [false, true] 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v2 36 | - name: Bundle installation 37 | run: | 38 | gem install bundler:2.1.2 39 | bundle install 40 | - name: Integration tests 41 | run: | 42 | sh scripts/integration_test.sh ${{matrix.mode}} ${{matrix.xcframework}} 43 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/helper/detected_prebuilt_pods/installer.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Installer 3 | # Returns the names of pod targets detected as prebuilt, including 4 | # those declared in Podfile and their dependencies 5 | def prebuilt_pod_names 6 | prebuilt_pod_targets.map(&:name).to_set 7 | end 8 | 9 | # Returns the pod targets detected as prebuilt, including 10 | # those declared in Podfile and their dependencies 11 | def prebuilt_pod_targets 12 | @prebuilt_pod_targets ||= begin 13 | explicit_prebuilt_pod_names = aggregate_targets 14 | .flat_map { |target| target.target_definition.explicit_prebuilt_pod_names } 15 | .uniq 16 | 17 | targets = pod_targets.select { |target| explicit_prebuilt_pod_names.include?(target.pod_name) } 18 | dependencies = targets.flat_map(&:recursive_dependent_targets) # Treat dependencies as prebuilt pods 19 | all = (targets + dependencies).uniq 20 | all = all.reject { |target| sandbox.local?(target.pod_name) } unless PodPrebuild.config.dev_pods_enabled? 21 | all 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 leavez 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-rome/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Boris Bügling 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/cahe_validator/validator_with_podfile_spec.rb: -------------------------------------------------------------------------------- 1 | describe "PodPrebuild::PodfileChangesCacheValidator" do 2 | describe "#validate" do 3 | let(:pods) do 4 | { 5 | "A" => { :version => "0.0.5" }, 6 | "B" => { :version => "0.0.5" }, 7 | "C" => { :version => "0.0.5" } 8 | } 9 | end 10 | let(:prebuilt_lockfile) { gen_lockfile(pods: pods) } 11 | let(:dev_pods_enabled) { true } 12 | let(:podfile) do 13 | Pod::Podfile.new do 14 | source "https://cdn.cocoapods.org/" 15 | pod "A", "0.0.6" # Updated 16 | pod "B", "0.0.5" 17 | pod "C", "0.0.5" 18 | pod "D", "0.0.5" # Added 19 | pod "E", :path => "Local/" # Added, but local 20 | end 21 | end 22 | 23 | before do 24 | allow(PodPrebuild.config).to receive(:dev_pods_enabled).and_return(dev_pods_enabled) 25 | validation_result = PodPrebuild::PodfileChangesCacheValidator.new( 26 | podfile: podfile, 27 | prebuilt_lockfile: prebuilt_lockfile 28 | ).validate 29 | @missed = validation_result.missed 30 | @hit = validation_result.hit 31 | end 32 | 33 | it "returns changes as missed" do 34 | expect(@missed).to eq(Set["A", "D", "E"]) 35 | expect(@hit).to eq(Set["B", "C"]) 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/helper/build.rb: -------------------------------------------------------------------------------- 1 | require_relative "../../pod-rome/xcodebuild_raw" 2 | require_relative "../../pod-rome/xcodebuild_command" 3 | 4 | module PodPrebuild 5 | def self.build(options) 6 | targets = options[:targets] || [] 7 | return if targets.empty? 8 | 9 | options[:sandbox] = Pod::Sandbox.new(Pathname(options[:sandbox])) unless options[:sandbox].is_a?(Pod::Sandbox) 10 | options[:build_dir] = build_dir(options[:sandbox].root) 11 | 12 | case targets[0].platform.name 13 | when :ios, :tvos, :watchos 14 | PodPrebuild::XcodebuildCommand.new(options).run 15 | when :osx 16 | xcodebuild( 17 | sandbox: options[:sandbox], 18 | targets: targets, 19 | configuration: options[:configuration], 20 | sdk: "macosx", 21 | args: options[:args] 22 | ) 23 | else 24 | raise "Unsupported platform for '#{targets[0].name}': '#{targets[0].platform.name}'" 25 | end 26 | raise "The build directory was not found in the expected location" unless options[:build_dir].directory? 27 | end 28 | 29 | def self.remove_build_dir(sandbox_root) 30 | path = build_dir(sandbox_root) 31 | path.rmtree if path.exist? 32 | end 33 | 34 | def self.build_dir(sandbox_root) 35 | sandbox_root.parent + "build" 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /cocoapods-binary-cache.gemspec: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | 4 | lib = File.expand_path("lib", __dir__) 5 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "cocoapods-binary-cache" 9 | spec.version = File.read("VERSION") 10 | spec.authors = ["Bang Nguyen"] 11 | spec.email = ["bang.nguyen@grabtaxi.com"] 12 | spec.description = "Reduce build time by building pod frameworks and cache to remote storage, reuse on multiple machines" 13 | spec.summary = "Reduce build time by building pod frameworks and cache to remote storage, reuse on multiple machines" 14 | spec.homepage = "https://github.com/grab/cocoapods-binary-cache" 15 | spec.license = "MIT" 16 | 17 | spec.files = Dir["lib/**/*"] 18 | spec.require_paths = ["lib"] 19 | 20 | spec.add_dependency "cocoapods", ">= 1.5.0" 21 | spec.add_dependency "fourflusher", "~> 2.0" 22 | spec.add_dependency "rgl", "~> 0.5.6" 23 | spec.add_dependency "xcpretty", "~> 0.3.0" 24 | spec.add_dependency "parallel", "~> 1.0" 25 | 26 | spec.add_development_dependency "bundler", ">= 1.3" 27 | spec.add_development_dependency "rake", "~> 10.0" 28 | end 29 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/helper/detected_prebuilt_pods/target_definition.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Podfile 3 | class TargetDefinition 4 | def detect_prebuilt_pod(name, requirements) 5 | @explicit_prebuilt_pod_names ||= [] 6 | options = requirements.last || {} 7 | @explicit_prebuilt_pod_names << Specification.root_name(name) if options.is_a?(Hash) && options[:binary] 8 | options.delete(:binary) if options.is_a?(Hash) 9 | requirements.pop if options.empty? 10 | end 11 | 12 | # Returns the names of pod targets explicitly declared as prebuilt in Podfile using `:binary => true`. 13 | def explicit_prebuilt_pod_names 14 | names = @explicit_prebuilt_pod_names || [] 15 | names += parent.explicit_prebuilt_pod_names if !parent.nil? && parent.is_a?(TargetDefinition) 16 | names 17 | end 18 | 19 | # ---- patch method ---- 20 | # We want modify `store_pod` method, but it's hard to insert a line in the 21 | # implementation. So we patch a method called in `store_pod`. 22 | original_parse_inhibit_warnings = instance_method(:parse_inhibit_warnings) 23 | define_method(:parse_inhibit_warnings) do |name, requirements| 24 | detect_prebuilt_pod(name, requirements) 25 | original_parse_inhibit_warnings.bind(self).call(name, requirements) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/env/env_spec.rb: -------------------------------------------------------------------------------- 1 | describe "PodPrebuild::Env" do 2 | describe "stages" do 3 | let(:prebuild_job) { false } 4 | before do 5 | PodPrebuild::Env.reset! 6 | allow(PodPrebuild.config).to receive(:prebuild_job?).and_return(prebuild_job) 7 | end 8 | 9 | def expect_current_stage_as(stage) 10 | expect(PodPrebuild::Env.current_stage).to eq(stage) 11 | expect(PodPrebuild::Env.prebuild_stage?).to be(stage == :prebuild) 12 | expect(PodPrebuild::Env.integration_stage?).to be(stage == :integration) 13 | end 14 | 15 | context "in a prebuild job" do 16 | let(:prebuild_job) { true } 17 | 18 | it "has 2 stages: prebuild and integration" do 19 | expect(PodPrebuild::Env.stages).to eq([:prebuild, :integration]) 20 | end 21 | 22 | it "initially lands on prebuild stage" do 23 | expect_current_stage_as(:prebuild) 24 | end 25 | 26 | it "transits to integration stage on next" do 27 | PodPrebuild::Env.next_stage! 28 | expect_current_stage_as(:integration) 29 | end 30 | end 31 | 32 | context "in a non-prebuild job" do 33 | it "has only 1 stage: integration" do 34 | expect(PodPrebuild::Env.stages).to eq([:integration]) 35 | end 36 | 37 | it "initially lands on integration stage" do 38 | expect_current_stage_as(:integration) 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Report a bug 3 | about: If something is not working properly 4 | --- 5 | 6 | ### Checklist 7 | 8 | - [ ] Updated the plugin to the latest version 9 | - [ ] I've read the [Contribution Guidelines](https://github.com/grab/cocoapods-binary-cache/blob/master/CONTRIBUTING.md) 10 | - [ ] I've searched for [existing GitHub issues](https://github.com/grab/cocoapods-binary-cache/issues) 11 | 12 | ### Issue Description 13 | #### Command executed 14 | 15 |
[Details go here]
16 | 17 | #### What went wrong? 18 | 19 |
[Details go here]
20 | 21 | #### Stack trace 22 | 23 |
[Details go here]
24 | 25 | ### Environment 26 | #### Plugin version 27 | 28 |
[Details go here]
29 | 30 | #### Installed CocoaPods plugins 31 | 32 |
[Details go here]
33 | -------------------------------------------------------------------------------- /spec/helper/json_spec.rb: -------------------------------------------------------------------------------- 1 | describe "JSONFile" do 2 | let(:data) { { "a" => 1, "b" => 2 } } 3 | let(:file) { create_tempfile("sample.json", content: data.to_json) } 4 | before do 5 | @json = PodPrebuild::JSONFile.new(file.path) 6 | end 7 | after do 8 | file.unlink 9 | end 10 | 11 | describe "initialization" do 12 | context "file not exist" do 13 | let(:file) do 14 | f = create_tempfile("sample.json") 15 | FileUtils.rm_rf(f) 16 | f 17 | end 18 | it "represents empty data" do 19 | expect(@json.data).to be_empty 20 | end 21 | end 22 | 23 | context "file exist with non-empty content" do 24 | it "parses correct data" do 25 | expect(@json.data).to eq(data) 26 | end 27 | end 28 | end 29 | 30 | describe "data update" do 31 | let(:should_save) { true } 32 | before do 33 | @json["c"] = 3 34 | @json.save! if should_save 35 | @reloaded_json = PodPrebuild::JSONFile.new(file.path) 36 | end 37 | context "without saving" do 38 | let(:should_save) { false } 39 | it "does not serialize data to persistent" do 40 | expect(@reloaded_json.data).to eq(data) 41 | end 42 | end 43 | 44 | context "when saving" do 45 | it "serializes updated data to persistent" do 46 | expect(@reloaded_json.data).to eq(data.merge("c" => 3)) 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | You are more than welcome to contribute to this repo regardless of whether it is a change in the codebase or just a reported issue. 4 | 5 | ## Report an issue 6 | 7 | When reporting the issue, kindly describe: 8 | - The [cococoapods-binary-cache](https://github.com/grab/cocoapods-binary-cache/tags) gem version you are using 9 | - The prebuild configuration (if possible), including: 10 | - The `PodBinaryCacheConfig.json` contents 11 | - The setup code in Podfile: 12 | ```rb 13 | config_cocoapods_binary_cache( 14 | prebuild_config: "Debug", 15 | ... 16 | ) 17 | ``` 18 | - If possible, please help reproduce the issue with a demo project if the issue is related to some strange build issues. 19 | 20 | ## Contribute to the codebase 21 | 22 | Do note that the Github repo [cococoapods-binary-cache](https://github.com/grab/cocoapods-binary-cache) is a mirror of an internal repo. 23 | 24 | If you are a Grabber or you have access to the internal repo, kindly submit your change to this internal repo. 25 | 26 | In case you do not have access to this repo, please check out the following steps: 27 | 28 | 1. Submit your change to the Github repo [cococoapods-binary-cache](https://github.com/grab/cocoapods-binary-cache). 29 | 2. The change is reviewed by our team. 30 | 3. Once the change is approved, we'll proceed to merge your change. 31 | 32 | ... 33 | 34 | Look forward to your contribution! 😉 35 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/integration/patch/embed_framework_script.rb: -------------------------------------------------------------------------------- 1 | # A fix in embeded frameworks script. 2 | # 3 | # The framework file in pod target folder is a symblink. The EmbedFrameworksScript use `readlink` 4 | # to read the read path. As the symlink is a relative symlink, readlink cannot handle it well. So 5 | # we override the `readlink` to a fixed version. 6 | # 7 | module Pod 8 | module Generator 9 | class EmbedFrameworksScript 10 | old_method = instance_method(:script) 11 | define_method(:script) do 12 | script = old_method.bind(self).call 13 | patch = <<-SH.strip_heredoc 14 | #!/bin/sh 15 | # ---- this is added by cocoapods-binary --- 16 | # Readlink cannot handle relative symlink well, so we override it to a new one 17 | # If the path isn't an absolute path, we add a realtive prefix. 18 | old_read_link=`which readlink` 19 | readlink () { 20 | path=`$old_read_link "$1"`; 21 | if [ $(echo "$path" | cut -c 1-1) = '/' ]; then 22 | echo $path; 23 | else 24 | echo "`dirname $1`/$path"; 25 | fi 26 | } 27 | # --- 28 | SH 29 | 30 | # patch the rsync for copy dSYM symlink 31 | script = script.gsub "rsync --delete", "rsync --copy-links --delete" 32 | patch + script 33 | end 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/helper/target_checker.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Prebuild 3 | # Check the targets, for the current limitation of the plugin 4 | # 5 | # @param [Array] prebuilt_targets 6 | def self.check_one_pod_should_have_only_one_target(prebuilt_targets) 7 | targets_have_different_platforms = prebuilt_targets.reject { |t| t.pod_name == t.name } 8 | return unless targets_have_different_platforms.empty? 9 | 10 | names = targets_have_different_platforms.map(&:pod_name) 11 | raw_names = targets_have_different_platforms.map(&:name) 12 | message = "Oops, you came across a limitation of cocoapods-binary. 13 | 14 | The plugin requires that one pod should have ONLY ONE target in the 'Pod.xcodeproj'. There are mainly 2 situations \ 15 | causing this problem: 16 | 17 | 1. One pod integrates in 2 or more different platforms' targets. e.g. 18 | ``` 19 | target 'iphoneApp' do 20 | pod 'A', :binary => true 21 | end 22 | target 'watchApp' do 23 | pod 'A' 24 | end 25 | ``` 26 | 27 | 2. Use different subspecs in multiple targets. e.g. 28 | ``` 29 | target 'iphoneApp' do 30 | pod 'A/core' 31 | pod 'A/network' 32 | end 33 | target 'iphoneAppTest' do 34 | pod 'A/core' 35 | end 36 | ``` 37 | 38 | Related pods: #{names}, target names: #{raw_names}" 39 | raise Informative, message 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/prebuild_output/output.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | 4 | module PodPrebuild 5 | class Output 6 | def initialize(prebuild_sandbox) 7 | @sandbox = prebuild_sandbox 8 | end 9 | 10 | def prebuild_delta_path 11 | @prebuild_delta_path ||= PodPrebuild.config.prebuild_delta_path 12 | end 13 | 14 | def delta_dir 15 | @delta_dir ||= File.dirname(prebuild_delta_path) 16 | end 17 | 18 | def clean_delta_file 19 | Pod::UI.message "Clean delta file: #{prebuild_delta_path}" 20 | FileUtils.rm_rf(prebuild_delta_path) 21 | end 22 | 23 | def create_dir_if_needed(dir) 24 | FileUtils.mkdir_p dir unless File.directory?(dir) 25 | end 26 | 27 | def write_delta_file(options) 28 | updated = options[:updated] 29 | deleted = options[:deleted] 30 | 31 | if updated.empty? && deleted.empty? 32 | Pod::UI.puts "No changes in prebuild" 33 | return 34 | end 35 | 36 | Pod::UI.message "Write prebuild changes to: #{prebuild_delta_path}" 37 | create_dir_if_needed(delta_dir) 38 | changes = PodPrebuild::JSONFile.new(prebuild_delta_path) 39 | changes["updated"] = updated 40 | changes["deleted"] = deleted 41 | changes.save! 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/prebuild_output/metadata.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class Metadata < JSONFile 3 | def self.in_dir(dir) 4 | PodPrebuild::Metadata.new(dir + "metadata.json") 5 | end 6 | 7 | def resources 8 | @data["resources"] || [] 9 | end 10 | 11 | def resources=(value) 12 | @data["resources"] = value 13 | end 14 | 15 | def framework_name 16 | @data["framework_name"] 17 | end 18 | 19 | def framework_name=(value) 20 | @data["framework_name"] = value 21 | end 22 | 23 | def static_framework? 24 | @data["static_framework"] || false 25 | end 26 | 27 | def static_framework=(value) 28 | @data["static_framework"] = value 29 | end 30 | 31 | def resource_bundles 32 | @data["resource_bundles"] || [] 33 | end 34 | 35 | def resource_bundles=(value) 36 | @data["resource_bundles"] = value 37 | end 38 | 39 | def build_settings 40 | @data["build_settings"] || {} 41 | end 42 | 43 | def build_settings=(value) 44 | @data["build_settings"] = value 45 | end 46 | 47 | def source_hash 48 | @data["source_hash"] || {} 49 | end 50 | 51 | def source_hash=(value) 52 | @data["source_hash"] = value 53 | end 54 | 55 | def project_root 56 | @data["project_root"] 57 | end 58 | 59 | def project_root=(value) 60 | @data["project_root"] = value 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /docs/how_it_works.md: -------------------------------------------------------------------------------- 1 | # How cocoapods-binary-cache works 2 | 3 | ## Terminology 4 | - Cache-hit: a pod framework is prebuilt and has same version with the one in Pod lock file. 5 | - Cache-miss: a pod framework is not prebuilt or has different version with the on in Pod lock file. 6 | 7 | 8 | 9 | ## 1. Prebuild pod frameworks to binary and push to cache 10 | + With an added flag (`:binary => true`) to your pod in the Podfile, in the pod pre-install hook, It filters all pods which need to be built, then creates a separated Pod sandbox and generates a Pod.xcproject. We are using [cocoapods-binary](https://github.com/leavez/cocoapods-binary) for this process. 11 | + Then it builds frameworks in the generated project above using [cocoapods-rome](https://github.com/CocoaPods/Rome). The products are frameworks and a Manifest file. 12 | + Compresses all built frameworks to zips and commit to the cache repo. 13 | 14 | ## 2. Fetch and use cached frameworks 15 | + It fetches built frameworks from cache repo and unzip them. 16 | + In pod pre-install hook, it reads Manifest.lock and Podfile.lock to compare prebuilt lib's version with the one in Podfile.lock, if they're matched -> add to the cache-hit dictionary, otherwise, add to the cache-miss dictionary. Then the plugin intercepts pod install-source flow and base on generated cache hit/miss dictionaries to decide using cached frameworks or source code. 17 | 18 | Because we don't upgrade vendor pods every day, even once in a few months, the cache hit rate will likely be 100% most of the time. -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegration/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/BuildBenchMark.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | 4 | build_project() { 5 | echo "param: " $1 6 | rm -rf Pods 7 | rm -rf DerivedData 8 | 9 | start_fetch_time="$(date -u +%s)" 10 | if [ $1 = "cache_on" ]; then 11 | echo "fetch prebuilt binary cache" 12 | pod binary-cache --cmd=fetch 13 | fi 14 | end_fetch_time="$(date -u +%s)" 15 | fetch_cache_time="$(($end_fetch_time-$start_fetch_time))" 16 | echo 'fetch_cache_time: ' $fetch_cache_time 17 | 18 | echo "Install pods" 19 | start_install_time="$(date -u +%s)" 20 | bundle exec pod install 21 | end_install_time="$(date -u +%s)" 22 | pod_install_time="$(($end_install_time-$start_install_time))" 23 | echo 'pod_install_time:' $pod_install_time 24 | 25 | start_build_time="$(date -u +%s)" 26 | time xcodebuild -workspace PodBinCacheExample.xcworkspace -scheme PodBinCacheExample -configuration Debug -sdk iphonesimulator ARCHS=x86_64 ONLY_ACTIVE_ARCH=YES >/dev/null 2>&1 27 | end_build_time="$(date -u +%s)" 28 | xcodebuild_time="$(($end_build_time-$start_build_time))" 29 | echo 'xcodebuild_time:' $xcodebuild_time 30 | } 31 | 32 | export IS_POD_BINARY_CACHE_ENABLED='false' 33 | start_time="$(date -u +%s)" 34 | build_project "cache_off" 35 | end_time="$(date -u +%s)" 36 | buildtime_no_cache="$(($end_time-$start_time))" 37 | 38 | export IS_POD_BINARY_CACHE_ENABLED='true' 39 | 40 | start_time="$(date -u +%s)" 41 | build_project "cache_on" 42 | end_time="$(date -u +%s)" 43 | buildtime_with_cache="$(($end_time-$start_time))" 44 | 45 | echo '-------------------' 46 | echo "Build time no cache: $buildtime_no_cache \nBuild time with cache: $buildtime_with_cache" -------------------------------------------------------------------------------- /spec/cahe_validator/validator_with_dev_pods_spec.rb: -------------------------------------------------------------------------------- 1 | describe "PodPrebuild::DevPodsCacheValidator" do 2 | describe "#validate" do 3 | let(:pods) do 4 | { 5 | "A" => { :version => "0.0.5", :path => "local/A" }, 6 | "B" => { :version => "0.0.5", :path => "local/B" }, 7 | "C" => { :version => "0.0.5", :path => "local/C" }, 8 | "X" => { :version => "0.0.5" } 9 | } 10 | end 11 | let(:generated_framework_path) { Pathname("GeneratedFrameworks") } 12 | let(:pod_lockfile) { gen_lockfile(pods: pods) } 13 | let(:prebuilt_lockfile) { gen_lockfile(pods: pods) } 14 | let(:source_hash) { "abc1234" } 15 | let(:metadata_hash) { { "source_hash" => source_hash } } 16 | 17 | before do 18 | allow_any_instance_of(PodPrebuild::Metadata).to receive(:load_json).and_return(metadata_hash) 19 | allow(FolderChecksum).to receive(:git_checksum).with(anything).and_return(source_hash) 20 | allow(FolderChecksum).to receive(:git_checksum).with("local/A").and_return("not" + source_hash) 21 | 22 | validation_result = PodPrebuild::DevPodsCacheValidator.new( 23 | pod_lockfile: pod_lockfile, 24 | prebuilt_lockfile: prebuilt_lockfile, 25 | generated_framework_path: generated_framework_path 26 | ).validate 27 | @missed = validation_result.missed 28 | @hit = validation_result.hit 29 | end 30 | 31 | it "detects pods with changed checksum as missed" do 32 | expect(@missed).to eq(Set["A"]) 33 | end 34 | 35 | it "detects pods with unchanged checksum as hit" do 36 | expect(@hit).to eq(Set["B", "C"]) 37 | end 38 | 39 | it "does not check non-dev pods" do 40 | expect(@missed).not_to include("X") 41 | expect(@hit).not_to include("X") 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegration/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /lib/command/prebuild.rb: -------------------------------------------------------------------------------- 1 | require_relative "executor/prebuilder" 2 | require_relative "../cocoapods-binary-cache/pod-binary/prebuild_dsl" 3 | 4 | module Pod 5 | class Command 6 | class Binary < Command 7 | class Prebuild < Binary 8 | attr_reader :prebuilder 9 | 10 | self.arguments = [CLAide::Argument.new("CACHE-BRANCH", false)] 11 | def self.options 12 | [ 13 | ["--config", "Config (Debug, Test...) to prebuild"], 14 | ["--repo-update", "Update pod repo before installing"], 15 | ["--no-fetch", "Do not perform a cache fetch beforehand"], 16 | ["--push", "Push cache to repo upon completion"], 17 | ["--all", "Prebuild all binary pods regardless of cache validation"], 18 | ["--targets", "Targets to prebuild. Use comma (,) to specify a list of targets"] 19 | ].concat(super) 20 | end 21 | 22 | def initialize(argv) 23 | super 24 | prebuild_all_pods = argv.flag?("all") 25 | prebuild_targets = argv.option("targets", "").split(",") 26 | update_cli_config( 27 | :prebuild_job => true, 28 | :prebuild_all_pods => prebuild_all_pods, 29 | :prebuild_config => argv.option("config") 30 | ) 31 | update_cli_config(:prebuild_targets => prebuild_targets) unless prebuild_all_pods 32 | @prebuilder = PodPrebuild::CachePrebuilder.new( 33 | config: prebuild_config, 34 | cache_branch: argv.shift_argument || "master", 35 | repo_update: argv.flag?("repo-update"), 36 | no_fetch: argv.flag?("fetch") == false, 37 | push_cache: argv.flag?("push") 38 | ) 39 | end 40 | 41 | def run 42 | @prebuilder.run 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /scripts/integration_test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | export WORKING_DIR=$(PWD) 5 | export INTEGRATION_TESTS_DIR="${WORKING_DIR}/integration_tests" 6 | export TEST_DEVICE=${INTEGRATION_TEST_DEVICE_NAME:-iPhone 8} 7 | export DERIVED_DATA_PATH=${DERIVED_DATA_PATH:-DerivedData} 8 | 9 | log_section() { 10 | echo "-------------------------------------------" 11 | echo "$1" 12 | echo "-------------------------------------------" 13 | } 14 | 15 | pod_install() { 16 | bundle exec pod install --ansi || bundle exec pod install --ansi --repo-update 17 | } 18 | 19 | pod_bin_fetch() { 20 | bundle exec pod binary fetch --ansi 21 | } 22 | 23 | pod_bin_prebuild() { 24 | bundle exec pod binary prebuild --ansi $1 25 | } 26 | 27 | xcodebuild_test() { 28 | log_section "Running xcodebuild test..." 29 | 30 | set -o pipefail && env NSUnbufferedIO=YES xcodebuild \ 31 | -workspace PrebuiltPodIntegration.xcworkspace \ 32 | -scheme PrebuiltPodIntegration \ 33 | -configuration Debug \ 34 | -sdk "iphonesimulator" \ 35 | -destination "platform=iOS Simulator,name=${TEST_DEVICE}" \ 36 | -derivedDataPath "${DERIVED_DATA_PATH}" \ 37 | clean \ 38 | test | bundle exec xcpretty --color 39 | } 40 | 41 | # ------------------------- 42 | 43 | echo "Working dir: ${WORKING_DIR}" 44 | echo "Integeration tests dir: ${INTEGRATION_TESTS_DIR}" 45 | 46 | TEST_MODE="${1:-prebuild-all}" 47 | export USE_XCFRAMEWORK="${2:-false}" 48 | 49 | cd "${INTEGRATION_TESTS_DIR}" 50 | echo "Running test with mode: ${TEST_MODE}..." 51 | 52 | rm -rf Pods _Prebuild DerivedData 53 | case ${TEST_MODE} in 54 | non-prebuild ) 55 | pod_bin_fetch 56 | pod_install 57 | xcodebuild_test 58 | ;; 59 | prebuild-changes ) 60 | pod_bin_prebuild 61 | xcodebuild_test 62 | ;; 63 | prebuild-all ) 64 | pod_bin_prebuild --all 65 | xcodebuild_test 66 | ;; 67 | * ) break ;; 68 | esac 69 | -------------------------------------------------------------------------------- /spec/spec_helper/lockfile.rb: -------------------------------------------------------------------------------- 1 | def gen_lockfile(options = {}) 2 | hash = {} 3 | pods_options = options[:pods].map do |name, pod| 4 | pod_ = pod.clone 5 | pod_[:name] = name 6 | pod_[:version] ||= "0.0.1" 7 | [name, pod_] 8 | end.to_h 9 | 10 | pods = pods_options.values 11 | hash["PODS"] = pods.map { |pod| gen_pod_item(pod) } 12 | hash["DEPENDENCIES"] = pods.map { |pod| gen_dependencies_item(pod) } 13 | hash["EXTERNAL SOURCES"] = \ 14 | pods_options[:external_sources] || \ 15 | pods_options \ 16 | .select { |_, pod| pod.key?(:path) || pod.key?(:git) } 17 | .map { |name, pod| [name, gen_external_sources_item(pod)] }.to_h 18 | hash["SPEC CHECKSUMS"] = pods_options[:spec_checksums] || {} 19 | hash["COCOAPODS"] = pods_options[:cocoapods] || "1.7.5" 20 | Pod::Lockfile.new(hash) 21 | end 22 | 23 | private 24 | 25 | def gen_pod_item(pod) 26 | name_with_version = "#{pod[:name]} (#{pod[:version]})" 27 | pod[:dependencies].nil? ? name_with_version : { pod[:name_with_version] => pod[:dependencies] } 28 | end 29 | 30 | def gen_dependencies_item(pod) 31 | source = begin 32 | return "from #{pod[:path]}" unless pod[:path].nil? 33 | 34 | unless pod[:git].nil? 35 | return "from #{pod[:git]}, tag: #{pod[:tag]}" unless pod[:tag].nil? 36 | return "from #{pod[:git]}, branch: #{pod[:branch]}" unless pod[:branch].nil? 37 | return "from #{pod[:git]}, commit: #{pod[:commit]}" unless pod[:commit].nil? 38 | end 39 | "= #{pod[:version]}" 40 | end 41 | "#{pod[:name]} (#{source})" 42 | end 43 | 44 | def gen_external_sources_item(pod) 45 | return { :path => pod[:path] } unless pod[:path].nil? 46 | return { :git => pod[:git], :tag => pod[:tag] } unless pod[:tag].nil? 47 | return { :git => pod[:git], :tag => pod[:branch] } unless pod[:branch].nil? 48 | return { :git => pod[:git], :commit => pod[:commit] } unless pod[:commit].nil? 49 | 50 | { :git => pod[:git] } 51 | end 52 | -------------------------------------------------------------------------------- /lib/command/executor/prebuilder.rb: -------------------------------------------------------------------------------- 1 | require_relative "base" 2 | require_relative "fetcher" 3 | require_relative "pusher" 4 | 5 | module PodPrebuild 6 | class CachePrebuilder < CommandExecutor 7 | attr_reader :repo_update, :fetcher, :pusher 8 | 9 | def initialize(options) 10 | super(options) 11 | @repo_update = options[:repo_update] 12 | @fetcher = PodPrebuild::CacheFetcher.new(options) unless options[:no_fetch] 13 | @pusher = PodPrebuild::CachePusher.new(options) if options[:push_cache] 14 | end 15 | 16 | def run 17 | @fetcher&.run 18 | prebuild 19 | changes = PodPrebuild::JSONFile.new(@config.prebuild_delta_path) 20 | return if changes.empty? 21 | 22 | sync_cache(changes) 23 | @pusher&.run 24 | end 25 | 26 | private 27 | 28 | def prebuild 29 | Pod::UI.step("Installation") do 30 | installer.repo_update = @repo_update 31 | installer.install! 32 | end 33 | end 34 | 35 | def sync_cache(changes) 36 | Pod::UI.step("Syncing cache") do 37 | FileUtils.cp(@config.manifest_path, @config.manifest_path(in_cache: true)) 38 | clean_cache(changes["deleted"]) 39 | zip_to_cache(changes["updated"]) 40 | end 41 | end 42 | 43 | def zip_to_cache(pods_to_update) 44 | FileUtils.mkdir_p(@config.generated_frameworks_dir(in_cache: true)) 45 | pods_to_update.each do |pod| 46 | Pod::UI.puts "- Update cache: #{pod}" 47 | ZipUtils.zip( 48 | "#{@config.generated_frameworks_dir}/#{pod}", 49 | to_dir: @config.generated_frameworks_dir(in_cache: true) 50 | ) 51 | end 52 | end 53 | 54 | def clean_cache(pods_to_delete) 55 | pods_to_delete.each do |pod| 56 | Pod::UI.puts "- Clean up cache: #{pod}" 57 | FileUtils.rm_rf("#{@config.generated_frameworks_dir(in_cache: true)}/#{pod}.zip") 58 | end 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /spec/helper/lockfile_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Lockfile" do 2 | describe "data extraction" do 3 | let(:dev_pods) do 4 | { 5 | "A_dev" => { :path => "local" }, 6 | "B_dev" => { :path => "local" } 7 | } 8 | end 9 | let(:external_remote_pods) do 10 | { 11 | "C_remote" => { :git => "remote_url", :tag => "0.0.1" }, 12 | "D_remote" => { :git => "remote_url", :commit => "abc1234" } 13 | } 14 | end 15 | let(:non_dev_pods) { external_remote_pods.merge("E" => { :version => "0.0.1" }) } 16 | let(:external_pods) { dev_pods.merge(external_remote_pods) } 17 | let(:pods) { dev_pods.merge(non_dev_pods) } 18 | let(:internal_lockfile) { gen_lockfile(pods: pods) } 19 | before do 20 | @lockfile = PodPrebuild::Lockfile.new(internal_lockfile) 21 | end 22 | 23 | it "extracts data correctly" do 24 | expect(@lockfile.pods.keys).to eq(pods.keys) 25 | expect(@lockfile.external_sources).to eq(external_pods) 26 | expect(@lockfile.dev_pods.keys).to eq(dev_pods.keys) 27 | expect(@lockfile.send(:dev_pod_hashes_map).keys).to eq(dev_pods.keys) 28 | expect(@lockfile.non_dev_pods.keys).to eq(non_dev_pods.keys) 29 | end 30 | 31 | context "has subspec pods" do 32 | let(:external_remote_pods) do 33 | { 34 | "C_remote" => { :git => "remote_url", :tag => "0.0.1" }, 35 | "S/A" => { :git => "remote_url", :tag => "0.0.1" }, 36 | "S/B" => { :git => "remote_url", :tag => "0.0.1" }, 37 | "T/C" => { :git => "remote_url", :tag => "0.0.1" }, 38 | "DevA" => { :path => "local" }, 39 | "DevA/Sub" => { :path => "local" } 40 | } 41 | end 42 | it "extracts subspec pods correctly" do 43 | subspec_pods = { 44 | "S" => ["S/A", "S/B"], 45 | "T" => ["T/C"] 46 | } 47 | expect(@lockfile.subspec_vendor_pods).to eq(subspec_pods) 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegration/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "size" : "1024x1024", 91 | "scale" : "1x" 92 | } 93 | ], 94 | "info" : { 95 | "version" : 1, 96 | "author" : "xcode" 97 | } 98 | } -------------------------------------------------------------------------------- /integration_tests/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, "12.0" 2 | use_frameworks! 3 | 4 | source "https://cdn.cocoapods.org/" 5 | 6 | def binary_pod(name, *args, **kwargs) 7 | kwargs_cloned = kwargs.clone 8 | kwargs_cloned[:binary] = true if kwargs_cloned[:binary].nil? 9 | pod name, *args, **kwargs_cloned 10 | end 11 | 12 | plugin "cocoapods-binary-cache" 13 | config_cocoapods_binary_cache( 14 | cache_repo: { 15 | "default" => { 16 | "local" => ENV["PREBUILT_CACHE_LOCAL_DIR"] || "prebuilt_cache/default" # use local cache for integration tests 17 | } 18 | }, 19 | excluded_pods: [], 20 | dev_pods_enabled: true, 21 | save_cache_validation_to: ".stats/cocoapods_binary_cache.json", 22 | strict_diagnosis: true, # Fail if any abnormal integration is spotted 23 | xcodebuild_log_path: ".logs/xcodebuild", 24 | xcframework: ENV["USE_XCFRAMEWORK"] == "true" 25 | ) 26 | 27 | target "PrebuiltPodIntegration" do 28 | # Has `*.bundle` outside the framework 29 | binary_pod "SwiftDate", "6.1.0" 30 | binary_pod "BKMoneyKit", "0.0.12" 31 | binary_pod "IQKeyboardManagerSwift", "6.1.1" 32 | binary_pod "GoogleSignIn", "4.2.0" 33 | 34 | # Has `*.bundle` inside the framework 35 | binary_pod "GoogleMaps", "2.7.0" 36 | 37 | binary_pod "UICommonsStatic", :path => "LocalPods/UICommonsStatic" 38 | binary_pod "UICommonsDynamic", :path => "LocalPods/UICommonsDynamic" 39 | 40 | target "PrebuiltPodIntegrationTests" do 41 | inherit! :search_paths 42 | end 43 | end 44 | 45 | pre_install do |installer| 46 | must_be_dynamic_frameworks = [] 47 | installer.pod_targets.each do |pod| 48 | linkage = must_be_dynamic_frameworks.include?(pod.name) ? :dynamic : :static 49 | pod.instance_variable_set(:@build_type, Pod::BuildType.new(:linkage => linkage, :packaging => :framework)) 50 | end 51 | end 52 | 53 | post_install do |installer| 54 | installer.pods_project.targets.each do |target| 55 | target.build_configurations.each do |config| 56 | config.build_settings["SWIFT_SUPPRESS_WARNINGS"] = "YES" 57 | config.build_settings["GCC_WARN_INHIBIT_ALL_WARNINGS"] = "YES" 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/prebuild/prebuild_installer_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Pod::PrebuildInstaller" do 2 | let(:tmp_dir) { create_tempdir } 3 | let(:sandbox) { Pod::Sandbox.new(tmp_dir) } 4 | let(:podfile) { Pod::Podfile.new } 5 | let(:lockfile) { Pod::Lockfile.new({}) } 6 | let(:cache_validation) { PodPrebuild::CacheValidationResult.new } 7 | 8 | before do 9 | @installer = Pod::PrebuildInstaller.new( 10 | sandbox: sandbox, 11 | podfile: podfile, 12 | lockfile: lockfile, 13 | cache_validation: cache_validation 14 | ) 15 | end 16 | 17 | after do 18 | FileUtils.remove_entry tmp_dir 19 | end 20 | 21 | describe "#initialize" do 22 | it "sets lockfile_wrapper correctly" do 23 | expect(@installer.lockfile_wrapper).to be_a(PodPrebuild::Lockfile) 24 | expect(@installer.lockfile_wrapper.lockfile).to eq(lockfile) 25 | end 26 | 27 | context "lockfile is missing" do 28 | let(:lockfile) { nil } 29 | it "sets lockfile_wrapper as nil" do 30 | expect(@installer.lockfile_wrapper).to eq(nil) 31 | end 32 | end 33 | end 34 | 35 | describe "#prebuild_frameworks!" do 36 | let(:prebuild_code_gen) { ->(_, _) {} } 37 | let(:targets_to_prebuild) { [] } 38 | 39 | before do 40 | allow(@installer).to receive(:pod_targets).and_return([]) 41 | allow(@installer).to receive(:targets_to_prebuild).and_return(targets_to_prebuild) 42 | # TODO (thuyen): Structure method `build` in pod-binary/prebuild with smaller methods 43 | # so that it's easier to stub/mock. Then update `targets_to_prebuild` to non-empty 44 | allow(sandbox).to receive(:exsited_framework_target_names).and_return([]) 45 | allow(sandbox).to receive(:generate_framework_path).and_return(tmp_dir + "/Generated") 46 | allow(PodPrebuild.config).to receive(:prebuild_code_gen).and_return(prebuild_code_gen) 47 | allow(Pod::Prebuild).to receive(:build) 48 | end 49 | 50 | it "runs code generation before building" do 51 | expect(prebuild_code_gen).to receive(:call).with(@installer, targets_to_prebuild) 52 | @installer.prebuild_frameworks! 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '10.0' 2 | 3 | def is_pod_binary_cache_enabled 4 | ENV['IS_POD_BINARY_CACHE_ENABLED'] == 'true' 5 | end 6 | 7 | if is_pod_binary_cache_enabled 8 | plugin 'cocoapods-binary-cache' 9 | config_cocoapods_binary_cache( 10 | cache_repo: { 11 | "default" => { 12 | "remote" => "https://github.com/bigbangvn/demo-pod-binary-cache-prebuilt-libs.git", 13 | "local" => "~/.cocoapods-binary-cache/PodBinaryCacheExample-libs" 14 | } 15 | }, 16 | prebuild_config: "Debug", 17 | dev_pods_enabled: true 18 | ) 19 | # set_unbuilt_dev_pods(['ConfigSDK']) // To test ABI breaking (crash, call wrong function) when there're code change in ConfigSDK and don't rebuild it's clients (ConfigService) 20 | end 21 | 22 | def binary_pod(name, *args) 23 | if is_pod_binary_cache_enabled 24 | pod name, args, :binary => true 25 | else 26 | pod name, args 27 | end 28 | end 29 | 30 | target 'PodBinCacheExample' do 31 | use_frameworks! 32 | 33 | # Pods for PodBinCacheExample 34 | # https://www.raywenderlich.com/259-top-10-libraries-for-ios-developers 35 | 36 | binary_pod 'AFNetworking', '3.2.1' 37 | binary_pod 'SDWebImage' 38 | binary_pod 'Alamofire' 39 | binary_pod 'MBProgressHUD' 40 | binary_pod 'Masonry' 41 | binary_pod 'SwiftyJSON' 42 | binary_pod 'SVProgressHUD' 43 | binary_pod 'MJRefresh' 44 | binary_pod 'CocoaLumberjack' 45 | binary_pod 'Realm' 46 | binary_pod 'SnapKit' 47 | binary_pod 'Kingfisher' 48 | 49 | # Example of Vendor pod with external source from git 50 | pod 'RxSwift', :git => 'https://github.com/ReactiveX/RxSwift.git', :commit => 'a580d07ed002217fd91d8446c3a852486e9beefa', :binary => true 51 | 52 | # Development/Local Pods 53 | pod 'ConfigService', :path => 'DevPods/ConfigService', :binary => true 54 | pod 'ConfigSDK', :path => 'DevPods/ConfigSDK', :binary => true 55 | end 56 | 57 | post_install do |installer| 58 | installer.pods_project.targets.each do |target| 59 | target.build_configurations.each do |config| 60 | config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' # Only work from Xcode 11 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/validation_result.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class CacheValidationResult 3 | attr_reader :hit, :missed_with_reasons 4 | 5 | def initialize(missed_with_reasons = {}, hit = Set.new) 6 | @missed_with_reasons = missed_with_reasons 7 | @hit = hit.to_set - missed_with_reasons.keys 8 | end 9 | 10 | def all 11 | (hit + missed).to_set 12 | end 13 | 14 | def missed 15 | @missed_with_reasons.keys.to_set 16 | end 17 | 18 | def missed?(name) 19 | @missed_with_reasons.key?(name) 20 | end 21 | 22 | def hit?(name) 23 | @hit.include?(name) 24 | end 25 | 26 | def include?(name) 27 | missed?(name) || hit?(name) 28 | end 29 | 30 | def merge(other) 31 | PodPrebuild::CacheValidationResult.new( 32 | @missed_with_reasons.merge(other.missed_with_reasons), 33 | @hit + other.hit 34 | ) 35 | end 36 | 37 | def update_to(path) 38 | FileUtils.mkdir_p(File.dirname(path)) 39 | json_file = PodPrebuild::JSONFile.new(path) 40 | json_file["cache_missed"] = missed.to_a 41 | json_file["cache_hit"] = hit.to_a 42 | json_file.save! 43 | end 44 | 45 | def keep(names) 46 | base_names = names.map { |name| name.split("/")[0] }.to_set 47 | select { |name| base_names.include?(name.split("/")[0]) } 48 | end 49 | 50 | def discard(names) 51 | base_names = names.map { |name| name.split("/")[0] }.to_set 52 | reject { |name| base_names.include?(name.split("/")[0]) } 53 | end 54 | 55 | def select(&predicate) 56 | PodPrebuild::CacheValidationResult.new( 57 | @missed_with_reasons.select { |name, _| predicate.call(name) }, 58 | @hit.select(&predicate) 59 | ) 60 | end 61 | 62 | def reject(&predicate) 63 | select { |name| !predicate.call(name) } 64 | end 65 | 66 | def print_summary 67 | Pod::UI.puts "Cache validation: hit (#{@hit.count}) #{@hit.to_a}" 68 | @missed_with_reasons.each do |name, reason| 69 | Pod::UI.puts "Cache validation: missed #{name}. Reason: #{reason}".yellow 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Include: 3 | - 'lib/**/*Gemfile' 4 | - 'lib/**/*.rb' 5 | - 'spec/**/*' 6 | Exclude: 7 | - 'integration_tests/**/*' 8 | - 'PodBinaryCacheExample/**/*' 9 | 10 | Lint/RaiseException: 11 | Enabled: false 12 | 13 | Lint/StructNewOverride: 14 | Enabled: false 15 | 16 | Lint/DeprecatedOpenSSLConstant: 17 | Enabled: true 18 | 19 | Layout/EmptyLinesAroundAttributeAccessor: 20 | Enabled: true 21 | 22 | Layout/SpaceAroundMethodCallOperator: 23 | Enabled: false 24 | 25 | Layout/LineLength: 26 | Max: 120 27 | 28 | Layout/IndentationWidth: 29 | Enabled: false 30 | 31 | Layout/SpaceInsideHashLiteralBraces: 32 | Enabled: false 33 | 34 | Layout/MultilineMethodCallIndentation: 35 | EnforcedStyle: indented 36 | 37 | Style/ExponentialNotation: 38 | Enabled: false 39 | 40 | Style/HashEachMethods: 41 | Enabled: false 42 | 43 | Style/HashTransformKeys: 44 | Enabled: false 45 | 46 | Style/HashTransformValues: 47 | Enabled: false 48 | 49 | Style/FrozenStringLiteralComment: 50 | Enabled: false 51 | 52 | # Not yet enforce documentation 53 | Style/Documentation: 54 | Enabled: false 55 | 56 | Style/CommentAnnotation: 57 | Enabled: false 58 | 59 | Style/EmptyMethod: 60 | EnforcedStyle: expanded 61 | 62 | Style/HashSyntax: 63 | EnforcedStyle: no_mixed_keys 64 | UseHashRocketsWithSymbolValues: true 65 | PreferHashRocketsForNonAlnumEndingSymbols: false 66 | 67 | Style/RescueStandardError: 68 | Enabled: false 69 | 70 | Style/StringLiterals: 71 | EnforcedStyle: double_quotes 72 | 73 | Style/SlicingWithRange: 74 | Enabled: true 75 | 76 | Style/WordArray: 77 | EnforcedStyle: brackets 78 | 79 | Style/SymbolArray: 80 | EnforcedStyle: brackets 81 | 82 | Metrics/MethodLength: 83 | Enabled: false 84 | 85 | Metrics/PerceivedComplexity: 86 | Enabled: false 87 | 88 | Metrics/AbcSize: 89 | Enabled: false 90 | 91 | Metrics/CyclomaticComplexity: 92 | Enabled: false 93 | 94 | Metrics/BlockLength: 95 | Enabled: false 96 | 97 | Naming/FileName: 98 | Exclude: 99 | - Gemfile 100 | - Dangerfile 101 | - lib/cocoapods-binary-cache.rb 102 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/DevPods/ConfigSDK/Classes/ConfigVar.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2020 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 3 | Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 4 | */ 5 | 6 | /* This class is mainly to demonstrate ABI breaking. 7 | + Host: ConfigSDK this framework (can be static or dynamic linking) 8 | + Client: ConfigService that uses ConfigSDK (can be static or dynamic linking) 9 | If we prebuild ConfigService, then change ConfigSDK private stuffs (add variable, change variable from let to var, 10 | add new function) => the binary interface of ConfigSDK change => ConfigService stops working: crash, call worng funcs 11 | */ 12 | 13 | import Foundation 14 | 15 | public protocol ConfigVarDelegate: AnyObject { 16 | } 17 | 18 | public class ConfigVar { 19 | 20 | // ⚠️ Try to change let -> var after prebuild can cause crash. when client call reload(). Because the memory layout is different. 21 | private let delegate: ConfigVarDelegate? = nil 22 | 23 | public init() { 24 | print("\(#function)") 25 | } 26 | 27 | // After prebuild, run again to see all functions are called as normal. 28 | // Then insert a new function here -> clean DerivedData -> run again to see magic happen 29 | 30 | // ⚠️ This function demonstrate ABI breaking when we add new function, variable on top of it. 31 | // + If we add new function -> client module (without recompiling) will wrongly call to new function 32 | // + If we change "let delegate" -> "var delegate" => cause crash when call this function 33 | // Note: remember to clean: rm -rf DerivedData after change, because Xcode will check that ExperimentService has no change (prebuilt) -> It don't rebuild ScribeSDK 34 | public func reload() { 35 | print("\(#function)") 36 | } 37 | 38 | // ⚠️ Has same issue with func above even with "dynamic" keyword (Xcode 10) 39 | public func reload2() { 40 | print("\(#function)") 41 | } 42 | 43 | // ✅ This function can be called correctly even when this class changes (add new ivar, func), and this Module's clients 44 | // don't need to recompile because it's lookup via objc-runtime 45 | @objc public dynamic func saveToPersistent() { 46 | print("\(#function)") 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/integration/patch/source_installation.rb: -------------------------------------------------------------------------------- 1 | require_relative "../source_installer" 2 | 3 | module Pod 4 | class Installer 5 | # Override the download step to skip download and prepare file in target folder 6 | alias original_create_pod_installer create_pod_installer 7 | def create_pod_installer(name) 8 | if should_integrate_prebuilt_pod?(name) 9 | create_prebuilt_source_installer(name) 10 | else 11 | create_normal_source_installer(name) 12 | end 13 | end 14 | 15 | private 16 | 17 | def create_normal_source_installer(name) 18 | original_create_pod_installer(name) 19 | end 20 | 21 | def original_specs_by_platform(name) 22 | specs_for_pod(name).map do |platform, specs| 23 | specs_ = specs.map { |spec| @original_specs[spec.name] } 24 | [platform, specs_] 25 | end.to_h 26 | end 27 | 28 | def create_prebuilt_source_installer(name) 29 | # A source installer needs to install with the original spec (instead of the altered spec). 30 | # Otherwise, the cache will be corrupted because CocoaPods packs necessary dirs/files from temp dir 31 | # to the cache dir based on the spec. 32 | source_installer = PodSourceInstaller.new(sandbox, podfile, original_specs_by_platform(name)) 33 | pod_installer = PrebuiltSourceInstaller.new( 34 | sandbox, 35 | podfile, 36 | specs_for_pod(name), 37 | source_installer: source_installer 38 | ) 39 | pod_installers << pod_installer 40 | pod_installer 41 | end 42 | 43 | def should_integrate_prebuilt_pod?(name) 44 | if PodPrebuild.config.prebuild_job? && PodPrebuild.config.targets_to_prebuild_from_cli.empty? 45 | # In a prebuild job, at the integration stage, all prebuilt frameworks should be 46 | # ready for integration regardless of whether there was any cache miss or not. 47 | # Those that are missed were prebuilt in the prebuild stage. 48 | PodPrebuild.state.cache_validation.include?(name) 49 | else 50 | prebuilt = PodPrebuild.state.cache_validation.hit + PodPrebuild.config.targets_to_prebuild_from_cli 51 | prebuilt.include?(name) 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /spec/cahe_validator/validation_result_spec.rb: -------------------------------------------------------------------------------- 1 | describe "PodPrebuild::CacheValidationResult" do 2 | describe "merge behavior" do 3 | let(:one) { [{ "A" => "missing" }, Set["X"]] } 4 | let(:another) { [{ "C" => "outdated" }, Set["Y"]] } 5 | before do 6 | @merged = PodPrebuild::CacheValidationResult.new(*one).merge( 7 | PodPrebuild::CacheValidationResult.new(*another) 8 | ) 9 | end 10 | 11 | it "returns correct result" do 12 | expect(@merged.missed).to eq(Set["A", "C"]) 13 | expect(@merged.hit).to eq(Set["X", "Y"]) 14 | end 15 | 16 | context "a pod appears in both hit and missed" do 17 | let(:another) { [{ "X" => "outdated" }, Set["Y"]] } 18 | it "treats that pod as missed" do 19 | expect(@merged.missed).to eq(Set["A", "X"]) 20 | expect(@merged.hit).to eq(Set["Y"]) 21 | end 22 | end 23 | end 24 | 25 | describe "exclude_pods behavior" do 26 | let(:data) do 27 | cache_miss = { "A" => "missing", "B" => "missing", "B/Sub" => "missing", "B/AnotherSub" => "missing" } 28 | cache_hit = Set["X", "Y", "Y/Sub", "Y/AnotherSub"] 29 | [cache_miss, cache_hit] 30 | end 31 | let(:excluded_pods) { Set.new } 32 | before do 33 | @excluded = PodPrebuild::CacheValidationResult.new(*data).discard(excluded_pods) 34 | end 35 | 36 | context "excludes pods without subspec" do 37 | let(:excluded_pods) { Set["A", "X"] } 38 | 39 | it "excludes matched pods" do 40 | expect(@excluded.missed).to eq(Set["B", "B/Sub", "B/AnotherSub"]) 41 | expect(@excluded.hit).to eq(Set["Y", "Y/Sub", "Y/AnotherSub"]) 42 | end 43 | end 44 | 45 | context "excludes pod that has subspec" do 46 | let(:excluded_pods) { Set["B", "Y"] } 47 | 48 | it "excludes all subspecs and the parent pod" do 49 | expect(@excluded.missed).to eq(Set["A"]) 50 | expect(@excluded.hit).to eq(Set["X"]) 51 | end 52 | end 53 | 54 | context "excludes a subspec" do 55 | let(:excluded_pods) { Set["B/Sub", "Y/Sub"] } 56 | 57 | it "excludes all subspecs and the parent pod" do 58 | expect(@excluded.missed).to eq(Set["A"]) 59 | expect(@excluded.hit).to eq(Set["X"]) 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-rome/xcodebuild_raw.rb: -------------------------------------------------------------------------------- 1 | require "fourflusher" 2 | 3 | module PodPrebuild 4 | class XcodebuildCommand 5 | PLATFORM_OF_SDK = { 6 | "iphonesimulator" => "iOS", 7 | "appletvsimulator" => "tvOS", 8 | "watchsimulator" => "watchOS" 9 | }.freeze 10 | 11 | DESTINATION_OF_SDK = { 12 | "iphoneos" => "\"generic/platform=iOS\"", 13 | "iphonesimulator" => "\"generic/platform=iOS Simulator\"" 14 | }.freeze 15 | 16 | def self.xcodebuild(options) 17 | sdk = options[:sdk] || "iphonesimulator" 18 | targets = options[:targets] || [options[:target]] 19 | platform = PLATFORM_OF_SDK[sdk] 20 | 21 | cmd = ["xcodebuild"] 22 | cmd << "-project" << options[:sandbox].project_path.realdirpath.shellescape 23 | targets.each { |target| cmd << "-target" << target } 24 | cmd << "-configuration" << options[:configuration].shellescape 25 | cmd << "-sdk" << sdk 26 | if DESTINATION_OF_SDK.key?(sdk) 27 | cmd << "-destination" << DESTINATION_OF_SDK[sdk] 28 | else 29 | unless platform.nil? 30 | cmd << Fourflusher::SimControl.new.destination(:oldest, platform, options[:deployment_target]) 31 | end 32 | end 33 | cmd += options[:args] if options[:args] 34 | cmd << "build" 35 | 36 | if options[:log_path].nil? 37 | cmd << "2>&1" 38 | else 39 | FileUtils.mkdir_p(File.dirname(options[:log_path])) 40 | cmd << "> #{options[:log_path].shellescape}" 41 | end 42 | cmd = cmd.join(" ") 43 | 44 | Pod::UI.puts_indented "$ #{cmd}" unless PodPrebuild.config.silent_build? 45 | 46 | log = `#{cmd}` 47 | return if $?.exitstatus.zero? # rubocop:disable Style/SpecialGlobalVars 48 | 49 | begin 50 | require "xcpretty" # TODO (thuyen): Revise this dependency 51 | # use xcpretty to print build log 52 | # 64 represent command invalid. http://www.manpagez.com/man/3/sysexits/ 53 | printer = XCPretty::Printer.new({:formatter => XCPretty::Simple, :colorize => "auto"}) 54 | log.each_line do |line| 55 | printer.pretty_print(line) 56 | end 57 | rescue 58 | Pod::UI.puts log.red 59 | ensure 60 | raise "Fail to build targets: #{targets}" 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 3 | Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 4 | */ 5 | 6 | import UIKit 7 | 8 | @UIApplicationMain 9 | class AppDelegate: UIResponder, UIApplicationDelegate { 10 | 11 | var window: UIWindow? 12 | 13 | 14 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 15 | // Override point for customization after application launch. 16 | return true 17 | } 18 | 19 | func applicationWillResignActive(_ application: UIApplication) { 20 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 21 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 22 | } 23 | 24 | func applicationDidEnterBackground(_ application: UIApplication) { 25 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 26 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 27 | } 28 | 29 | func applicationWillEnterForeground(_ application: UIApplication) { 30 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 31 | } 32 | 33 | func applicationDidBecomeActive(_ application: UIApplication) { 34 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 35 | } 36 | 37 | func applicationWillTerminate(_ application: UIApplication) { 38 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 39 | } 40 | 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /lib/command/executor/fetcher.rb: -------------------------------------------------------------------------------- 1 | require "parallel" 2 | require_relative "base" 3 | require_relative "../helper/zip" 4 | 5 | module PodPrebuild 6 | class CacheFetcher < CommandExecutor 7 | attr_reader :cache_branch 8 | 9 | def initialize(options) 10 | super(options) 11 | @cache_branch = options[:cache_branch] 12 | end 13 | 14 | def run 15 | Pod::UI.step("Fetching cache") do 16 | if @config.local_cache? 17 | print_message_for_local_cache(@config.cache_path) 18 | else 19 | fetch_remote_cache(@config.cache_repo, @cache_branch, @config.cache_path) 20 | end 21 | unzip_cache 22 | end 23 | end 24 | 25 | private 26 | 27 | def print_message_for_local_cache(cache_dir) 28 | Pod::UI.puts "You're using local cache at: #{cache_dir}.".yellow 29 | message = <<~HEREDOC 30 | To enable remote cache (with a git repo), add the `remote` field to the repo config in the `cache_repo` option. 31 | For more details, check out this doc: 32 | https://github.com/grab/cocoapods-binary-cache/blob/master/docs/configure_cocoapods_binary_cache.md#cache_repo- 33 | HEREDOC 34 | Pod::UI.puts message 35 | end 36 | 37 | def fetch_remote_cache(repo, branch, dest_dir) 38 | Pod::UI.puts "Fetching cache from #{repo} (branch: #{branch})".green 39 | if Dir.exist?(dest_dir + "/.git") 40 | git("fetch origin #{branch}") 41 | git("checkout -f FETCH_HEAD", ignore_output: true) 42 | git("branch -D #{branch}", ignore_output: true, can_fail: true) 43 | git("checkout -b #{branch}") 44 | else 45 | FileUtils.rm_rf(dest_dir) 46 | git_clone("--depth=1 --branch=#{branch} #{repo} #{dest_dir}") 47 | end 48 | end 49 | 50 | def unzip_cache 51 | Pod::UI.puts "Unzipping cache: #{@config.cache_path} -> #{@config.prebuild_sandbox_path}".green 52 | FileUtils.rm_rf(@config.prebuild_sandbox_path) 53 | FileUtils.mkdir_p(@config.prebuild_sandbox_path) 54 | 55 | if File.exist?(@config.manifest_path(in_cache: true)) 56 | FileUtils.cp( 57 | @config.manifest_path(in_cache: true), 58 | @config.manifest_path 59 | ) 60 | end 61 | zip_paths = Dir[@config.generated_frameworks_dir(in_cache: true) + "/*.zip"] 62 | Parallel.each(zip_paths, in_threads: 8) do |path| 63 | ZipUtils.unzip(path, to_dir: @config.generated_frameworks_dir) 64 | end 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/helper/prebuild_sandbox.rb: -------------------------------------------------------------------------------- 1 | require_relative "names" 2 | 3 | module Pod 4 | class PrebuildSandbox < Sandbox 5 | # [String] standard_sandbox_path 6 | def self.from_standard_sandbox_path(path) 7 | prebuild_sandbox_path = Pathname.new(path).realpath + ".." + PodPrebuild.config.prebuild_sandbox_path 8 | new(prebuild_sandbox_path) 9 | end 10 | 11 | def self.from_standard_sandbox(sandbox) 12 | from_standard_sandbox_path(sandbox.root) 13 | end 14 | 15 | def standard_sanbox_path 16 | root.parent 17 | end 18 | 19 | def generate_framework_path 20 | root + "GeneratedFrameworks" 21 | end 22 | 23 | # @param name [String] pass the target.name (may containing platform suffix) 24 | # @return [Pathname] the folder containing the framework file. 25 | def framework_folder_path_for_target_name(name) 26 | generate_framework_path + name 27 | end 28 | 29 | def exsited_framework_target_names 30 | existed_framework_name_pairs.map { |pair| pair[0] }.uniq 31 | end 32 | 33 | def exsited_framework_pod_names 34 | existed_framework_name_pairs.map { |pair| pair[1] }.uniq 35 | end 36 | 37 | def existed_target_names_for_pod_name(pod_name) 38 | existed_framework_name_pairs.select { |pair| pair[1] == pod_name }.map { |pair| pair[0] } 39 | end 40 | 41 | def save_pod_name_for_target(target) 42 | folder = framework_folder_path_for_target_name(target.name) 43 | return unless folder.exist? 44 | 45 | flag_file_path = folder + "#{target.pod_name}.pod_name" 46 | File.write(flag_file_path.to_s, "") 47 | end 48 | 49 | private 50 | 51 | def pod_name_for_target_folder(target_folder_path) 52 | name = Pathname.new(target_folder_path).children.find do |child| 53 | child.to_s.end_with? ".pod_name" 54 | end 55 | name = name.basename(".pod_name").to_s unless name.nil? 56 | name ||= Pathname.new(target_folder_path).basename.to_s # for compatibility with older version 57 | name 58 | end 59 | 60 | # Array<[target_name, pod_name]> 61 | def existed_framework_name_pairs 62 | return [] unless generate_framework_path.exist? 63 | 64 | generate_framework_path.children.map do |framework_path| 65 | if framework_path.directory? && !framework_path.children.empty? 66 | [framework_path.basename.to_s, pod_name_for_target_folder(framework_path)] 67 | end 68 | end.reject(&:nil?).uniq 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/dependencies_graph/graph_visualizer.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | # https://github.com/monora/rgl/blob/0b526e16f9fb344abf387f4c5523d7917ce8f4b1/lib/rgl/dot.rb 4 | 5 | require "rgl/rdot" 6 | 7 | module RGL 8 | module Graph 9 | def to_dot_graph(options) 10 | highlight_nodes = options[:highlight_nodes] || Set.new 11 | options["name"] ||= self.class.name.gsub(/:/, "_") 12 | fontsize = options["fontsize"] || "12" 13 | graph = (directed? ? DOT::Digraph : DOT::Graph).new(options) 14 | edge_class = directed? ? DOT::DirectedEdge : DOT::Edge 15 | vertex_options = options["vertex"] || {} 16 | edge_options = options["edge"] || {} 17 | 18 | each_vertex do |v| 19 | default_vertex_options = { 20 | "name" => vertex_id(v), 21 | "fontsize" => fontsize, 22 | "label" => vertex_label(v), 23 | "style" => "filled" 24 | } 25 | default_vertex_options.merge!("color" => "red", "fillcolor" => "red") if highlight_nodes.include?(v) 26 | each_vertex_options = default_vertex_options.merge(vertex_options) 27 | vertex_options.each { |option, val| each_vertex_options[option] = val.call(v) if val.is_a?(Proc) } 28 | graph << DOT::Node.new(each_vertex_options) 29 | end 30 | 31 | each_edge do |u, v| 32 | default_edge_options = { 33 | "from" => vertex_id(u), 34 | "to" => vertex_id(v), 35 | "fontsize" => fontsize 36 | } 37 | each_edge_options = default_edge_options.merge(edge_options) 38 | edge_options.each { |option, val| each_edge_options[option] = val.call(u, v) if val.is_a?(Proc) } 39 | graph << edge_class.new(each_edge_options) 40 | end 41 | 42 | graph 43 | end 44 | 45 | def write_to_graphic_file(options) 46 | output_path = Pathname.new(options[:output_path]) 47 | fmt = output_path.extname.delete_prefix(".") 48 | dotfile = output_path.sub_ext(".dot") 49 | 50 | File.open(dotfile, "w") do |f| 51 | f << to_dot_graph(options).to_s 52 | end 53 | 54 | unless system("dot -T#{fmt} #{dotfile} -o #{output_path}") 55 | message = <<~HEREDOC 56 | Error executing dot. Did you install GraphViz? 57 | Try installing it via Homebrew: `brew install graphviz`. 58 | Visit https://graphviz.org/download/ for more installation instructions. 59 | HEREDOC 60 | raise message 61 | end 62 | output_path 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /integration_tests/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - BKMoneyKit (0.0.12) 3 | - GoogleMaps (2.7.0): 4 | - GoogleMaps/Maps (= 2.7.0) 5 | - GoogleMaps/Base (2.7.0) 6 | - GoogleMaps/Maps (2.7.0): 7 | - GoogleMaps/Base 8 | - GoogleSignIn (4.2.0): 9 | - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" 10 | - "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)" 11 | - GTMOAuth2 (~> 1.0) 12 | - GTMSessionFetcher/Core (~> 1.1) 13 | - GoogleToolboxForMac/DebugUtils (2.2.2): 14 | - GoogleToolboxForMac/Defines (= 2.2.2) 15 | - GoogleToolboxForMac/Defines (2.2.2) 16 | - "GoogleToolboxForMac/NSDictionary+URLArguments (2.2.2)": 17 | - GoogleToolboxForMac/DebugUtils (= 2.2.2) 18 | - GoogleToolboxForMac/Defines (= 2.2.2) 19 | - "GoogleToolboxForMac/NSString+URLArguments (= 2.2.2)" 20 | - "GoogleToolboxForMac/NSString+URLArguments (2.2.2)" 21 | - GTMOAuth2 (1.1.6): 22 | - GTMSessionFetcher (~> 1.1) 23 | - GTMSessionFetcher (1.4.0): 24 | - GTMSessionFetcher/Full (= 1.4.0) 25 | - GTMSessionFetcher/Core (1.4.0) 26 | - GTMSessionFetcher/Full (1.4.0): 27 | - GTMSessionFetcher/Core (= 1.4.0) 28 | - IQKeyboardManagerSwift (6.1.1) 29 | - SwiftDate (6.1.0) 30 | - UICommonsDynamic (0.0.1) 31 | - UICommonsStatic (0.0.1) 32 | 33 | DEPENDENCIES: 34 | - BKMoneyKit (= 0.0.12) 35 | - GoogleMaps (= 2.7.0) 36 | - GoogleSignIn (= 4.2.0) 37 | - IQKeyboardManagerSwift (= 6.1.1) 38 | - SwiftDate (= 6.1.0) 39 | - UICommonsDynamic (from `LocalPods/UICommonsDynamic`) 40 | - UICommonsStatic (from `LocalPods/UICommonsStatic`) 41 | 42 | SPEC REPOS: 43 | trunk: 44 | - BKMoneyKit 45 | - GoogleMaps 46 | - GoogleSignIn 47 | - GoogleToolboxForMac 48 | - GTMOAuth2 49 | - GTMSessionFetcher 50 | - IQKeyboardManagerSwift 51 | - SwiftDate 52 | 53 | EXTERNAL SOURCES: 54 | UICommonsDynamic: 55 | :path: LocalPods/UICommonsDynamic 56 | UICommonsStatic: 57 | :path: LocalPods/UICommonsStatic 58 | 59 | SPEC CHECKSUMS: 60 | BKMoneyKit: 1e075497e9b6d9c3ba37c646dcf647ca6358ec43 61 | GoogleMaps: f79af95cb24d869457b1f961c93d3ce8b2f3b848 62 | GoogleSignIn: 591e46382014e591269f862ba6e7bc0fbd793532 63 | GoogleToolboxForMac: 800648f8b3127618c1b59c7f97684427630c5ea3 64 | GTMOAuth2: e8b6512c896235149df975c41d9a36c868ab7fba 65 | GTMSessionFetcher: 6f5c8abbab8a9bce4bb3f057e317728ec6182b10 66 | IQKeyboardManagerSwift: 977affaeb4d6e971975c7790a6850f31d38f1207 67 | SwiftDate: fa2bb3962056bb44047b4b85a30044e5eae30b03 68 | UICommonsDynamic: 91a56a831dc441d3e5439b11dd997c1331f38eed 69 | UICommonsStatic: cdabede00ad1bdd269e5781144b8708aaa20814d 70 | 71 | PODFILE CHECKSUM: 838123ad6e2d7b48472dbb98fff20ef1c2064e30 72 | 73 | COCOAPODS: 1.9.3 74 | -------------------------------------------------------------------------------- /spec/command/prebuild_command_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Pod::Command::Binary::Prebuild" do 2 | describe "#initialize" do 3 | let(:args) { [] } 4 | before do 5 | PodPrebuild.config.reset! 6 | allow_any_instance_of(PodPrebuild::CachePrebuilder).to receive(:run) 7 | @command = Pod::Command::Binary::Prebuild.new(CLAide::ARGV.new(args)) 8 | end 9 | after do 10 | PodPrebuild.config.reset! 11 | end 12 | 13 | context "when no options is specified" do 14 | it "creates fetcher with cache branch as master" do 15 | expect(@command.prebuilder.fetcher&.cache_branch).to eq("master") 16 | end 17 | it "does not push to cache repo upon completion" do 18 | expect(@command.prebuilder.pusher).to eq(nil) 19 | end 20 | end 21 | 22 | context "cache branch is specified" do 23 | let(:cache_branch) { "dummy" } 24 | let(:args) { ["--push", cache_branch] } 25 | it "creates fetcher & pusher with the given cache branch" do 26 | expect(@command.prebuilder.fetcher&.cache_branch).to eq(cache_branch) 27 | expect(@command.prebuilder.pusher&.cache_branch).to eq(cache_branch) 28 | end 29 | end 30 | 31 | context "option --push is specified" do 32 | let(:args) { ["--push"] } 33 | it "creates pusher with cache branch as master" do 34 | expect(@command.prebuilder.pusher&.cache_branch).to eq("master") 35 | end 36 | end 37 | 38 | context "option --all is specified" do 39 | let(:args) { ["--all"] } 40 | it "updates :prebuild_all_pods to CLI config" do 41 | expect(PodPrebuild.config.cli_config[:prebuild_all_pods]).to eq(true) 42 | end 43 | end 44 | 45 | context "option --config is specified" do 46 | let(:args) { ["--config=Test"] } 47 | it "updates :prebuild_config to CLI config" do 48 | expect(PodPrebuild.config.cli_config[:prebuild_config]).to eq("Test") 49 | end 50 | end 51 | 52 | context "option --targets is specified" do 53 | let(:args) { ["--targets=A,B,C"] } 54 | it "updates :prebuild_targets to CLI config" do 55 | expect(PodPrebuild.config.cli_config[:prebuild_targets]).to eq(["A", "B", "C"]) 56 | end 57 | end 58 | 59 | context "option --repo-update is specified" do 60 | let(:args) { ["--repo-update"] } 61 | it "sets repo_update to the installer" do 62 | expect(@command.prebuilder.repo_update).to eq(true) 63 | end 64 | end 65 | 66 | context "option --no-fetch is specified" do 67 | let(:args) { ["--no-fetch"] } 68 | it "sets fetcher to nil" do 69 | expect(@command.prebuilder.fetcher).to eq(nil) 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /integration_tests/prebuilt_cache/default/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - BKMoneyKit (0.0.12) 3 | - GoogleMaps (2.7.0): 4 | - GoogleMaps/Maps (= 2.7.0) 5 | - GoogleMaps/Base (2.7.0) 6 | - GoogleMaps/Maps (2.7.0): 7 | - GoogleMaps/Base 8 | - GoogleSignIn (4.2.0): 9 | - "GoogleToolboxForMac/NSDictionary+URLArguments (~> 2.1)" 10 | - "GoogleToolboxForMac/NSString+URLArguments (~> 2.1)" 11 | - GTMOAuth2 (~> 1.0) 12 | - GTMSessionFetcher/Core (~> 1.1) 13 | - GoogleToolboxForMac/DebugUtils (2.2.2): 14 | - GoogleToolboxForMac/Defines (= 2.2.2) 15 | - GoogleToolboxForMac/Defines (2.2.2) 16 | - "GoogleToolboxForMac/NSDictionary+URLArguments (2.2.2)": 17 | - GoogleToolboxForMac/DebugUtils (= 2.2.2) 18 | - GoogleToolboxForMac/Defines (= 2.2.2) 19 | - "GoogleToolboxForMac/NSString+URLArguments (= 2.2.2)" 20 | - "GoogleToolboxForMac/NSString+URLArguments (2.2.2)" 21 | - GTMOAuth2 (1.1.6): 22 | - GTMSessionFetcher (~> 1.1) 23 | - GTMSessionFetcher (1.4.0): 24 | - GTMSessionFetcher/Full (= 1.4.0) 25 | - GTMSessionFetcher/Core (1.4.0) 26 | - GTMSessionFetcher/Full (1.4.0): 27 | - GTMSessionFetcher/Core (= 1.4.0) 28 | - IQKeyboardManagerSwift (6.1.1) 29 | - SwiftDate (6.1.0) 30 | - UICommonsDynamic (0.0.1) 31 | - UICommonsStatic (0.0.1) 32 | 33 | DEPENDENCIES: 34 | - BKMoneyKit (= 0.0.12) 35 | - GoogleMaps (= 2.7.0) 36 | - GoogleSignIn (= 4.2.0) 37 | - IQKeyboardManagerSwift (= 6.1.1) 38 | - SwiftDate (= 6.1.0) 39 | - UICommonsDynamic (from `LocalPods/UICommonsDynamic`) 40 | - UICommonsStatic (from `LocalPods/UICommonsStatic`) 41 | 42 | SPEC REPOS: 43 | trunk: 44 | - BKMoneyKit 45 | - GoogleMaps 46 | - GoogleSignIn 47 | - GoogleToolboxForMac 48 | - GTMOAuth2 49 | - GTMSessionFetcher 50 | - IQKeyboardManagerSwift 51 | - SwiftDate 52 | 53 | EXTERNAL SOURCES: 54 | UICommonsDynamic: 55 | :path: LocalPods/UICommonsDynamic 56 | UICommonsStatic: 57 | :path: LocalPods/UICommonsStatic 58 | 59 | SPEC CHECKSUMS: 60 | BKMoneyKit: 1e075497e9b6d9c3ba37c646dcf647ca6358ec43 61 | GoogleMaps: f79af95cb24d869457b1f961c93d3ce8b2f3b848 62 | GoogleSignIn: 591e46382014e591269f862ba6e7bc0fbd793532 63 | GoogleToolboxForMac: 800648f8b3127618c1b59c7f97684427630c5ea3 64 | GTMOAuth2: e8b6512c896235149df975c41d9a36c868ab7fba 65 | GTMSessionFetcher: 6f5c8abbab8a9bce4bb3f057e317728ec6182b10 66 | IQKeyboardManagerSwift: 977affaeb4d6e971975c7790a6850f31d38f1207 67 | SwiftDate: fa2bb3962056bb44047b4b85a30044e5eae30b03 68 | UICommonsDynamic: 91a56a831dc441d3e5439b11dd997c1331f38eed 69 | UICommonsStatic: cdabede00ad1bdd269e5781144b8708aaa20814d 70 | 71 | PODFILE CHECKSUM: 838123ad6e2d7b48472dbb98fff20ef1c2064e30 72 | 73 | COCOAPODS: 1.9.3 74 | -------------------------------------------------------------------------------- /spec/integration/installer_spec.rb: -------------------------------------------------------------------------------- 1 | require "cocoapods-binary-cache/pod-binary/integration/patch/source_installation" 2 | 3 | describe "Pod::Installer" do 4 | describe "#create_pod_installer" do 5 | let(:prebuilt_pod_names_cache_missed) { ["A"] } 6 | let(:prebuilt_pod_names_cache_hit) { ["B"] } 7 | let(:prebuilt_pod_names) { prebuilt_pod_names_cache_missed + prebuilt_pod_names_cache_hit } 8 | let(:non_prebuilt_pod_names) { ["X"] } 9 | let(:pod_names) { prebuilt_pod_names + non_prebuilt_pod_names } 10 | let(:tmp_dir) { create_tempdir } 11 | let(:sandbox) { Pod::Sandbox.new(tmp_dir) } 12 | 13 | before do 14 | allow(PodPrebuild.state).to receive(:cache_validation).and_return( 15 | PodPrebuild::CacheValidationResult.new( 16 | prebuilt_pod_names_cache_missed.map { |name| [name, "missing"] }.to_h, 17 | prebuilt_pod_names_cache_hit.to_set 18 | ) 19 | ) 20 | 21 | @installer = Pod::Installer.new(sandbox, Pod::Podfile.new, nil) 22 | allow(@installer).to receive(:create_prebuilt_source_installer).and_return("prebuilt") 23 | allow(@installer).to receive(:create_normal_source_installer).and_return("normal") 24 | end 25 | 26 | after do 27 | FileUtils.remove_entry tmp_dir 28 | end 29 | 30 | def expect_installed_as(type, pods) 31 | pods.each { |name| expect(@installer.create_pod_installer(name)).to eq(type) } 32 | end 33 | 34 | def expect_installed_as_normal(pods) 35 | expect_installed_as("normal", pods) 36 | end 37 | 38 | def expect_installed_as_prebuilt(pods) 39 | expect_installed_as("prebuilt", pods) 40 | end 41 | 42 | it "installs source of non prebuilt pods as normal" do 43 | expect_installed_as_normal(non_prebuilt_pod_names) 44 | end 45 | 46 | it "installs source the missed pod as normal" do 47 | expect_installed_as_normal(prebuilt_pod_names_cache_missed) 48 | end 49 | 50 | it "installs source of prebuilt pods with cache hit differently" do 51 | expect_installed_as_prebuilt(prebuilt_pod_names_cache_hit) 52 | end 53 | 54 | context "is prebuild job" do 55 | before do 56 | allow(PodPrebuild.config).to receive(:prebuild_job?).and_return(true) 57 | end 58 | it "installs source of all prebuilt pods differently" do 59 | expect_installed_as_prebuilt(prebuilt_pod_names) 60 | end 61 | 62 | context "targets were specified in CLI" do 63 | let(:targets_to_prebuild_from_cli) { ["Y"] } 64 | before do 65 | allow(PodPrebuild.config).to receive(:targets_to_prebuild_from_cli).and_return(targets_to_prebuild_from_cli) 66 | end 67 | it "installs specified targets & cache hit as prebuilt" do 68 | expect_installed_as_prebuilt(prebuilt_pod_names_cache_hit + targets_to_prebuild_from_cli) 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: .. 3 | specs: 4 | cocoapods-binary-cache (0.1.3) 5 | cocoapods (>= 1.5.0) 6 | fourflusher (~> 2.0) 7 | rgl (~> 0.5.6) 8 | xcpretty (~> 0.3.0) 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | CFPropertyList (3.0.2) 14 | activesupport (4.2.11.1) 15 | i18n (~> 0.7) 16 | minitest (~> 5.1) 17 | thread_safe (~> 0.3, >= 0.3.4) 18 | tzinfo (~> 1.1) 19 | algoliasearch (1.27.1) 20 | httpclient (~> 2.8, >= 2.8.3) 21 | json (>= 1.5.1) 22 | atomos (0.1.3) 23 | claide (1.0.3) 24 | cocoapods (1.8.4) 25 | activesupport (>= 4.0.2, < 5) 26 | claide (>= 1.0.2, < 2.0) 27 | cocoapods-core (= 1.8.4) 28 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 29 | cocoapods-downloader (>= 1.2.2, < 2.0) 30 | cocoapods-plugins (>= 1.0.0, < 2.0) 31 | cocoapods-search (>= 1.0.0, < 2.0) 32 | cocoapods-stats (>= 1.0.0, < 2.0) 33 | cocoapods-trunk (>= 1.4.0, < 2.0) 34 | cocoapods-try (>= 1.1.0, < 2.0) 35 | colored2 (~> 3.1) 36 | escape (~> 0.0.4) 37 | fourflusher (>= 2.3.0, < 3.0) 38 | gh_inspector (~> 1.0) 39 | molinillo (~> 0.6.6) 40 | nap (~> 1.0) 41 | ruby-macho (~> 1.4) 42 | xcodeproj (>= 1.11.1, < 2.0) 43 | cocoapods-core (1.8.4) 44 | activesupport (>= 4.0.2, < 6) 45 | algoliasearch (~> 1.0) 46 | concurrent-ruby (~> 1.1) 47 | fuzzy_match (~> 2.0.4) 48 | nap (~> 1.0) 49 | cocoapods-deintegrate (1.0.4) 50 | cocoapods-downloader (1.3.0) 51 | cocoapods-plugins (1.0.0) 52 | nap 53 | cocoapods-search (1.0.0) 54 | cocoapods-stats (1.1.0) 55 | cocoapods-trunk (1.5.0) 56 | nap (>= 0.8, < 2.0) 57 | netrc (~> 0.11) 58 | cocoapods-try (1.2.0) 59 | colored2 (3.1.2) 60 | concurrent-ruby (1.1.6) 61 | escape (0.0.4) 62 | fourflusher (2.3.1) 63 | fuzzy_match (2.0.4) 64 | gh_inspector (1.1.3) 65 | httpclient (2.8.3) 66 | i18n (0.9.5) 67 | concurrent-ruby (~> 1.0) 68 | json (2.3.0) 69 | lazy_priority_queue (0.1.1) 70 | minitest (5.14.0) 71 | molinillo (0.6.6) 72 | nanaimo (0.2.6) 73 | nap (1.1.0) 74 | netrc (0.11.0) 75 | rgl (0.5.6) 76 | lazy_priority_queue (~> 0.1.0) 77 | stream (~> 0.5.2) 78 | rouge (2.0.7) 79 | ruby-macho (1.4.0) 80 | stream (0.5.2) 81 | thread_safe (0.3.6) 82 | tzinfo (1.2.7) 83 | thread_safe (~> 0.1) 84 | xcodeproj (1.17.0) 85 | CFPropertyList (>= 2.3.3, < 4.0) 86 | atomos (~> 0.1.3) 87 | claide (>= 1.0.2, < 2.0) 88 | colored2 (~> 3.1) 89 | nanaimo (~> 0.2.6) 90 | xcpretty (0.3.0) 91 | rouge (~> 2.0.7) 92 | 93 | PLATFORMS 94 | ruby 95 | 96 | DEPENDENCIES 97 | cocoapods (= 1.8.4) 98 | cocoapods-binary-cache! 99 | 100 | BUNDLED WITH 101 | 2.1.4 102 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/helper/lockfile.rb: -------------------------------------------------------------------------------- 1 | require_relative "checksum" 2 | 3 | module PodPrebuild 4 | class Lockfile 5 | attr_reader :lockfile, :data 6 | 7 | def initialize(lockfile) 8 | @lockfile = lockfile 9 | @data = lockfile.to_hash 10 | end 11 | 12 | def pods 13 | @pods ||= (@data["PODS"] || []).map { |v| pod_from(v) }.to_h 14 | end 15 | 16 | def external_sources 17 | @data["EXTERNAL SOURCES"] || {} 18 | end 19 | 20 | def dev_pod_sources 21 | @dev_pod_sources ||= external_sources.select { |_, attributes| attributes.key?(:path) } || {} 22 | end 23 | 24 | def dev_pod_names 25 | # There are 2 types of external sources: 26 | # - Development pods: declared with `:path` option in Podfile, corresponding to `:path` in the Lockfile 27 | # - External remote pods: declared with `:git` option in Podfile, corresponding to `:git` in the Lockfile 28 | # -------------------- 29 | # EXTERNAL SOURCES: 30 | # ADevPod: 31 | # :path: path/to/dev_pod 32 | # AnExternalRemotePod: 33 | # :git: git@remote_url 34 | # :commit: abc1234 35 | # -------------------- 36 | @dev_pod_names ||= dev_pod_sources.keys.to_set 37 | end 38 | 39 | def dev_pods 40 | dev_pod_names_ = dev_pod_names 41 | @dev_pods ||= pods.select { |name, _| dev_pod_names_.include?(name) } 42 | end 43 | 44 | def non_dev_pods 45 | dev_pod_names_ = dev_pod_names 46 | @non_dev_pods ||= pods.reject { |name, _| dev_pod_names_.include?(name) } 47 | end 48 | 49 | def subspec_vendor_pods 50 | dev_pod_names_ = dev_pod_names 51 | @subspec_vendor_pods ||= subspec_pods.reject { |name, _| dev_pod_names_.include?(name) } 52 | end 53 | 54 | # Return content hash (Hash the directory at source path) of a dev_pod 55 | # Return nil if it's not a dev_pod 56 | def dev_pod_hash(pod_name) 57 | dev_pod_hashes_map[pod_name] 58 | end 59 | 60 | private 61 | 62 | def subspec_pods 63 | @subspec_pods ||= pods.keys 64 | .select { |k| k.include?("/") } 65 | .group_by { |k| k.split("/")[0] } 66 | end 67 | 68 | # Generate a map between a dev_pod and it source hash 69 | def dev_pod_hashes_map 70 | @dev_pod_hashes_map ||= 71 | dev_pod_sources.map { |name, attribs| [name, FolderChecksum.git_checksum(attribs[:path])] }.to_h 72 | end 73 | 74 | # Parse an item under `PODS` section of a Lockfile 75 | # @param hash_or_string: an item under `PODS` section, could be a Hash (if having dependencies) or a String 76 | # Examples: 77 | # -------------------------- 78 | # PODS: 79 | # - FrameworkA (0.0.1) 80 | # - FrameworkB (0.0.2): 81 | # - DependencyOfB 82 | # ------------------------- 83 | # @return [framework_name, version] (for ex. ["AFramework", "0.0.1"]) 84 | def pod_from(hash_or_string) 85 | name_with_version = hash_or_string.is_a?(Hash) ? hash_or_string.keys[0] : hash_or_string 86 | match = name_with_version.match(/(\S+) \((\S+)\)/) 87 | [match[1], match[2]] 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /spec/helper/podspec_spec.rb: -------------------------------------------------------------------------------- 1 | describe "Specification" do 2 | describe "#empty_source_files?" do 3 | it "returns true if #source_files is not specified and is empty" do 4 | spec = Pod::Specification.new 5 | expect(spec.empty_source_files?).to be true 6 | 7 | spec = Pod::Specification.new { |s| s.source_files = [] } 8 | expect(spec.empty_source_files?).to be true 9 | 10 | spec = Pod::Specification.new { |s| s.source_files = "" } 11 | expect(spec.empty_source_files?).to be true 12 | end 13 | 14 | it "returns true if #source_files only contains headers" do 15 | spec = Pod::Specification.new { |s| s.source_files = ["path/to/*.h"] } 16 | expect(spec.empty_source_files?).to be true 17 | 18 | spec = Pod::Specification.new { |s| s.source_files = "path/to/*.hpp" } 19 | expect(spec.empty_source_files?).to be true 20 | end 21 | 22 | it "returns false if #source_files contains non-header files" do 23 | spec = Pod::Specification.new { |s| s.source_files = ["path/to/*.swift"] } 24 | expect(spec.empty_source_files?).to be false 25 | 26 | spec = Pod::Specification.new { |s| s.source_files = "path/to/*.swift" } 27 | expect(spec.empty_source_files?).to be false 28 | end 29 | 30 | context "multi-platforms" do 31 | it "returns false if exists a platform containing non-header files" do 32 | spec = Pod::Specification.new do |s| 33 | s.ios.source_files = [] 34 | s.tvos.source_files = "path/to/*.hpp" 35 | s.osx.source_files = ["path/to/*.swift"] 36 | end 37 | expect(spec.empty_source_files?).to be false 38 | end 39 | 40 | it "returns true if all platforms satisfy" do 41 | spec = Pod::Specification.new do |s| 42 | s.ios.source_files = [] 43 | s.tvos.source_files = "" 44 | s.osx.source_files = [""] 45 | end 46 | expect(spec.empty_source_files?).to be true 47 | end 48 | end 49 | 50 | context "has subspecs" do 51 | it "returns true if all subspecs have empty source files" do 52 | spec = Pod::Specification.new do |s| 53 | s.subspec("A") { |ss| ss.source_files = [] } 54 | s.subspec("B") { |ss| ss.source_files = ["path/to/*.h"] } 55 | end 56 | expect(spec.empty_source_files?).to be true 57 | end 58 | 59 | it "returns false if exists a subspec having source files" do 60 | spec = Pod::Specification.new do |s| 61 | s.subspec("A") { |ss| ss.source_files = [] } 62 | s.subspec("B") { |ss| ss.source_files = ["path/to/*.swift"] } 63 | end 64 | expect(spec.empty_source_files?).to be false 65 | end 66 | 67 | it "returns false if all subspecs have empty source files, but the parent spec has" do 68 | spec = Pod::Specification.new do |s| 69 | s.source_files = ["path/to/*.cpp"] 70 | s.subspec("A") { |ss| ss.source_files = [] } 71 | s.subspec("B") { |ss| ss.source_files = [] } 72 | end 73 | expect(spec.empty_source_files?).to be false 74 | end 75 | 76 | it "returns false if a subspec and the spec itself have source files" do 77 | spec = Pod::Specification.new do |s| 78 | s.source_files = ["path/to/*.cpp"] 79 | s.subspec("A") { |ss| ss.source_files = [] } 80 | s.subspec("B") { |ss| ss.source_files = ["path/to/*.swift"] } 81 | end 82 | expect(spec.empty_source_files?).to be false 83 | end 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/dependencies_graph/dependencies_graph.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2019 Grabtaxi Holdings PTE LTE (GRAB), All rights reserved. 2 | # Use of this source code is governed by an MIT-style license that can be found in the LICENSE file 3 | 4 | require "rgl/adjacency" 5 | require "rgl/dot" 6 | require_relative "graph_visualizer" 7 | 8 | # Using RGL graph because GraphViz doesn't store adjacent of a node/vertex 9 | # but we need to traverse a substree from any node 10 | # https://github.com/monora/rgl/blob/master/lib/rgl/adjacency.rb 11 | 12 | class DependenciesGraph 13 | def initialize(options) 14 | @lockfile = options[:lockfile] 15 | @devpod_only = options[:devpod_only] 16 | @max_deps = options[:max_deps].to_i if options[:max_deps] 17 | # A normal edge is an edge (one direction) from library A to library B which is a dependency of A. 18 | @invert_edge = options[:invert_edge] || false 19 | end 20 | 21 | # Input : a list of library names. 22 | # Output: a set of library names which are clients (directly and indirectly) of those input libraries. 23 | def get_clients(libnames) 24 | result = Set.new 25 | libnames.each do |lib| 26 | if graph.has_vertex?(lib) 27 | result.merge(traverse_sub_tree(graph, lib)) 28 | else 29 | Pod::UI.puts "Warning: cannot find lib: #{lib}" 30 | end 31 | end 32 | result 33 | end 34 | 35 | def write_graphic_file(options) 36 | graph.write_to_graphic_file(options) 37 | end 38 | 39 | private 40 | 41 | def dependencies 42 | @dependencies ||= (@lockfile && @lockfile.to_hash["PODS"]) 43 | end 44 | 45 | def dev_pod_sources 46 | @dev_pod_sources ||= @lockfile.to_hash["EXTERNAL SOURCES"].select { |_, attributes| attributes.key?(:path) } || {} 47 | end 48 | 49 | # Convert array of dictionaries -> a dictionary with format {A: [A's dependencies]} 50 | def pod_to_dependencies 51 | dependencies 52 | .map { |d| d.is_a?(Hash) ? d : { d => [] } } 53 | .reduce({}) { |combined, individual| combined.merge!(individual) } 54 | end 55 | 56 | def add_vertex(graph, pod) 57 | node_name = sanitized_pod_name(pod) 58 | return if @devpod_only && dev_pod_sources[node_name].nil? 59 | 60 | graph.add_vertex(node_name) 61 | node_name 62 | end 63 | 64 | def sanitized_pod_name(name) 65 | Pod::Dependency.from_string(name).name 66 | end 67 | 68 | def reach_max_deps(deps) 69 | return unless @max_deps 70 | return deps.count > @max_deps unless @devpod_only 71 | 72 | deps = deps.reject { |name| dev_pod_sources[name].nil? } 73 | deps.count > @max_deps 74 | end 75 | 76 | def graph 77 | @graph ||= begin 78 | graph = RGL::DirectedAdjacencyGraph.new 79 | pod_to_dependencies.each do |pod, dependencies| 80 | next if reach_max_deps(dependencies) 81 | 82 | pod_node = add_vertex(graph, pod) 83 | next if pod_node.nil? 84 | 85 | dependencies.each do |dependency| 86 | dep_node = add_vertex(graph, dependency) 87 | next if dep_node.nil? 88 | 89 | if @invert_edge 90 | graph.add_edge(dep_node, pod_node) 91 | else 92 | graph.add_edge(pod_node, dep_node) 93 | end 94 | end 95 | end 96 | graph 97 | end 98 | end 99 | 100 | def traverse_sub_tree(graph, vertex) 101 | visited_nodes = Set.new 102 | graph.each_adjacent(vertex) do |v| 103 | visited_nodes.add(v) 104 | visited_nodes.merge(traverse_sub_tree(graph, v)) 105 | end 106 | visited_nodes 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /integration_tests/PrebuiltPodIntegration.xcodeproj/xcshareddata/xcschemes/PrebuiltPodIntegration.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 55 | 61 | 62 | 63 | 64 | 70 | 72 | 78 | 79 | 80 | 81 | 83 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/PodBinCacheExample.xcodeproj/xcshareddata/xcschemes/PodBinCacheExample.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 57 | 63 | 64 | 65 | 66 | 67 | 68 | 74 | 76 | 82 | 83 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /PodBinaryCacheExample/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AFNetworking (3.2.1): 3 | - AFNetworking/NSURLSession (= 3.2.1) 4 | - AFNetworking/Reachability (= 3.2.1) 5 | - AFNetworking/Security (= 3.2.1) 6 | - AFNetworking/Serialization (= 3.2.1) 7 | - AFNetworking/UIKit (= 3.2.1) 8 | - AFNetworking/NSURLSession (3.2.1): 9 | - AFNetworking/Reachability 10 | - AFNetworking/Security 11 | - AFNetworking/Serialization 12 | - AFNetworking/Reachability (3.2.1) 13 | - AFNetworking/Security (3.2.1) 14 | - AFNetworking/Serialization (3.2.1) 15 | - AFNetworking/UIKit (3.2.1): 16 | - AFNetworking/NSURLSession 17 | - Alamofire (4.9.1) 18 | - CocoaLumberjack (3.6.0): 19 | - CocoaLumberjack/Core (= 3.6.0) 20 | - CocoaLumberjack/Core (3.6.0) 21 | - ConfigSDK (0.0.1): 22 | - ProtocolBuffers-Swift 23 | - SQLite.swift 24 | - ConfigService (0.0.1): 25 | - ConfigSDK 26 | - RxSwift 27 | - Kingfisher (5.11.0): 28 | - Kingfisher/Core (= 5.11.0) 29 | - Kingfisher/Core (5.11.0) 30 | - Masonry (1.1.0) 31 | - MBProgressHUD (1.1.0) 32 | - MJRefresh (3.3.1) 33 | - ProtocolBuffers-Swift (4.0.6) 34 | - Realm (4.1.1): 35 | - Realm/Headers (= 4.1.1) 36 | - Realm/Headers (4.1.1) 37 | - RxSwift (5.1.1) 38 | - SDWebImage (5.4.0): 39 | - SDWebImage/Core (= 5.4.0) 40 | - SDWebImage/Core (5.4.0) 41 | - SnapKit (5.0.1) 42 | - SQLite.swift (0.12.2): 43 | - SQLite.swift/standard (= 0.12.2) 44 | - SQLite.swift/standard (0.12.2) 45 | - SVProgressHUD (2.2.5) 46 | - SwiftyJSON (5.0.0) 47 | 48 | DEPENDENCIES: 49 | - AFNetworking (= 3.2.1) 50 | - Alamofire 51 | - CocoaLumberjack 52 | - ConfigSDK (from `DevPods/ConfigSDK`) 53 | - ConfigService (from `DevPods/ConfigService`) 54 | - Kingfisher 55 | - Masonry 56 | - MBProgressHUD 57 | - MJRefresh 58 | - Realm 59 | - RxSwift (from `https://github.com/ReactiveX/RxSwift.git`, commit `a580d07ed002217fd91d8446c3a852486e9beefa`) 60 | - SDWebImage 61 | - SnapKit 62 | - SVProgressHUD 63 | - SwiftyJSON 64 | 65 | SPEC REPOS: 66 | https://github.com/CocoaPods/Specs.git: 67 | - AFNetworking 68 | - Alamofire 69 | - CocoaLumberjack 70 | - Kingfisher 71 | - Masonry 72 | - MBProgressHUD 73 | - MJRefresh 74 | - ProtocolBuffers-Swift 75 | - Realm 76 | - SDWebImage 77 | - SnapKit 78 | - SQLite.swift 79 | - SVProgressHUD 80 | - SwiftyJSON 81 | 82 | EXTERNAL SOURCES: 83 | ConfigSDK: 84 | :path: DevPods/ConfigSDK 85 | ConfigService: 86 | :path: DevPods/ConfigService 87 | RxSwift: 88 | :commit: a580d07ed002217fd91d8446c3a852486e9beefa 89 | :git: https://github.com/ReactiveX/RxSwift.git 90 | 91 | CHECKOUT OPTIONS: 92 | RxSwift: 93 | :commit: a580d07ed002217fd91d8446c3a852486e9beefa 94 | :git: https://github.com/ReactiveX/RxSwift.git 95 | 96 | SPEC CHECKSUMS: 97 | AFNetworking: b6f891fdfaed196b46c7a83cf209e09697b94057 98 | Alamofire: 85e8a02c69d6020a0d734f6054870d7ecb75cf18 99 | CocoaLumberjack: 78b0c238666f4f58db069738ec176f4519557516 100 | ConfigSDK: adac49ecc5094210b806c74c3fb1b1e8cb0c289f 101 | ConfigService: cae3fbae5b4bda148f481de790c4da1823443178 102 | Kingfisher: 4569606189149e19c7d9439f47e885d0679b7a90 103 | Masonry: 678fab65091a9290e40e2832a55e7ab731aad201 104 | MBProgressHUD: e7baa36a220447d8aeb12769bf0585582f3866d9 105 | MJRefresh: eeda70fbf0ad277f3178cef1cd0c3532591d6237 106 | ProtocolBuffers-Swift: 09b5cfcc641ab512d6aaa1d2cc3879a40432052c 107 | Realm: 25f0a8244cbb1c2271c922963ea8666fc2d8104d 108 | RxSwift: 81470a2074fa8780320ea5fe4102807cb7118178 109 | SDWebImage: 5bf6aec6481ae2a062bdc59f9d6c1d1e552090e0 110 | SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb 111 | SQLite.swift: d2b4642190917051ce6bd1d49aab565fe794eea3 112 | SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 113 | SwiftyJSON: 36413e04c44ee145039d332b4f4e2d3e8d6c4db7 114 | 115 | PODFILE CHECKSUM: 1cb7b7ae66561363f746423ff953196a642277a6 116 | 117 | COCOAPODS: 1.8.4 118 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/cache/validator_base.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class BaseCacheValidator 3 | attr_reader :podfile, :pod_lockfile, :prebuilt_lockfile 4 | attr_reader :validate_prebuilt_settings, :generated_framework_path 5 | 6 | def initialize(options) 7 | @podfile = options[:podfile] 8 | @pod_lockfile = options[:pod_lockfile] && PodPrebuild::Lockfile.new(options[:pod_lockfile]) 9 | @prebuilt_lockfile = options[:prebuilt_lockfile] && PodPrebuild::Lockfile.new(options[:prebuilt_lockfile]) 10 | @validate_prebuilt_settings = options[:validate_prebuilt_settings] 11 | @generated_framework_path = options[:generated_framework_path] 12 | end 13 | 14 | def validate(*) 15 | raise NotImplementedError 16 | end 17 | 18 | def changes_of_prebuilt_lockfile_vs_podfile 19 | @changes_of_prebuilt_lockfile_vs_podfile ||= Pod::Installer::Analyzer::SpecsState.new( 20 | @prebuilt_lockfile.lockfile.detect_changes_with_podfile(@podfile) 21 | ) 22 | end 23 | 24 | def validate_with_podfile 25 | changes = changes_of_prebuilt_lockfile_vs_podfile 26 | missed = changes.added.map { |pod| [pod, "Added from Podfile"] }.to_h 27 | missed.merge!(changes.changed.map { |pod| [pod, "Updated from Podfile"] }.to_h) 28 | PodPrebuild::CacheValidationResult.new(missed, changes.unchanged) 29 | end 30 | 31 | def validate_pods(options) 32 | pods = options[:pods] 33 | subspec_pods = options[:subspec_pods] 34 | prebuilt_pods = options[:prebuilt_pods] 35 | 36 | missed = {} 37 | hit = Set.new 38 | 39 | check_pod = lambda do |name| 40 | root_name = name.split("/")[0] 41 | version = pods[name] 42 | prebuilt_version = prebuilt_pods[name] 43 | result = false 44 | if prebuilt_version.nil? 45 | missed[name] = "Not available (#{version})" 46 | elsif prebuilt_version != version 47 | missed[name] = "Outdated: (prebuilt: #{prebuilt_version}) vs (#{version})" 48 | elsif load_metadata(root_name).blank? 49 | missed[name] = "Metadata not available (probably #{root_name}.zip is not in GeneratedFrameworks)" 50 | else 51 | diff = incompatible_pod(root_name) 52 | if diff.empty? 53 | hit << name 54 | result = true 55 | else 56 | missed[name] = "Incompatible: #{diff}" 57 | end 58 | end 59 | result 60 | end 61 | 62 | subspec_pods.each do |parent, children| 63 | missed_children = children.reject { |child| check_pod.call(child) } 64 | if missed_children.empty? 65 | hit << parent 66 | else 67 | missed[parent] = "Subspec pods were missed: #{missed_children}" 68 | end 69 | end 70 | 71 | non_subspec_pods = pods.reject { |pod| subspec_pods.include?(pod) } 72 | non_subspec_pods.each { |pod, _| check_pod.call(pod) } 73 | PodPrebuild::CacheValidationResult.new(missed, hit) 74 | end 75 | 76 | def incompatible_pod(name) 77 | # Pod incompatibility is a universal concept. Generally, it requires build settings compatibility. 78 | # For more checks, do override this function to define what it means by `incompatible`. 79 | incompatible_build_settings(name) 80 | end 81 | 82 | def incompatible_build_settings(name) 83 | settings_diff = {} 84 | prebuilt_build_settings = read_prebuilt_build_settings(name) 85 | validate_prebuilt_settings&.(name)&.each do |key, value| 86 | prebuilt_value = prebuilt_build_settings[key] 87 | unless prebuilt_value.nil? || value == prebuilt_value 88 | settings_diff[key] = { :current => value, :prebuilt => prebuilt_value } 89 | end 90 | end 91 | settings_diff 92 | end 93 | 94 | def load_metadata(name) 95 | @metadata_cache ||= {} 96 | cache = @metadata_cache[name] 97 | return cache unless cache.nil? 98 | 99 | metadata = PodPrebuild::Metadata.in_dir(generated_framework_path + name) 100 | @metadata_cache[name] = metadata 101 | metadata 102 | end 103 | 104 | def read_prebuilt_build_settings(name) 105 | load_metadata(name).build_settings 106 | end 107 | 108 | def read_source_hash(name) 109 | load_metadata(name).source_hash 110 | end 111 | end 112 | end 113 | -------------------------------------------------------------------------------- /docs/configure_cocoapods_binary_cache.md: -------------------------------------------------------------------------------- 1 | # Configure cocoapods-binary-cache 2 | 3 | This document guides you through how to config `cocoapods-binary-cache` via the `config_cocoapods_binary_cache` method in Podfile. 4 | 5 | Following are the options available in `config_cocoapods_binary_cache`. Options marked with (*) are mandatory for the plugin. 6 | 7 | ### `cache_repo` (*) 8 | 9 | Configure cache repo 10 | ```rb 11 | config_cocoapods_binary_cache( 12 | cache_repo: { 13 | "default" => { 14 | "remote" => "git@cache_repo.git", 15 | "local" => "~/.cocoapods-binary-cache/prebuilt-frameworks-debug-config" 16 | }, 17 | "test" => { 18 | "remote" => "git@another_cache_repo.git", 19 | "local" => "~/.cocoapods-binary-cache/prebuilt-frameworks-test-config" 20 | } 21 | } 22 | ) 23 | ``` 24 | 25 | Note: The cache repo can be specified in the CLI of `fetch`/`prebuild`/`push` command with the `--repo` option (`default` is used if not specified): 26 | ```sh 27 | bundle exec pod binary fetch --repo=test 28 | ``` 29 | 30 | ### `prebuild_sandbox_path` 31 | - Default: `_Prebuild`. 32 | - The path to the prebuild sandbox. 33 | 34 | ### `prebuild_config` 35 | - Default: `Debug`. 36 | - The configuration to use (such as `Debug`) when prebuilding pods. 37 | 38 | Note: This config can be overriden by the option `--config` in the `prebuild` CLI: 39 | ```sh 40 | bundle exec pod binary prebuild --config=Test 41 | ``` 42 | 43 | ### `excluded_pods` 44 | - Default: `[]`. 45 | - A list of pods to exclude (ie. treat them as non-prebuilt pods). 46 | 47 | Note: 48 | - By default, pods with empty sources (ie. pods with header files only) will be automatically excluded and they will be later integrated as normal. For now, we rely on the `source_files` patterns declared in podspec to heuristically detect empty-sources pods. 49 | - However, there are cases in which the `source_files` of a pod looks like non-empty sources (ex. `s.source_files = "**/*.{c,h,m,mm,cpp}"`) despite having header files only. For those cases, you need to manually add them to the `excluded_pods` option. 50 | 51 | ### `bitcode_enabled` 52 | - Default: `false`. 53 | - Enable bitcode generation when building frameworks 54 | 55 | ### `device_build_enabled` 56 | - Default: `false`. 57 | - Enable prebuilt frameworks to be used with devices. 58 | 59 | ### `xcframework` 60 | - Default: `false`. 61 | - Enable `xcframework` support. This is useful when prebuilding for multi architectures (for simulators & devices).\ 62 | NOTE: On ARM-based macs, please set this option to `true` as creating fat binaries with `lipo` no longer works on those machines. 63 | 64 | ### `disable_dsym` 65 | - Default: `false`. 66 | - Disable dSYM generation when prebuilding frameworks. 67 | 68 | ### `save_cache_validation_to` 69 | - Default: `nil`. 70 | - The path to save cache validation (missed/hit). Do nothing if not specified. 71 | 72 | ### `validate_prebuilt_settings` 73 | - Default: `nil`. 74 | - Validate build settings of the prebuilt frameworks. A framework that has incompatible build settings will be treated as a cache miss. If this option is not specified, only versions of the prebuilt pods are used to check for cache hit/miss. Below is a sample build settings validation: 75 | ```rb 76 | config_cocoapods_binary_cache( 77 | validate_prebuilt_settings: lambda { |target| 78 | settings = {} 79 | settings["MACH_O_TYPE"] = "mh_dylib" if must_be_dynamic_frameworks.include?(target) 80 | settings["SWIFT_VERSION"] = swift_version_for(target) 81 | settings 82 | } 83 | ) 84 | ``` 85 | 86 | ### `prebuild_code_gen` 87 | - Default: `nil`. 88 | - This option provide a hook to run code generation for prebuilding frameworks (in a prebuild job). A typical example is when you need to generate code using [R.swift](https://github.com/mac-cain13/R.swift). 89 | - If the code generation is independent of `Pods.xcodeproj`, it is recommended to move code generation prior to pod installation. In that case, you don't need this option. 90 | - Otherwise, use this option to trigger code generation. It will be triggered just before prebuilding frameworks.\ 91 | Do take note that if the code generation requires the `Pods.xcodeproj`, the project should correspond to the prebuilt sandbox (for ex. `_Prebuild/`, accessed via `installer.sandbox.root`), not the standard sandbox (`Pods`) 92 | ```rb 93 | config_cocoapods_binary_cache( 94 | prebuild_code_gen: lambda { |installer, targets_to_prebuild| 95 | `sh scripts/codegen_for_prebuild.sh` 96 | } 97 | ) 98 | ``` 99 | 100 | ### `silent_build` 101 | - Default: `false`. 102 | - Suppress build output. 103 | 104 | ### `xcodebuild_log_path` 105 | - Default: `nil`. 106 | - The `xcodebuild` log when prebuilding frameworks. 107 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/pod-binary/integration/source_installer.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Installer 3 | class PrebuiltSourceInstaller < PodSourceInstaller 4 | def initialize(*args, **kwargs) 5 | @source_installer = kwargs.delete(:source_installer) 6 | super(*args, **kwargs) 7 | end 8 | 9 | def prebuild_sandbox 10 | @prebuild_sandbox ||= Pod::PrebuildSandbox.from_standard_sandbox(sandbox) 11 | end 12 | 13 | def install! 14 | @source_installer.install! 15 | install_prebuilt_framework! 16 | end 17 | 18 | private 19 | 20 | def install_prebuilt_framework! 21 | return if !PodPrebuild.config.dev_pods_enabled? && sandbox.local?(name) 22 | 23 | # make a symlink to target folder 24 | # TODO (bang): Unify to 1 sandbox to optimize and avoid inconsistency 25 | # if spec used in multiple platforms, it may return multiple paths 26 | target_names = prebuild_sandbox.existed_target_names_for_pod_name(name) 27 | target_names.each do |name| 28 | real_file_folder = prebuild_sandbox.framework_folder_path_for_target_name(name) 29 | 30 | # If have only one platform, just place int the root folder of this pod. 31 | # If have multiple paths, we use a sperated folder to store different 32 | # platform frameworks. e.g. AFNetworking/AFNetworking-iOS/AFNetworking.framework 33 | target_folder = sandbox.pod_dir(self.name) 34 | target_folder += real_file_folder.basename if target_names.count > 1 35 | target_folder += PodPrebuild.config.prebuilt_path 36 | target_folder.rmtree if target_folder.exist? 37 | target_folder.mkpath 38 | 39 | walk(real_file_folder) do |child| 40 | source = child 41 | # only make symlink to file and `.framework` folder 42 | if child.directory? && [".framework", ".xcframework", ".dSYM"].include?(child.extname) 43 | if [".framework", ".xcframework"].include?(child.extname) 44 | mirror_with_symlink(source, real_file_folder, target_folder) 45 | end 46 | # Ignore dsym here to avoid cocoapods from adding install_dsym to buildphase-script 47 | # That can cause duplicated output files error in Xcode 11 (warning in Xcode 10) 48 | # We need more setup to support local debuging with prebuilt dSYM 49 | next false # Don't go deeper 50 | elsif child.file? 51 | mirror_with_symlink(source, real_file_folder, target_folder) 52 | next true 53 | else 54 | next true 55 | end 56 | end 57 | 58 | # symbol link copy resource for static framework 59 | metadata = PodPrebuild::Metadata.in_dir(real_file_folder) 60 | next unless metadata.static_framework? 61 | 62 | metadata.resources.each do |path| 63 | target_file_path = Pathname(path) 64 | .sub("${PODS_ROOT}", sandbox.root.to_path) 65 | .sub("${PODS_CONFIGURATION_BUILD_DIR}", sandbox.root.to_path) 66 | next if target_file_path.exist? 67 | 68 | real_file_path = real_file_folder + metadata.framework_name + File.basename(path) 69 | 70 | # TODO (thuyen): Fix https://github.com/grab/cocoapods-binary-cache/issues/45 71 | 72 | case File.extname(path) 73 | when ".xib" 74 | # https://github.com/grab/cocoapods-binary-cache/issues/7 75 | # When ".xib" files are compiled in a framework, it becomes ".nib" files 76 | # --> We need to correct the path extension 77 | real_file_path = real_file_path.sub_ext(".nib") 78 | target_file_path = target_file_path.sub(".xib", ".nib") 79 | when ".bundle" 80 | next if metadata.resource_bundles.include?(File.basename(path)) 81 | 82 | real_file_path = real_file_folder + File.basename(path) unless real_file_path.exist? 83 | end 84 | make_link(real_file_path, target_file_path) 85 | end 86 | end 87 | end 88 | 89 | def walk(path, &action) 90 | return unless path.exist? 91 | 92 | path.children.each do |child| 93 | result = action.call(child, &action) 94 | if child.directory? 95 | walk(child, &action) if result 96 | end 97 | end 98 | end 99 | 100 | def make_link(source, target) 101 | source = Pathname.new(source) 102 | target = Pathname.new(target) 103 | target.rmtree if target.exist? 104 | target.parent.mkpath unless target.parent.exist? 105 | relative_source = source.relative_path_from(target.parent) 106 | FileUtils.ln_sf(relative_source, target) 107 | end 108 | 109 | def mirror_with_symlink(source, basefolder, target_folder) 110 | make_link(source, target_folder + source.relative_path_from(basefolder)) 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/cocoapods-binary-cache/hooks/pre_install.rb: -------------------------------------------------------------------------------- 1 | module PodPrebuild 2 | class PreInstallHook 3 | include ObjectSpace 4 | 5 | attr_reader :installer_context, :podfile, :prebuild_sandbox, :standard_sandbox, :cache_validation 6 | 7 | def initialize(installer_context) 8 | @installer_context = installer_context 9 | @podfile = installer_context.podfile 10 | @pod_install_options = {} 11 | @prebuild_sandbox = nil 12 | @standard_sandbox = installer_context.sandbox 13 | @cache_validation = nil 14 | end 15 | 16 | def run 17 | return if @installer_context.sandbox.is_a?(Pod::PrebuildSandbox) 18 | 19 | log_section "🚀 Prebuild frameworks" 20 | ensure_valid_podfile 21 | save_installation_states 22 | create_prebuild_sandbox 23 | Pod::UI.title("Detect implicit dependencies") { detect_implicit_dependencies } 24 | Pod::UI.title("Validate prebuilt cache") { validate_cache } 25 | prebuild! if PodPrebuild.config.prebuild_job? 26 | 27 | PodPrebuild::Env.next_stage! 28 | prepare_for_integration 29 | log_section "🤖 Resume pod installation" 30 | require_relative "../pod-binary/integration" 31 | end 32 | 33 | private 34 | 35 | def save_installation_states 36 | save_pod_install_options 37 | end 38 | 39 | def save_pod_install_options 40 | # Fetch original installer (which is running this pre-install hook) options, 41 | # then pass them to our installer to perform update if needed 42 | # Looks like this is the most appropriate way to figure out that something should be updated 43 | @original_installer = ObjectSpace.each_object(Pod::Installer).first 44 | @pod_install_options[:update] = @original_installer.update 45 | @pod_install_options[:repo_update] = @original_installer.repo_update 46 | end 47 | 48 | def ensure_valid_podfile 49 | podfile.target_definition_list.each do |target_definition| 50 | next if target_definition.explicit_prebuilt_pod_names.empty? 51 | raise "cocoapods-binary-cache requires `use_frameworks!`" unless target_definition.uses_frameworks? 52 | end 53 | end 54 | 55 | def create_prebuild_sandbox 56 | @prebuild_sandbox = Pod::PrebuildSandbox.from_standard_sandbox(standard_sandbox) 57 | Pod::UI.message "Create prebuild sandbox at #{@prebuild_sandbox.root}" 58 | end 59 | 60 | def detect_implicit_dependencies 61 | @original_installer.resolve_dependencies 62 | all_specs = @original_installer.analysis_result.specifications 63 | pods_with_empty_source_files = all_specs 64 | .group_by { |spec| spec.name.split("/")[0] } 65 | .select { |_, specs| specs.all?(&:empty_source_files?) } 66 | .keys 67 | PodPrebuild.config.update_detected_excluded_pods!(pods_with_empty_source_files) 68 | PodPrebuild.config.update_detected_prebuilt_pod_names!(@original_installer.prebuilt_pod_names) 69 | Pod::UI.puts "Exclude pods with empty source files: #{pods_with_empty_source_files.to_a}" 70 | end 71 | 72 | def validate_cache 73 | prebuilt_lockfile = Pod::Lockfile.from_file(prebuild_sandbox.root + "Manifest.lock") 74 | @cache_validation = PodPrebuild::CacheValidator.new( 75 | podfile: podfile, 76 | pod_lockfile: installer_context.lockfile, 77 | prebuilt_lockfile: prebuilt_lockfile, 78 | validate_prebuilt_settings: PodPrebuild.config.validate_prebuilt_settings, 79 | generated_framework_path: prebuild_sandbox.generate_framework_path, 80 | sandbox_root: prebuild_sandbox.root, 81 | ignored_pods: PodPrebuild.config.excluded_pods, 82 | prebuilt_pod_names: PodPrebuild.config.prebuilt_pod_names 83 | ).validate 84 | path_to_save_cache_validation = PodPrebuild.config.save_cache_validation_to 85 | @cache_validation.update_to(path_to_save_cache_validation) unless path_to_save_cache_validation.nil? 86 | cache_validation.print_summary 87 | PodPrebuild.state.update(:cache_validation => cache_validation) 88 | end 89 | 90 | def prebuild! 91 | binary_installer = Pod::PrebuildInstaller.new( 92 | sandbox: prebuild_sandbox, 93 | podfile: podfile, 94 | lockfile: installer_context.lockfile, 95 | cache_validation: cache_validation 96 | ) 97 | binary_installer.update = @pod_install_options[:update] 98 | binary_installer.repo_update = @pod_install_options[:repo_update] 99 | 100 | Pod::UI.title("Prebuilding...") do 101 | binary_installer.clean_delta_file 102 | binary_installer.install! 103 | end 104 | end 105 | 106 | def prepare_for_integration 107 | # Remove local podspec of external sources so that it downloads sources correctly. 108 | # Otherwise, with incremental pod installation, CocoaPods downloads the sources 109 | # based on the `s.source` declaration in the podspecs which are sometimes incorrect. 110 | PodPrebuild.config.prebuilt_pod_names.each do |name| 111 | @standard_sandbox.remove_local_podspec(name) if @standard_sandbox.checkout_sources.key?(name) 112 | end 113 | end 114 | 115 | def log_section(message) 116 | Pod::UI.puts "-----------------------------------------" 117 | Pod::UI.puts message 118 | Pod::UI.puts "-----------------------------------------" 119 | end 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## master (to be 0.1.15) 4 | ### Enhancements 5 | NA 6 | 7 | ### Bug fixes 8 | - Fix duplicate resources for dynamic frameworks https://github.com/grab/cocoapods-binary-cache/issues/74. 9 | 10 | ## 0.1.14 11 | ### Enhancements 12 | - Add `xcodebuild_log_path` option. 13 | - CLI: Add options to visualize dependencies of dev pods only. 14 | 15 | ### Bug fixes 16 | - Fix invalid Info.plist https://github.com/grab/cocoapods-binary-cache/issues/69. 17 | - Fix incorrect CocoaPods cache caused by podspec alterations. 18 | 19 | ## 0.1.13 20 | ### Enhancements 21 | - Don't add Pods project of the prebuild sandbox to the workspace https://github.com/grab/cocoapods-binary-cache/issues/56. 22 | 23 | ### Bug fixes 24 | - Fix `readlink` https://github.com/grab/cocoapods-binary-cache/issues/49. Kudos to [Roger Oba](https://github.com/rogerluan) 25 | 26 | ## 0.1.12 27 | ### Enhancements 28 | - Speed up cache unzip by running them in parallel. 29 | - Remove the `still_download_sources` option. Instead, always download sources to avoid improper integration. 30 | - Add `xcframework` support (instead of creating fat framework with `lipo`) https://github.com/grab/cocoapods-binary-cache/issues/54. 31 | - dSYMs and BCSymbolMaps for `xcframework`. Kudos to [Kien Nguyen](https://github.com/kientux). 32 | 33 | ### Bug fixes 34 | - Fix resources integration (for ex. using `SwiftDate` as a static framework). 35 | 36 | ## 0.1.11 37 | ### Enhancements 38 | - Support local cache dir https://github.com/grab/cocoapods-binary-cache/issues/31. 39 | 40 | ### Bug fixes 41 | - Project path was not escaped in the `xcodebuild` command. 42 | - By default, should set `ONLY_ACTIVE_ARCH=NO` when building for devices. 43 | 44 | ## 0.1.10 45 | ### Enhancements 46 | - Add option `--no-fetch` to the `prebuild` command. 47 | 48 | ### Bug fixes 49 | - Sources of external-sources pods are not fetched properly in incremental pod installation. It should use the checkout options declared in Podfile instead. 50 | - Conflict definition of `xcodebuild` in this plugin and in `cocoapods-rome` causing prebuild failures https://github.com/grab/cocoapods-binary-cache/issues/36. 51 | 52 | ## 0.1.9 53 | ### Enhancements 54 | - Provide an option to keep sources downloading behavior, useful for maintaining the `preserve_paths` of the podspecs. 55 | 56 | ### Bug fixes 57 | - Handle git failures properly (throwing errors if any). 58 | 59 | ## 0.1.8 60 | ### Enhancements 61 | - Prebuild multiple targets concurently to ultilize build parallelism. 62 | 63 | ### Bug fixes 64 | - Abnormal integration when some prebuilt pods are detected as unchanged in the integration step https://github.com/grab/cocoapods-binary-cache/issues/21. 65 | - Wrong merge of `Info.plist` when prebuilding for simulators and devices https://github.com/grab/cocoapods-binary-cache/issues/25. 66 | - Cache validation when subspecs have empty source but the parent spec does have sources (https://github.com/grab/cocoapods-binary-cache/pull/26). Kudos to Christian Nadeau. 67 | 68 | --- 69 | ## 0.1.7 70 | ### Enhancements 71 | - Change the prebuilt path from `Pods/A/A.framework` to `Pods/A/_Prebuilt/A.framework`. No config change is required. 72 | - Show warnings if there exists an inapplicable option in `config_cocoapods_binary_cache`. 73 | - Deprecate configs (`cache_repo`, `cache_path`, `prebuild_path`...) in `PodBinaryCacheConfig.json`. Rather, declare them in `config_cocoapods_binary_cache`. Refer to [Configure cocoapods-binary-cache](/docs/configure_cocoapods_binary_cache.md) for more details. 74 | - Multi-cache-repo support https://github.com/grab/cocoapods-binary-cache/issues/18. 75 | 76 | ### Bug fixes 77 | None 78 | 79 | --- 80 | ## 0.1.6 81 | ### Enhancements 82 | - Remove the `prebuild_all_vendor_pods` option. Specify this in the CLI instead: `pod binary prebuild --all` 83 | - Allow prebuilding specific targets: `pod binary prebuild --targets=A,B,C` 84 | - Provide an option to run code generation for prebuild. Refer to the [`prebuild_code_gen` option](/docs/configure_cocoapods_binary_cache.md). 85 | 86 | ### Bug fixes 87 | - Exclude files ignored by git when calculating checksums for development pods. 88 | - Exception thrown when `Podfile.lock` is not present https://github.com/grab/cocoapods-binary-cache/issues/20. 89 | 90 | --- 91 | ## 0.1.5 92 | ### Enhancements 93 | - Enable device support when prebuild frameworks https://github.com/grab/cocoapods-binary-cache/issues/19. Refer to the [`device_build_enabled` option](/docs/configure_cocoapods_binary_cache.md). 94 | 95 | ### Bug fixes 96 | None 97 | 98 | --- 99 | ## 0.1.4 100 | ### Enhancements 101 | - Allow specifying the prebuild sandbox path (default as `_Prebuild`, previously as `Pods/_Prebuild`). 102 | - Add diagnosis action to spot unintegrated prebuilt frameworks. 103 | - Preparation work for development pods supported. 104 | 105 | ### Bug fixes 106 | - Missing `push` command in the CLI: `pod binary push` 107 | - Exception thrown when requirements of a pod are not specified https://github.com/grab/cocoapods-binary-cache/pull/17. Kudos to [Mack Hasz](https://github.com/lazyvar). 108 | 109 | --- 110 | ## 0.1.3 111 | ### Enhancements 112 | - No need to specify `prebuild_job` in the `config_cocoapods_binary_cache` in a prebuild job. 113 | 114 | ### Bug fixes 115 | None 116 | 117 | --- 118 | ## 0.1.2 119 | ### Enhancements 120 | None 121 | 122 | ### Bug fixes 123 | - Corrupted cache zip/unzip if there are symlinks inside the framework. 124 | 125 | --- 126 | ## 0.1.1 127 | ### Enhancements 128 | - Enhance cache validation mechanism. 129 | - Update DSL: use `config_cocoapods_binary_cache` for cocoapods-binary-cache related configs. 130 | - Validate build settings (for ex. changing a framework from `dynamic` to `static` is considered cache-missed). 131 | - Auto-exclude frameworks with no source (for ex. originally distributed as prebuilt). 132 | - Detect dependencies of pods explicitly declared as prebuilt and treat them as prebuilt. 133 | 134 | ### Bug fixes 135 | - Various fixes for static frameworks: 136 | - Resources bundle not integrated properly. 137 | - XIB resources not integrated properly https://github.com/grab/cocoapods-binary-cache/issues/7. 138 | - Various fixes for cache validation with subspecs. 139 | 140 | --- 141 | ## 0.0.1 - 0.0.5 142 | - Initially released on 2019-12-20 🎉 (0.0.1). 143 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | cocoapods-binary-cache (0.1.14) 5 | cocoapods (>= 1.5.0) 6 | fourflusher (~> 2.0) 7 | parallel (~> 1.0) 8 | rgl (~> 0.5.6) 9 | xcpretty (~> 0.3.0) 10 | 11 | GEM 12 | remote: https://rubygems.org/ 13 | specs: 14 | CFPropertyList (3.0.2) 15 | activesupport (4.2.11.3) 16 | i18n (~> 0.7) 17 | minitest (~> 5.1) 18 | thread_safe (~> 0.3, >= 0.3.4) 19 | tzinfo (~> 1.1) 20 | addressable (2.7.0) 21 | public_suffix (>= 2.0.2, < 5.0) 22 | algoliasearch (1.27.4) 23 | httpclient (~> 2.8, >= 2.8.3) 24 | json (>= 1.5.1) 25 | ast (2.4.0) 26 | atomos (0.1.3) 27 | bacon (1.2.0) 28 | claide (1.0.3) 29 | claide-plugins (0.9.2) 30 | cork 31 | nap 32 | open4 (~> 1.3) 33 | cocoapods (1.9.3) 34 | activesupport (>= 4.0.2, < 5) 35 | claide (>= 1.0.2, < 2.0) 36 | cocoapods-core (= 1.9.3) 37 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 38 | cocoapods-downloader (>= 1.2.2, < 2.0) 39 | cocoapods-plugins (>= 1.0.0, < 2.0) 40 | cocoapods-search (>= 1.0.0, < 2.0) 41 | cocoapods-stats (>= 1.0.0, < 2.0) 42 | cocoapods-trunk (>= 1.4.0, < 2.0) 43 | cocoapods-try (>= 1.1.0, < 2.0) 44 | colored2 (~> 3.1) 45 | escape (~> 0.0.4) 46 | fourflusher (>= 2.3.0, < 3.0) 47 | gh_inspector (~> 1.0) 48 | molinillo (~> 0.6.6) 49 | nap (~> 1.0) 50 | ruby-macho (~> 1.4) 51 | xcodeproj (>= 1.14.0, < 2.0) 52 | cocoapods-core (1.9.3) 53 | activesupport (>= 4.0.2, < 6) 54 | algoliasearch (~> 1.0) 55 | concurrent-ruby (~> 1.1) 56 | fuzzy_match (~> 2.0.4) 57 | nap (~> 1.0) 58 | netrc (~> 0.11) 59 | typhoeus (~> 1.0) 60 | cocoapods-deintegrate (1.0.4) 61 | cocoapods-downloader (1.4.0) 62 | cocoapods-plugins (1.0.0) 63 | nap 64 | cocoapods-search (1.0.0) 65 | cocoapods-stats (1.1.0) 66 | cocoapods-trunk (1.5.0) 67 | nap (>= 0.8, < 2.0) 68 | netrc (~> 0.11) 69 | cocoapods-try (1.2.0) 70 | coderay (1.1.3) 71 | colored2 (3.1.2) 72 | concurrent-ruby (1.1.7) 73 | cork (0.3.0) 74 | colored2 (~> 3.1) 75 | danger (8.0.5) 76 | claide (~> 1.0) 77 | claide-plugins (>= 0.9.2) 78 | colored2 (~> 3.1) 79 | cork (~> 0.1) 80 | faraday (>= 0.9.0, < 2.0) 81 | faraday-http-cache (~> 2.0) 82 | git (~> 1.7) 83 | kramdown (~> 2.3) 84 | kramdown-parser-gfm (~> 1.0) 85 | no_proxy_fix 86 | octokit (~> 4.7) 87 | terminal-table (~> 1) 88 | danger-rubocop (0.9.0) 89 | danger 90 | rubocop (~> 0.83) 91 | diff-lcs (1.3) 92 | escape (0.0.4) 93 | ethon (0.12.0) 94 | ffi (>= 1.3.0) 95 | faraday (1.0.1) 96 | multipart-post (>= 1.2, < 3) 97 | faraday-http-cache (2.2.0) 98 | faraday (>= 0.8) 99 | ffi (1.13.1) 100 | fourflusher (2.3.1) 101 | fuzzy_match (2.0.4) 102 | generator (0.0.1) 103 | gh_inspector (1.1.3) 104 | git (1.7.0) 105 | rchardet (~> 1.8) 106 | httpclient (2.8.3) 107 | i18n (0.9.5) 108 | concurrent-ruby (~> 1.0) 109 | interception (0.5) 110 | json (2.3.1) 111 | kramdown (2.3.0) 112 | rexml 113 | kramdown-parser-gfm (1.1.0) 114 | kramdown (~> 2.0) 115 | lazy_priority_queue (0.1.1) 116 | method_source (0.9.2) 117 | minitest (5.14.2) 118 | mocha (1.10.2) 119 | mocha-on-bacon (0.2.3) 120 | mocha (>= 0.13.0) 121 | molinillo (0.6.6) 122 | multipart-post (2.1.1) 123 | nanaimo (0.3.0) 124 | nap (1.1.0) 125 | netrc (0.11.0) 126 | no_proxy_fix (0.1.2) 127 | octokit (4.18.0) 128 | faraday (>= 0.9) 129 | sawyer (~> 0.8.0, >= 0.5.3) 130 | open4 (1.3.4) 131 | parallel (1.19.1) 132 | parser (2.7.1.3) 133 | ast (~> 2.4.0) 134 | prettybacon (0.0.2) 135 | bacon (~> 1.2) 136 | pry (0.12.2) 137 | coderay (~> 1.1.0) 138 | method_source (~> 0.9.0) 139 | pry-nav (0.3.0) 140 | pry (>= 0.9.10, < 0.13.0) 141 | pry-rescue (1.5.1) 142 | interception (>= 0.5) 143 | pry (>= 0.12.0) 144 | public_suffix (4.0.6) 145 | rainbow (3.0.0) 146 | rake (10.5.0) 147 | rchardet (1.8.0) 148 | rexml (3.2.4) 149 | rgl (0.5.7) 150 | lazy_priority_queue (~> 0.1.0) 151 | stream (~> 0.5.3) 152 | rouge (2.0.7) 153 | rspec (3.9.0) 154 | rspec-core (~> 3.9.0) 155 | rspec-expectations (~> 3.9.0) 156 | rspec-mocks (~> 3.9.0) 157 | rspec-core (3.9.1) 158 | rspec-support (~> 3.9.1) 159 | rspec-expectations (3.9.1) 160 | diff-lcs (>= 1.2.0, < 2.0) 161 | rspec-support (~> 3.9.0) 162 | rspec-mocks (3.9.1) 163 | diff-lcs (>= 1.2.0, < 2.0) 164 | rspec-support (~> 3.9.0) 165 | rspec-support (3.9.2) 166 | rubocop (0.84.0) 167 | parallel (~> 1.10) 168 | parser (>= 2.7.0.1) 169 | rainbow (>= 2.2.2, < 4.0) 170 | rexml 171 | rubocop-ast (>= 0.0.3) 172 | ruby-progressbar (~> 1.7) 173 | unicode-display_width (>= 1.4.0, < 2.0) 174 | rubocop-ast (0.0.3) 175 | parser (>= 2.7.0.1) 176 | ruby-macho (1.4.0) 177 | ruby-progressbar (1.10.1) 178 | sawyer (0.8.2) 179 | addressable (>= 2.3.5) 180 | faraday (> 0.8, < 2.0) 181 | stream (0.5.3) 182 | generator 183 | terminal-table (1.8.0) 184 | unicode-display_width (~> 1.1, >= 1.1.1) 185 | thread_safe (0.3.6) 186 | typhoeus (1.4.0) 187 | ethon (>= 0.9.0) 188 | tzinfo (1.2.7) 189 | thread_safe (~> 0.1) 190 | unicode-display_width (1.7.0) 191 | xcodeproj (1.18.0) 192 | CFPropertyList (>= 2.3.3, < 4.0) 193 | atomos (~> 0.1.3) 194 | claide (>= 1.0.2, < 2.0) 195 | colored2 (~> 3.1) 196 | nanaimo (~> 0.3.0) 197 | xcpretty (0.3.0) 198 | rouge (~> 2.0.7) 199 | 200 | PLATFORMS 201 | ruby 202 | 203 | DEPENDENCIES 204 | bacon 205 | bundler (>= 1.3) 206 | cocoapods 207 | cocoapods-binary-cache! 208 | danger 209 | danger-rubocop 210 | mocha 211 | mocha-on-bacon 212 | prettybacon 213 | pry 214 | pry-nav 215 | pry-rescue 216 | rake (~> 10.0) 217 | rspec 218 | rubocop (= 0.84.0) 219 | xcpretty 220 | 221 | BUNDLED WITH 222 | 2.1.4 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CocoaPods binary cache 2 | 3 | [![Test](https://img.shields.io/github/workflow/status/grab/cocoapods-binary-cache/test)](https://img.shields.io/github/workflow/status/grab/cocoapods-binary-cache/test) 4 | [![License](https://img.shields.io/badge/license-MIT-green.svg?style=flat&color=blue)](https://github.com/grab/cocoapods-binary-cache/blob/master/LICENSE) 5 | [![Gem](https://img.shields.io/gem/v/cocoapods-binary-cache.svg?style=flat&color=blue)](https://rubygems.org/gems/cocoapods-binary-cache) 6 | 7 | A plugin that helps to reduce the build time of Xcode projects which use CocoaPods by prebuilding pod frameworks and cache them in a remote repository to share across multiple machines. 8 | 9 | ## Installation 10 | 11 | Requirements 12 | 13 | - Ruby: >= 2.4 14 | - CocoaPods: >= 1.5.0 15 | 16 | ### Via [Bundler](https://bundler.io/) 17 | 18 | Add the gem `cocoapods-binary-cache` to the `Gemfile` of your project. 19 | 20 | ```rb 21 | gem "cocoapods-binary-cache", :git => "https://github.com/grab/cocoapods-binary-cache.git", :tag => "0.1.11" 22 | ``` 23 | 24 | Then, run `bundle install` to install the added gem. 25 | 26 | In case you're not familiar with [`bundler`](https://bundler.io/), take a look at [Learn how to set it up here](https://www.mokacoding.com/blog/ruby-for-ios-developers-bundler/). 27 | 28 | ### Via [RubyGems](https://rubygems.org/) 29 | 30 | ```sh 31 | $ gem install cocoapods-binary-cache 32 | ``` 33 | 34 | ## How it works 35 | 36 | Check out the [documentation on how it works](/docs/how_it_works.md) for more information. 37 | 38 | ## Usage 39 | 40 | ### 1. Configure cache repo 41 | 42 | First of all, create a git repo that will be used as a storage of your prebuilt frameworks. Make sure this git repo is accessible via `git clone` and `git fetch`. Specify this cache repo in the following section. 43 | 44 | ### 2. Configure Podfile 45 | 46 | **2.1. Load the `cocoapods-binary-cache` plugin.** 47 | 48 | Add the following line at the beginning of Podfile: 49 | 50 | ```rb 51 | plugin "cocoapods-binary-cache" 52 | ``` 53 | 54 | **2.2. Configure `cocoapods-binary-cache`** 55 | 56 | ```rb 57 | config_cocoapods_binary_cache( 58 | cache_repo: { 59 | "default" => { 60 | "remote" => "git@cache_repo.git", 61 | "local" => "~/.cocoapods-binary-cache/prebuilt-frameworks" 62 | } 63 | }, 64 | prebuild_config: "Debug" 65 | ) 66 | ``` 67 | For details about options to use with the `config_cocoapods_binary_cache` function, check out [our guidelines on how to configure `cocoapods-binary-cache`](/docs/configure_cocoapods_binary_cache.md). 68 | 69 | **2.3. Declare pods as prebuilt pods** 70 | 71 | To declare a pod as a prebuilt pod (sometimes referred to as *binary pod*), add the option `:binary => true` as follows: 72 | ```rb 73 | pod "Alamofire", "5.2.1", :binary => true 74 | ``` 75 | 76 | NOTE: 77 | 78 | - Dependencies of a prebuilt pod will be automatically treated as prebuilt pods.\ 79 | For example, if `RxCocoa` is declared as a prebuilt pod using the `:binary => true` option, then `RxSwift`, one of its dependencies, is also treated as a prebuilt pod. 80 | 81 | ### 3. CLI 82 | 83 | We provided some command line interfaces (CLI): 84 | 85 | - Fetch from cache repo 86 | ```sh 87 | $ bundle exec pod binary fetch 88 | ``` 89 | - Prebuild binary pods 90 | ```sh 91 | $ bundle exec pod binary prebuild [--push] 92 | ``` 93 | - Push the prebuilt pods to the cache repo 94 | ```sh 95 | $ bundle exec pod binary push 96 | ``` 97 | 98 | For each command, you can run with option `--help` for more details about how to use each: 99 | ```sh 100 | $ bundle exec pod binary fetch --help 101 | ``` 102 | 103 | ### 4. A trivial workflow 104 | 105 | A trivial workflow when using this plugin is to fetch from cache repo, followed by a pod installation, as follows: 106 | 107 | ```sh 108 | $ bundle exec pod binary fetch 109 | $ bundle exec pod install 110 | ``` 111 | 112 | For other usages, check out the [best practices docs](/docs/best_practices.md). 113 | 114 | ## Benchmark 115 | 116 | We created a project to benchmark how much of the improvements we gain from this plugin. The demo project is using the following pods: 117 | 118 | ``` 119 | AFNetworking 120 | SDWebImage 121 | Alamofire 122 | MBProgressHUD 123 | Masonry 124 | SwiftyJSON 125 | SVProgressHUD 126 | MJRefresh 127 | CocoaLumberjack 128 | Realm 129 | SnapKit 130 | Kingfisher 131 | ``` 132 | 133 | Below is the result we recorded: 134 | 135 | 136 | 137 | Hardware specs of the above benchmark: 138 | ``` 139 | MacBook Pro (15-inch, 2018) 140 | Mac OS 10.14.6 141 | Processor 2.6 GHz Intel Core i7 142 | Memory 16 GB 2400 MHz DDR4 143 | ``` 144 | 145 | You can also try it out on your local: 146 | ```sh 147 | $ cd PodBinaryCacheExample 148 | $ sh BuildBenchMark.sh 149 | ``` 150 | 151 | In our real project with around 15% of swift/ObjC code from vendor pods. After applying this technique, we notice a reduction of around 10% in build time. 152 | 153 | 154 | ## Known issues and roadmap 155 | 156 | ### Exporting IPA with Bitcode 157 | - When exporting an IPA with Bitcode, remember to disable the _rebuild from bitcode_ option. Refer to https://github.com/grab/cocoapods-binary-cache/issues/24. 158 | 159 | ### Pods with headers only 160 | - By default, pods with empty sources (ie. pods with header files only) will be automatically excluded and they will be later integrated as normal. For now, we rely on the `source_files` patterns declared in podspec to heuristically detect empty-sources pods. 161 | - However, there are cases in which the `source_files` of a pod looks like non-empty sources (ex. `s.source_files = "**/*.{c,h,m,mm,cpp}"`) despite having header files only. For those cases, you need to manually add them to the `excluded_pods` option. 162 | 163 | ## Best practices 164 | 165 | Check out our [Best practices](/docs/best_practices.md) for for information. 166 | 167 | ## Troubleshooting 168 | 169 | Check out our [Troubleshooting guidelines](/docs/troubleshooting_guidelines.md) for more information. 170 | 171 | ## Contribution 172 | 173 | Check out [CONTRIBUTING.md](CONTRIBUTING.md) for more information on hw to contribute to this repo. 174 | 175 | ## License 176 | 177 | The cocoapods-binary-cache plugin is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 178 | It uses [cocoapods-rome](https://github.com/CocoaPods/Rome) and [cocoapods-binary](https://github.com/leavez/cocoapods-binary) internally, which are also under MIT License. 179 | -------------------------------------------------------------------------------- /lib/command/config.rb: -------------------------------------------------------------------------------- 1 | require_relative "../cocoapods-binary-cache/helper/json" 2 | 3 | module PodPrebuild 4 | def self.config 5 | PodPrebuild::Config.instance 6 | end 7 | 8 | class Config # rubocop:disable Metrics/ClassLength 9 | attr_accessor :dsl_config, :cli_config 10 | 11 | def initialize(path) 12 | @deprecated_config = File.exist?(path) ? PodPrebuild::JSONFile.new(path).data : {} 13 | @dsl_config = {} 14 | @cli_config = {} 15 | @detected_config = {} 16 | end 17 | 18 | def self.instance 19 | @instance ||= new("PodBinaryCacheConfig.json") 20 | end 21 | 22 | def reset! 23 | @deprecated_config = {} 24 | @dsl_config = {} 25 | @cli_config = {} 26 | end 27 | 28 | def cache_repo 29 | @cache_repo ||= cache_repo_config["remote"] 30 | end 31 | 32 | def local_cache? 33 | cache_repo.nil? 34 | end 35 | 36 | def cache_path 37 | @cache_path ||= File.expand_path(cache_repo_config["local"]) 38 | end 39 | 40 | def prebuild_sandbox_path 41 | @dsl_config[:prebuild_sandbox_path] || @deprecated_config["prebuild_path"] || "_Prebuild" 42 | end 43 | 44 | def prebuild_delta_path 45 | @dsl_config[:prebuild_delta_path] || @deprecated_config["prebuild_delta_path"] || "_Prebuild_delta/changes.json" 46 | end 47 | 48 | def manifest_path(in_cache: false) 49 | root_dir(in_cache) + "/Manifest.lock" 50 | end 51 | 52 | def root_dir(in_cache) 53 | in_cache ? cache_path : prebuild_sandbox_path 54 | end 55 | 56 | def generated_frameworks_dir(in_cache: false) 57 | root_dir(in_cache) + "/GeneratedFrameworks" 58 | end 59 | 60 | def prebuilt_path(path: nil) 61 | p = Pathname.new(path.nil? ? "_Prebuilt" : "_Prebuilt/#{path}") 62 | p = p.sub_ext(".xcframework") if xcframework? && p.extname == ".framework" 63 | p.to_s 64 | end 65 | 66 | def validate_dsl_config 67 | inapplicable_options = @dsl_config.keys - applicable_dsl_config 68 | return if inapplicable_options.empty? 69 | 70 | message = <<~HEREDOC 71 | [WARNING] The following options (in `config_cocoapods_binary_cache`) are not correct: #{inapplicable_options}. 72 | Available options: #{applicable_dsl_config}. 73 | Check out the following doc for more details 74 | https://github.com/grab/cocoapods-binary-cache/blob/master/docs/configure_cocoapods_binary_cache.md 75 | HEREDOC 76 | 77 | Pod::UI.puts message.yellow 78 | end 79 | 80 | def prebuild_config 81 | @cli_config[:prebuild_config] || @dsl_config[:prebuild_config] || "Debug" 82 | end 83 | 84 | def prebuild_job? 85 | @cli_config[:prebuild_job] || @dsl_config[:prebuild_job] 86 | end 87 | 88 | def prebuild_all_pods? 89 | @cli_config[:prebuild_all_pods] || @dsl_config[:prebuild_all_pods] 90 | end 91 | 92 | def excluded_pods 93 | ((@dsl_config[:excluded_pods] || Set.new) + (@detected_config[:excluded_pods] || Set.new)).to_set 94 | end 95 | 96 | def dev_pods_enabled? 97 | @dsl_config[:dev_pods_enabled] 98 | end 99 | 100 | def bitcode_enabled? 101 | @dsl_config[:bitcode_enabled] 102 | end 103 | 104 | def device_build_enabled? 105 | @dsl_config[:device_build_enabled] 106 | end 107 | 108 | def xcframework? 109 | @dsl_config[:xcframework] 110 | end 111 | 112 | def disable_dsym? 113 | @dsl_config[:disable_dsym] 114 | end 115 | 116 | def dont_remove_source_code? 117 | @dsl_config[:dont_remove_source_code] 118 | end 119 | 120 | def xcodebuild_log_path 121 | @dsl_config[:xcodebuild_log_path] 122 | end 123 | 124 | def build_args 125 | @dsl_config[:build_args] 126 | end 127 | 128 | def save_cache_validation_to 129 | @dsl_config[:save_cache_validation_to] 130 | end 131 | 132 | def validate_prebuilt_settings 133 | @dsl_config[:validate_prebuilt_settings] 134 | end 135 | 136 | def prebuild_code_gen 137 | @dsl_config[:prebuild_code_gen] 138 | end 139 | 140 | def strict_diagnosis? 141 | @dsl_config[:strict_diagnosis] 142 | end 143 | 144 | def silent_build? 145 | @dsl_config[:silent_build] 146 | end 147 | 148 | def targets_to_prebuild_from_cli 149 | @cli_config[:prebuild_targets] || [] 150 | end 151 | 152 | def update_detected_prebuilt_pod_names!(value) 153 | @detected_config[:prebuilt_pod_names] = value 154 | end 155 | 156 | def update_detected_excluded_pods!(value) 157 | @detected_config[:excluded_pods] = value 158 | end 159 | 160 | def prebuilt_pod_names 161 | @detected_config[:prebuilt_pod_names] || Set.new 162 | end 163 | 164 | def tracked_prebuilt_pod_names 165 | prebuilt_pod_names - excluded_pods 166 | end 167 | 168 | private 169 | 170 | def applicable_dsl_config 171 | [ 172 | :cache_repo, 173 | :prebuild_sandbox_path, 174 | :prebuild_delta_path, 175 | :prebuild_config, 176 | :prebuild_job, 177 | :prebuild_all_pods, 178 | :excluded_pods, 179 | :dev_pods_enabled, 180 | :bitcode_enabled, 181 | :device_build_enabled, 182 | :xcframework, 183 | :disable_dsym, 184 | :dont_remove_source_code, 185 | :xcodebuild_log_path, 186 | :build_args, 187 | :save_cache_validation_to, 188 | :validate_prebuilt_settings, 189 | :prebuild_code_gen, 190 | :strict_diagnosis, 191 | :silent_build 192 | ] 193 | end 194 | 195 | def cache_repo_config 196 | @cache_repo_config ||= begin 197 | repo = @cli_config[:repo] || "default" 198 | config_ = @dsl_config[:cache_repo] || {} 199 | if config_[repo].nil? 200 | message = <<~HEREDOC 201 | [Deprecated] Configs in `PodBinaryCacheConfig.json` are deprecated. 202 | Declare option `cache_repo` in `config_cocoapods_binary_cache` instead. 203 | Check out the following doc for more details 204 | https://github.com/grab/cocoapods-binary-cache/blob/master/docs/configure_cocoapods_binary_cache.md 205 | HEREDOC 206 | Pod::UI.puts message.yellow 207 | end 208 | config_[repo] || { 209 | "remote" => @deprecated_config["cache_repo"] || @deprecated_config["prebuilt_cache_repo"], 210 | "local" => @deprecated_config["cache_path"] || "~/.cocoapods-binary-cache/prebuilt-frameworks" 211 | } 212 | end 213 | end 214 | end 215 | end 216 | --------------------------------------------------------------------------------