├── 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 | [](https://img.shields.io/github/workflow/status/grab/cocoapods-binary-cache/test)
4 | [](https://github.com/grab/cocoapods-binary-cache/blob/master/LICENSE)
5 | [](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 |
--------------------------------------------------------------------------------