├── .gitignore ├── .travis.yml ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Makefile ├── README.md ├── Rakefile ├── bin └── cocoacache ├── cocoacache.gemspec ├── codecov.yml ├── lib ├── cocoacache.rb └── cocoacache │ ├── command.rb │ ├── core.rb │ └── version.rb └── test ├── fixtures ├── OriginSpecs.zip └── Podfile.lock ├── helper.rb ├── test_command.rb └── test_core.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | coverage/ 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 2.3.1 3 | install: bundle install --jobs=3 --retry=3 4 | script: rake 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 6 | 7 | gem "colored2" 8 | gem "codecov" 9 | gem "rspec-mocks" 10 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | codecov (0.1.14) 5 | json 6 | simplecov 7 | url 8 | colored2 (3.1.2) 9 | diff-lcs (1.3) 10 | docile (1.3.2) 11 | json (2.2.0) 12 | minitest (5.11.3) 13 | rake (12.3.2) 14 | rspec-mocks (3.8.1) 15 | diff-lcs (>= 1.2.0, < 2.0) 16 | rspec-support (~> 3.8.0) 17 | rspec-support (3.8.2) 18 | simplecov (0.16.1) 19 | docile (~> 1.1) 20 | json (>= 1.8, < 3) 21 | simplecov-html (~> 0.10.0) 22 | simplecov-html (0.10.2) 23 | url (0.3.2) 24 | 25 | PLATFORMS 26 | ruby 27 | 28 | DEPENDENCIES 29 | codecov 30 | colored2 31 | minitest 32 | rake 33 | rspec-mocks 34 | 35 | BUNDLED WITH 36 | 2.0.1 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Suyeol Jeon (xoul.kr) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | @all: clean build uninstall install 2 | 3 | clean: 4 | rm -f cocoacache-*.gem 5 | 6 | build: 7 | gem build cocoacache.gemspec 8 | 9 | uninstall: 10 | gem uninstall cocoacache --all --executables 2>/dev/null 11 | 12 | install: 13 | gem install cocoacache-*.gem 14 | 15 | push: clean build 16 | gem push cocoacache-*.gem 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CocoaCache 2 | 3 | [![Gem](https://img.shields.io/gem/v/cocoacache.svg)](https://rubygems.org/gems/cocoacache) 4 | [![Build Status](https://travis-ci.org/StyleShare/CocoaCache.svg?branch=master)](https://travis-ci.org/StyleShare/CocoaCache) 5 | [![Codecov](https://img.shields.io/codecov/c/github/StyleShare/CocoaCache.svg)](https://codecov.io/gh/StyleShare/CocoaCache) 6 | 7 | Partial CocoaPods Specs cache for the faster CI build. 8 | 9 | ## Background 10 | 11 | It takes several minutes updating CocoaPods Specs repository while building a project on a CI server. In order to prevent from updating Specs repository, you have to cache the entire Specs repository located in `~/.coccoapods/repos/master`. But this is too large to cache in the CI server. CocoaCache helps to cache specific Pod specs to prevent from updating the Specs repository. 12 | 13 | ## Concepts 14 | 15 | ![CocoaCache](https://user-images.githubusercontent.com/931655/60092486-1aa3dd00-9782-11e9-9afe-6e4cb8933e9e.png) 16 | 17 | ### Saving Cache (Previous Build) 18 | 19 | 1. Find Pods from `Podfile.lock` to cache. 20 | 2. Copy the specific Specs from the origin Specs directory to `./Specs`. 21 | ``` 22 | $HOME/.cocoapods/repos/master/Specs/7/7/7/ReactorKit -> ./Specs/7/7/7/ReactorKit 23 | ``` 24 | 3. Cache `./Specs` to the CI server. 25 | 26 | ### Restoring Cache (Next Build) 27 | 28 | 1. Restore the `./Spec` directory from the CI server. 29 | 2. Find Pods from `Podfile.lock` to restore. 30 | 3. Copy the cached Specs back to the origin Specs directory from `./Specs`. 31 | ``` 32 | ./Specs/7/7/7/ReactorKit -> $HOME/.cocoapods/repos/master/Specs/7/7/7/ReactorKit 33 | ``` 34 | 35 | ## Installation 36 | 37 | ```console 38 | $ gem install cocoacache 39 | ``` 40 | 41 | ## Usage 42 | 43 | ``` 44 | Usage: cocoacache COMMAND [options] 45 | 46 | Commands: 47 | save Copy specs from the origin Specs to cache directory. 48 | restore Copy the cached Specs back to the origin directory. 49 | 50 | Options: 51 | --origin The origin Specs directory. Defaults to $HOME/.cocoapods/repos/master/Specs 52 | --cache Where to cache the Specs. Defaults to ~/Specs 53 | --podfile The path for the Podfile.lock. Defaults to ~/Podfile.lock 54 | ``` 55 | 56 | ## Configuration 57 | 58 | ### Travis 59 | 60 | **`.travis.yml`** 61 | 62 | ```yml 63 | cache: 64 | - directories: 65 | - Specs 66 | 67 | install: 68 | - gem install cocoacache && cocoacache restore 69 | - pod install 70 | 71 | before_cache: 72 | - cocoacache save 73 | ``` 74 | 75 | ## License 76 | 77 | **CocoaCache** is under MIT license. See the [LICENSE] file for more info. 78 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/testtask" 2 | 3 | Rake::TestTask.new do |t| 4 | t.libs << "test" 5 | end 6 | 7 | desc "Run tests" 8 | task :default => :test 9 | -------------------------------------------------------------------------------- /bin/cocoacache: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "cocoacache" 4 | 5 | core_factory = CocoaCache::Core 6 | CocoaCache::Command.run(core_factory, ARGV) 7 | -------------------------------------------------------------------------------- /cocoacache.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/cocoacache/version" 2 | require "date" 3 | 4 | Gem::Specification.new do |s| 5 | s.name = "cocoacache" 6 | s.version = CocoaCache::VERSION 7 | s.date = Date.today 8 | s.summary = "Partial CocoaPods Specs cache for the faster CI build" 9 | s.description = "Partial CocoaPods Specs cache for the faster CI build. It "\ 10 | "helps to cache specific Pod specs to prevent from updating"\ 11 | "the Specs repository." 12 | s.authors = ["Suyeol Jeon"] 13 | s.email = "devxoul@gmail.com" 14 | s.files = ["lib/cocoacache.rb"] 15 | s.homepage = "https://github.com/devxoul/CocoaCache" 16 | s.license = "MIT" 17 | 18 | s.files = Dir["lib/**/*.rb"] + %w{ bin/cocoacache README.md LICENSE } 19 | 20 | s.executables = %w{ cocoacache } 21 | s.require_paths = %w{ lib } 22 | 23 | s.add_runtime_dependency "colored2", "~> 3.1" 24 | 25 | s.required_ruby_version = ">= 2.2.2" 26 | end 27 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - bin 3 | - test 4 | 5 | coverage: 6 | status: 7 | project: off 8 | patch: off 9 | -------------------------------------------------------------------------------- /lib/cocoacache.rb: -------------------------------------------------------------------------------- 1 | module CocoaCache 2 | require "cocoacache/version" 3 | 4 | autoload :Command, "cocoacache/command" 5 | autoload :Core, "cocoacache/core" 6 | end 7 | -------------------------------------------------------------------------------- /lib/cocoacache/command.rb: -------------------------------------------------------------------------------- 1 | require "colored2" 2 | 3 | module CocoaCache 4 | class Command 5 | def self.run(core_factory, argv) 6 | case argv[0] 7 | when "save" 8 | core = self.get_core(core_factory, argv) 9 | core.save() 10 | 11 | when "restore" 12 | core = self.get_core(core_factory, argv) 13 | core.restore() 14 | 15 | when "--version" 16 | puts CocoaCache::VERSION 17 | 18 | else 19 | self.help 20 | end 21 | end 22 | 23 | def self.get_core(factory, argv) 24 | return factory.new( 25 | :origin_specs_dir => ( 26 | self.parse_argument(argv, "--origin") \ 27 | or "$HOME/.cocoapods/repos/master/Specs" 28 | ), 29 | :cache_specs_dir => ( 30 | self.parse_argument(argv, "--cache") \ 31 | or "Specs" 32 | ), 33 | :podfile_path => ( 34 | self.parse_argument(argv, "--podfile") \ 35 | or "Podfile.lock" 36 | ), 37 | ) 38 | end 39 | 40 | def self.help 41 | puts <<~HELP 42 | Usage: cocoacache COMMAND [options] 43 | 44 | Commands: 45 | save Copy specs from the origin Specs to cache directory. 46 | restore Copy the cached Specs back to the origin directory. 47 | 48 | Options: 49 | --origin The origin Specs directory. Defaults to $HOME/.cocoapods/repos/master/Specs 50 | --cache Where to cache the Specs. Defaults to ~/Specs 51 | --podfile The path for the Podfile.lock. Defaults to ~/Podfile.lock 52 | HELP 53 | end 54 | 55 | def self.parse_argument(argv, name) 56 | index = argv.index(name) 57 | if index.nil? 58 | return nil 59 | end 60 | 61 | value = argv[index + 1] 62 | if value.nil? or value.start_with?('--') 63 | raise Exception("[!] Insufficient value for option '#{name}'".red) 64 | end 65 | 66 | return value 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/cocoacache/core.rb: -------------------------------------------------------------------------------- 1 | require "digest" 2 | require "yaml" 3 | 4 | module CocoaCache 5 | class Core 6 | attr_accessor :mute 7 | 8 | def initialize(origin_specs_dir:, cache_specs_dir:, podfile_path:) 9 | @origin_specs_dir = origin_specs_dir 10 | @cache_specs_dir = cache_specs_dir 11 | @podfile_path = podfile_path 12 | end 13 | 14 | def save() 15 | pods = get_pods() 16 | pods.each do |pod| 17 | origin_path = get_origin_path(pod) 18 | cache_path = get_cache_path(pod) 19 | copy_dir(:from => origin_path, :to => cache_path) 20 | end 21 | end 22 | 23 | def restore() 24 | pods = get_pods() 25 | pods.each do |pod| 26 | origin_path = get_origin_path(pod) 27 | cache_path = get_cache_path(pod) 28 | copy_dir(:from => cache_path, :to => origin_path) 29 | end 30 | end 31 | 32 | def copy_dir(from:, to:) 33 | log "Copy #{from} -> #{to}" 34 | `mkdir -p #{to} && cp -R #{from} #{to}/../` 35 | end 36 | 37 | def get_pods() 38 | lockfile = YAML.load(File.read(@podfile_path)) 39 | pods = lockfile["SPEC REPOS"]["https://github.com/cocoapods/specs.git"] 40 | return pods 41 | end 42 | 43 | def get_origin_path(pod) 44 | return File.join(@origin_specs_dir, *get_shard_prefixes(pod), pod) 45 | end 46 | 47 | def get_cache_path(pod) 48 | return File.join(@cache_specs_dir, *get_shard_prefixes(pod), pod) 49 | end 50 | 51 | def get_shard_prefixes(pod) 52 | return Digest::MD5.hexdigest(pod)[0...3].split("") 53 | end 54 | 55 | def log(*strings) 56 | puts strings.join(" ") if not @mute 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/cocoacache/version.rb: -------------------------------------------------------------------------------- 1 | module CocoaCache 2 | VERSION = "0.1.0" 3 | end 4 | -------------------------------------------------------------------------------- /test/fixtures/OriginSpecs.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/StyleShare/CocoaCache/0dba6b47e6459381e634bfb0b4b23e8e156b1f69/test/fixtures/OriginSpecs.zip -------------------------------------------------------------------------------- /test/fixtures/Podfile.lock: -------------------------------------------------------------------------------- 1 | SPEC REPOS: 2 | https://github.com/cocoapods/specs.git: 3 | - Alamofire 4 | - Kingfisher 5 | - Moya 6 | - Nimble 7 | - Quick 8 | - ReactorKit 9 | - RxCocoa 10 | - RxSwift 11 | - RxTest 12 | - SnapKit 13 | - Stubber 14 | - Texture 15 | - Then 16 | - URLNavigator 17 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | 3 | SimpleCov.start do 4 | add_filter "test" 5 | end 6 | 7 | if ENV["CI"] == "true" 8 | require "codecov" 9 | SimpleCov.formatter = SimpleCov::Formatter::Codecov 10 | end 11 | -------------------------------------------------------------------------------- /test/test_command.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | require "minitest/autorun" 4 | require "rspec/mocks/minitest_integration" 5 | 6 | require_relative "../lib/cocoacache" 7 | 8 | class CommandTest < Minitest::Test 9 | 10 | def setup() 11 | @core = instance_spy(CocoaCache::Core) 12 | @factory = class_spy(CocoaCache::Core, :new => @core) 13 | end 14 | 15 | def test_save() 16 | # when 17 | CocoaCache::Command.run(@factory, ["save"]) 18 | 19 | # then 20 | expect(@core).to have_received(:save) 21 | expect(@core).to_not have_received(:restore) 22 | end 23 | 24 | def test_save_with_no_arguments__uses_defaults() 25 | # when 26 | CocoaCache::Command.run(@factory, ["save"]) 27 | 28 | # then 29 | expect(@factory).to have_received(:new).with( 30 | :origin_specs_dir => "$HOME/.cocoapods/repos/master/Specs", 31 | :cache_specs_dir => "Specs", 32 | :podfile_path => "Podfile.lock", 33 | ) 34 | end 35 | 36 | def test_save_with_arguments() 37 | # when 38 | CocoaCache::Command.run(@factory, [ 39 | "save", 40 | "--origin", "/awesome/path/to/MyOriginSpecs", 41 | "--cache", "/superb/path/to/MyCacheSpecs", 42 | "--podfile", "/wonderful/path/to/MyPodfile", 43 | ]) 44 | 45 | # then 46 | expect(@factory).to have_received(:new).with( 47 | :origin_specs_dir => "/awesome/path/to/MyOriginSpecs", 48 | :cache_specs_dir => "/superb/path/to/MyCacheSpecs", 49 | :podfile_path => "/wonderful/path/to/MyPodfile", 50 | ) 51 | end 52 | 53 | def test_save_with_insufficient_arguments() 54 | assert_raises Exception do 55 | CocoaCache::Command.run(@factory, [ 56 | "save", 57 | "--origin", 58 | "--cache", "/superb/path/to/MyCacheSpecs", 59 | "--podfile", "/wonderful/path/to/MyPodfile", 60 | ]) 61 | end 62 | 63 | assert_raises Exception do 64 | CocoaCache::Command.run(@factory, [ 65 | "save", 66 | "--origin", "/awesome/path/to/MyOriginSpecs", 67 | "--cache", 68 | "--podfile", "/wonderful/path/to/MyPodfile", 69 | ]) 70 | end 71 | 72 | assert_raises Exception do 73 | CocoaCache::Command.run(@factory, [ 74 | "save", 75 | "--origin", "/awesome/path/to/MyOriginSpecs", 76 | "--cache", "/superb/path/to/MyCacheSpecs", 77 | "--podfile" 78 | ]) 79 | end 80 | end 81 | 82 | def test_restore() 83 | # when 84 | CocoaCache::Command.run(@factory, ["restore"]) 85 | 86 | # then 87 | expect(@core).to have_received(:restore) 88 | expect(@core).to_not have_received(:save) 89 | end 90 | 91 | def test_restore_with_no_arguments__uses_defaults() 92 | # when 93 | CocoaCache::Command.run(@factory, ["restore"]) 94 | 95 | # then 96 | expect(@factory).to have_received(:new).with( 97 | :origin_specs_dir => "$HOME/.cocoapods/repos/master/Specs", 98 | :cache_specs_dir => "Specs", 99 | :podfile_path => "Podfile.lock", 100 | ) 101 | end 102 | 103 | def test_restore_with_arguments() 104 | # when 105 | CocoaCache::Command.run(@factory, [ 106 | "restore", 107 | "--origin", "/awesome/path/to/MyOriginSpecs", 108 | "--cache", "/superb/path/to/MyCacheSpecs", 109 | "--podfile", "/wonderful/path/to/MyPodfile", 110 | ]) 111 | 112 | # then 113 | expect(@factory).to have_received(:new).with( 114 | :origin_specs_dir => "/awesome/path/to/MyOriginSpecs", 115 | :cache_specs_dir => "/superb/path/to/MyCacheSpecs", 116 | :podfile_path => "/wonderful/path/to/MyPodfile", 117 | ) 118 | end 119 | 120 | def test_restore_with_insufficient_arguments() 121 | assert_raises Exception do 122 | CocoaCache::Command.run(@factory, [ 123 | "restore", 124 | "--origin", 125 | "--cache", "/superb/path/to/MyCacheSpecs", 126 | "--podfile", "/wonderful/path/to/MyPodfile", 127 | ]) 128 | end 129 | 130 | assert_raises Exception do 131 | CocoaCache::Command.run(@factory, [ 132 | "restore", 133 | "--origin", "/awesome/path/to/MyOriginSpecs", 134 | "--cache", 135 | "--podfile", "/wonderful/path/to/MyPodfile", 136 | ]) 137 | end 138 | 139 | assert_raises Exception do 140 | CocoaCache::Command.run(@factory, [ 141 | "restore", 142 | "--origin", "/awesome/path/to/MyOriginSpecs", 143 | "--cache", "/superb/path/to/MyCacheSpecs", 144 | "--podfile" 145 | ]) 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /test/test_core.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | require "digest" 4 | require "minitest/autorun" 5 | 6 | require_relative "../lib/cocoacache" 7 | 8 | class CoreTest < Minitest::Test 9 | 10 | def setup() 11 | @fixture_directory = "#{Dir.pwd}/test/fixtures" 12 | 13 | cleanup_fixtures() 14 | prepare_fixture("OriginSpecs") 15 | 16 | @core = CocoaCache::Core.new( 17 | :origin_specs_dir => File.join(@fixture_directory, "OriginSpecs"), 18 | :cache_specs_dir => File.join(@fixture_directory, "CacheSpecs"), 19 | :podfile_path => File.join(@fixture_directory, "Podfile.lock") 20 | ) 21 | @core.mute = true 22 | end 23 | 24 | def teardown 25 | cleanup_fixtures() 26 | end 27 | 28 | def prepare_fixture(name) 29 | archive = "#{@fixture_directory}/#{name}.zip" 30 | destination = @fixture_directory 31 | exclude = "__MACOSX/*" 32 | `unzip -qq -o #{archive} -d #{destination} -x #{exclude} 2>/dev/null` 33 | end 34 | 35 | def cleanup_fixtures 36 | return if @fixture_directory.nil? 37 | return unless @fixture_directory.include? "CocoaCache" 38 | `find #{@fixture_directory}/* -type d -maxdepth 0 | xargs rm -r` 39 | end 40 | 41 | def test_save() 42 | # when 43 | @core.save() 44 | 45 | # then 46 | assert_file_exist fixture_path("CacheSpecs/d/a/2/Alamofire") 47 | assert_file_exist fixture_path("CacheSpecs/a/a/6/Kingfisher") 48 | assert_file_exist fixture_path("CacheSpecs/8/a/7/Moya") 49 | assert_file_exist fixture_path("CacheSpecs/d/c/d/Nimble") 50 | assert_file_exist fixture_path("CacheSpecs/8/0/9/Quick") 51 | assert_file_exist fixture_path("CacheSpecs/7/7/7/ReactorKit") 52 | assert_file_exist fixture_path("CacheSpecs/3/c/1/RxCocoa") 53 | assert_file_exist fixture_path("CacheSpecs/2/e/c/RxSwift") 54 | assert_file_exist fixture_path("CacheSpecs/8/5/5/RxTest") 55 | assert_file_exist fixture_path("CacheSpecs/1/f/6/SnapKit") 56 | assert_file_exist fixture_path("CacheSpecs/2/b/9/Stubber") 57 | assert_file_exist fixture_path("CacheSpecs/a/3/e/Texture") 58 | assert_file_exist fixture_path("CacheSpecs/d/4/8/Then") 59 | assert_file_exist fixture_path("CacheSpecs/8/9/0/URLNavigator") 60 | end 61 | 62 | def test_restore() 63 | # given 64 | @core.save() 65 | FileUtils.rm_rf fixture_path("OriginSpecs/d/a/2/Alamofire") 66 | FileUtils.rm_rf fixture_path("OriginSpecs/7/7/7/ReactorKit/2.0.0") 67 | FileUtils.rm_rf fixture_path("OriginSpecs/7/7/7/ReactorKit/2.0.1") 68 | FileUtils.rm_rf fixture_path("OriginSpecs/1/f/6/SnapKit/5.0.0") 69 | 70 | # when 71 | @core.restore() 72 | 73 | # then 74 | assert_file_exist fixture_path("OriginSpecs/d/a/2/Alamofire") 75 | assert_file_exist fixture_path("OriginSpecs/7/7/7/ReactorKit/2.0.0") 76 | assert_file_exist fixture_path("OriginSpecs/7/7/7/ReactorKit/2.0.1") 77 | assert_file_exist fixture_path("OriginSpecs/1/f/6/SnapKit/5.0.0") 78 | end 79 | 80 | def fixture_path(path) 81 | return File.join(@fixture_directory, path) 82 | end 83 | end 84 | 85 | 86 | def assert_file_exist(path) 87 | assert File.exist?(path), "Expected #{path} to exist but not." 88 | end 89 | --------------------------------------------------------------------------------