├── .gitignore ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── cocoapods-monorepo.gemspec ├── lib ├── cocoapods-monorepo.rb ├── cocoapods-monorepo │ ├── command.rb │ ├── command │ │ └── monorepo.rb │ ├── gem_version.rb │ ├── podspec_local_cache.rb │ └── resolver.rb └── cocoapods_plugin.rb └── spec ├── command └── monorepo_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg 3 | .idea/ 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in cocoapods-monorepo.gemspec 4 | gemspec 5 | 6 | group :development do 7 | gem 'cocoapods' 8 | 9 | gem 'mocha' 10 | gem 'bacon' 11 | gem 'mocha-on-bacon' 12 | gem 'prettybacon' 13 | end 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 menttofly <1028365614@qq.com> 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cocoapods-monorepo 2 | [![Gem Version](https://badge.fury.io/rb/cocoapods-monorepo.svg)](https://badge.fury.io/rb/cocoapods-monorepo) 3 | 4 | A tool for organizing code in `monorepo` way. 5 | 6 | `cocoapods-monorepo` allowed you to specify a directory, so we can extend CocoaPods to support `monorepo` feature. Thanks to this plugin we can turn all the pods that under a specified directory into `Development Pods`. You will no longer need to specify all local paths in the `Podfile`, eg: 7 | 8 | ```ruby 9 | pod 'ModuleA', :path => 'path/to/ModuleA' 10 | pod 'ModuleB', :path => 'path/to/ModuleB' 11 | pod 'ModuleC', :path => 'path/to/ModuleC' 12 | ``` 13 | 14 | Besides, you can also specify a local dependency in `podsepc`, that's say you need to declare a dependency of `ModuleB ` in `ModuleA.podspec`: 15 | 16 | ```ruby 17 | s.dependency 'ModuleB' # inside ModuleA.podspec 18 | ``` 19 | 20 | ## Installation 21 | 22 | $ gem install cocoapods-monorepo 23 | 24 | ## Usage 25 | 26 | Add a reference to it in your `Podfile`, and specified the necessary option `:path` : 27 | 28 | ```ruby 29 | plugin 'cocoapods-monorepo', :path => 'path/to/repos-directory' 30 | ``` 31 | 32 | ## Requirements 33 | 34 | You should orgnize all these local modules under the same directory with `:path` option. 35 | 36 | ```bash 37 | . 38 | ├── ModuleA 39 | │   ├── ModuleA.podspec 40 | │   └── README.md 41 | ├── ModuleB 42 | │   ├── ModuleB.podspec 43 | │   ├── README.md 44 | ├── ModuleC 45 | │   ├── ModuleC.podspec 46 | │   ├── README.md 47 | ``` 48 | 49 | *Enjoy it!* 50 | 51 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cocoapods-monorepo.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'cocoapods-monorepo/gem_version.rb' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'cocoapods-monorepo' 8 | spec.version = CocoapodsMonorepo::VERSION 9 | spec.authors = ['menttofly'] 10 | spec.email = ['1028265614@qq.com'] 11 | spec.description = %q{A assistant for code organization by using monorepo.} 12 | spec.summary = %q{`cocoapods monorepo` will make the dependencies that under specified directory into development pods.} 13 | spec.homepage = 'https://github.com/' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 1.3' 22 | spec.add_development_dependency 'rake' 23 | end 24 | -------------------------------------------------------------------------------- /lib/cocoapods-monorepo.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods-monorepo/gem_version' 2 | -------------------------------------------------------------------------------- /lib/cocoapods-monorepo/command.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods-monorepo/command/monorepo' 2 | -------------------------------------------------------------------------------- /lib/cocoapods-monorepo/command/monorepo.rb: -------------------------------------------------------------------------------- 1 | module Pod 2 | class Command 3 | # This is an example of a cocoapods plugin adding a top-level subcommand 4 | # to the 'pod' command. 5 | # 6 | # You can also create subcommands of existing or new commands. Say you 7 | # wanted to add a subcommand to `list` to show newly deprecated pods, 8 | # (e.g. `pod list deprecated`), there are a few things that would need 9 | # to change. 10 | # 11 | # - move this file to `lib/pod/command/list/deprecated.rb` and update 12 | # the class to exist in the the Pod::Command::List namespace 13 | # - change this class to extend from `List` instead of `Command`. This 14 | # tells the plugin system that it is a subcommand of `list`. 15 | # - edit `lib/cocoapods_plugins.rb` to require this file 16 | # 17 | # @todo Create a PR to add your plugin to CocoaPods/cocoapods.org 18 | # in the `plugins.json` file, once your plugin is released. 19 | # 20 | # class Monorepo < Command 21 | # self.summary = 'Short description of cocoapods-githooks.' 22 | # 23 | # self.description = <<-DESC 24 | # Longer description of cocoapods-githooks. 25 | # DESC 26 | # 27 | # self.arguments = 'NAME' 28 | # 29 | # def initialize(argv) 30 | # @name = argv.shift_argument 31 | # super 32 | # end 33 | # 34 | # def validate! 35 | # super 36 | # help! 'A Pod name is required.' unless @name 37 | # end 38 | # 39 | # def run 40 | # UI.puts "Add your implementation for the cocoapods-githooks plugin in #{__FILE__}" 41 | # end 42 | # end 43 | 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/cocoapods-monorepo/gem_version.rb: -------------------------------------------------------------------------------- 1 | module CocoapodsMonorepo 2 | VERSION = "1.0.4" 3 | end 4 | -------------------------------------------------------------------------------- /lib/cocoapods-monorepo/podspec_local_cache.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods' 2 | 3 | module CocoapodsMonorepo 4 | 5 | # Cache all path of the `podspec` under the specified directory. 6 | class PodspecLocalCache 7 | # @return [Hash String>] 8 | # All podspec under specified directory. 9 | attr_reader :local_podspecs 10 | 11 | def initialize(local_podspecs) 12 | @local_podspecs = local_podspecs 13 | end 14 | 15 | # Create instance of `PodspecLocalCache` from some directory. 16 | def self.from_local_path(directory_path) 17 | unless File.directory?(directory_path) 18 | raise Pod::Informative, "[cocoapods-monorepo]: `#{directory_path}` is not a directory!" 19 | end 20 | podspecs = {} 21 | podspec_files = Dir.glob(File.join(directory_path, "*", "*.podspec")) 22 | podspec_files.each do |podspec_file| 23 | specification = Pod::Specification.from_file(podspec_file) 24 | validate_podspec(specification) 25 | podspecs[specification.name] = specification.defined_in_file.to_s 26 | end 27 | new(podspecs.freeze) 28 | end 29 | 30 | # Checking whether `podspec` is valid or not. 31 | def self.validate_podspec(podspec) 32 | defined_in_file = podspec.defined_in_file 33 | podspec.defined_in_file = nil 34 | 35 | validator = validator_for_podspec(podspec) 36 | validator.quick = true 37 | validator.allow_warnings = true 38 | validator.ignore_public_only_results = true 39 | Pod::Config.instance.with_changes(:silent => true) do 40 | validator.validate 41 | end 42 | unless validator.validated? 43 | raise Pod::Informative, "[cocoapods-monorepo]: `#{name}` is failed for validation: #{validator.failure_reason}:\n#{validator.results_message}" 44 | end 45 | ensure 46 | podspec.defined_in_file = defined_in_file 47 | end 48 | 49 | def self.validator_for_podspec(podspec) 50 | Pod::Validator.new(podspec, [], []) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/cocoapods-monorepo/resolver.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods' 2 | require 'cocoapods-monorepo/podspec_local_cache' 3 | 4 | module Pod 5 | class Resolver 6 | 7 | # Hook the original `find_cached_set` method. 8 | alias_method :origin_find_cached_set, :find_cached_set 9 | 10 | # Load and return the dependencies set of the given Pod. 11 | def find_cached_set(dependency) 12 | unless dependency.external_source 13 | 14 | name = dependency.root_name 15 | podspec_path = podspec_local_cache.local_podspecs[name] 16 | unless podspec_path.nil? 17 | # Specify the local pod as external source. 18 | dependency.external_source = {} 19 | dependency.external_source[:path] = podspec_path 20 | stored_to_sandbox_podspecs(name, dependency) 21 | end 22 | 23 | end 24 | 25 | origin_find_cached_set(dependency) 26 | end 27 | 28 | # Cache the pod which already stored into sandbox to avoid repeating injection. 29 | def stored_sandbox_podspecs 30 | @stored_sandbox_podspecs ||= {} 31 | end 32 | 33 | # Store the pod into sandbox as external source. 34 | def stored_to_sandbox_podspecs(name, dependency) 35 | stored_sandbox_podspecs[name] ||= begin 36 | source = ExternalSources.from_dependency(dependency, podfile.defined_in_file, true) 37 | source.fetch(sandbox) 38 | dependency 39 | end 40 | end 41 | 42 | # The cache of `podspec` 43 | def podspec_local_cache 44 | @podspec_local_cache ||= CocoapodsMonorepo::PodspecLocalCache.from_local_path(Resolver.monorepo_path) 45 | end 46 | 47 | # Represent monorepo's directory that you want read from. 48 | @monorepo_path = "" 49 | 50 | class << self 51 | attr_accessor :monorepo_path 52 | end 53 | 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/cocoapods_plugin.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods-monorepo/command' 2 | 3 | module CocoapodsMonorepo 4 | Pod::HooksManager.register("cocoapods-monorepo", :pre_install) do |context, options| 5 | 6 | Pod::UI.puts "[cocoapods-monorepo]: Integrating Pods with monorepo..".green 7 | require 'cocoapods-monorepo/resolver' 8 | 9 | unless options.key?(:path) 10 | raise Pod::DSLError.new("[cocoapods-monorepo]: Require pass `:path` option by using `:path => dir` in Podfile", 11 | context.podfile.defined_in_file.to_s, 12 | Exception.new("")) 13 | end 14 | 15 | Pod::Resolver.monorepo_path = options[:path] 16 | end 17 | 18 | Pod::HooksManager.register("cocoapods-monorepo", :post_install) do |context| 19 | end 20 | end -------------------------------------------------------------------------------- /spec/command/monorepo_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper', __FILE__) 2 | 3 | module Pod 4 | describe Command::Monorepo do 5 | describe 'CLAide' do 6 | it 'registers it self' do 7 | Command.parse(%w{ monorepo }).should.be.instance_of Command::Monorepo 8 | end 9 | end 10 | end 11 | end 12 | 13 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | ROOT = Pathname.new(File.expand_path('../../', __FILE__)) 3 | $:.unshift((ROOT + 'lib').to_s) 4 | $:.unshift((ROOT + 'spec').to_s) 5 | 6 | require 'bundler/setup' 7 | require 'bacon' 8 | require 'mocha-on-bacon' 9 | require 'pretty_bacon' 10 | require 'pathname' 11 | require 'cocoapods' 12 | 13 | Mocha::Configuration.prevent(:stubbing_non_existent_method) 14 | 15 | require 'cocoapods_plugin' 16 | 17 | #-----------------------------------------------------------------------------# 18 | 19 | module Pod 20 | 21 | # Disable the wrapping so the output is deterministic in the tests. 22 | # 23 | UI.disable_wrap = true 24 | 25 | # Redirects the messages to an internal store. 26 | # 27 | module UI 28 | @output = '' 29 | @warnings = '' 30 | 31 | class << self 32 | attr_accessor :output 33 | attr_accessor :warnings 34 | 35 | def puts(message = '') 36 | @output << "#{message}\n" 37 | end 38 | 39 | def warn(message = '', actions = []) 40 | @warnings << "#{message}\n" 41 | end 42 | 43 | def print(message) 44 | @output << message 45 | end 46 | end 47 | end 48 | end 49 | 50 | #-----------------------------------------------------------------------------# 51 | --------------------------------------------------------------------------------