├── .gitignore ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── cocoapods-user-defined-build-types.gemspec ├── lib ├── cocoapods-user-defined-build-types.rb ├── cocoapods-user-defined-build-types │ ├── gem_version.rb │ ├── main.rb │ ├── podfile_options.rb │ └── private_api_hooks.rb └── cocoapods_plugin.rb └── spec ├── command └── frameworks_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg 3 | .idea/ 4 | *.gem 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in cocoapods-user-defined-build-types.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: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Jonathan Cardasis 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-user-defined-build-types 2 | ![Latest Version](https://img.shields.io/badge/Latest_supported_CocoaPods-1.9.1-gray.svg) 3 | 4 | Allow CocoaPods to mix dynamic/static libaries/frameworks. 5 | 6 | This plugin allows for a Podfile to specify how each Pod (or multiple Pods) should be built (ex. *as a dynamic framework*). 7 | 8 | 9 | ## Installation 10 | ```Bash 11 | $ gem install cocoapods-user-defined-build-types 12 | ``` 13 | 14 | ## Usage 15 | ```C 16 | plugin 'cocoapods-user-defined-build-types' 17 | 18 | enable_user_defined_build_types! 19 | 20 | target "CoffeeApp" do 21 | pod 'Alamofire' 22 | pod "SwiftyJSON", :build_type => :dynamic_framework 23 | ``` 24 | - Add `plugin 'cocoapods-user-defined-build-types'` to the top of your Podfile 25 | - Add the `enable_user_defined_build_types!` directive to the top of your Podfile (removing `use_frameworks!` if present) 26 | - Add a `build_type` option to one or more Pods to direct how they're built (ex. `:build_type => :dynamic_framework`) 27 | - `pod install` 28 | 29 | Based on this configuration, CocoaPods will build your project such that `Alamofire` stays a static library (CocoaPods default) and is part of the umbrella `Pods-CoffeeApp` static library generated by CocoaPods. `SwiftyJSON` will become a dynamic framework and dynamically linked in CoffeeApp. 30 | 31 | | SUPPORTED BUILD TYPES | 32 | | --- | 33 | | `dynamic_library` | 34 | | `dynamic_framework` | 35 | | `static_library` | 36 | | `static_framework` | 37 | 38 | ## Why 39 | Cocoapod's `use_frameworks!` directive makes **all** integrated Pods build as dynamic frameworks. 40 | 41 | This can cause issues with certain Pods. You may want some pods to be static libraries and a single Pod to a dynamic framework. CocoaPods currently does not support this. The `cocoapods-user-defined-build-types` plugin allows for build types to be changed on a Pod-by-Pod basis, otherwise defaulting to CocoaPods default build type (static library). 42 | 43 | This plugin was specifically built for React Native projects to be able to incorporate dynamic Swift Pods without needing to change other Pods. 44 | 45 | ## Verbose Logging 46 | Having issues? Try enabling the plugin's verbose logging from the Podfile: 47 | ```C 48 | plugin 'cocoapods-user-defined-build-types', { 49 | verbose: true 50 | } 51 | 52 | ... 53 | ``` 54 | 55 | For even more detailed logging, the development flag can be set in your terminal env: `export CP_DEV=1`. 56 | 57 | ## How 58 | By overriding `Pod::Podfile::TargetDefinition`'s `build_type` function (from cocoapods-core) to return the specifed linking (static/dynamic) and packing (library/framework), we can change how Cococpods builts specific dependencies. Currently in core, there is support for multiple build type but the use_frameworks! directive is the only way to enable framework builds, and it is an all-or-nothing approach. 59 | 60 | ## License 61 | Available under the MIT license. See the LICENSE file for more info. 62 | -------------------------------------------------------------------------------- /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-user-defined-build-types.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-user-defined-build-types/gem_version.rb' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'cocoapods-user-defined-build-types' 8 | spec.version = CocoapodsUserDefinedBuildTypes::VERSION 9 | spec.authors = ['Jonathan Cardasis'] 10 | spec.email = ['joncardasis@gmail.com'] 11 | spec.description = %q{A Cocoapods plugin which selectively modifies a Pod build_type right before integration. This allows for mixing dynamic frameworks with the default static library build type used by Cocoapods.} 12 | spec.summary = %q{A Cocoapods plugin which can selectively set build type per pod (static library, dynamic framework, etc.)} 13 | spec.homepage = 'https://github.com/joncardasis/cocoapods-user-defined-build-types' 14 | spec.license = 'MIT' 15 | 16 | spec.files = Dir['lib/**/*'] 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_dependency "cocoapods", ">= 1.5.0", "< 2.0" 22 | 23 | spec.add_development_dependency 'bundler', '~> 1.3' 24 | spec.add_development_dependency 'rake' 25 | end 26 | -------------------------------------------------------------------------------- /lib/cocoapods-user-defined-build-types.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods-user-defined-build-types/gem_version' 2 | -------------------------------------------------------------------------------- /lib/cocoapods-user-defined-build-types/gem_version.rb: -------------------------------------------------------------------------------- 1 | module CocoapodsUserDefinedBuildTypes 2 | VERSION = "0.0.7" 3 | end 4 | -------------------------------------------------------------------------------- /lib/cocoapods-user-defined-build-types/main.rb: -------------------------------------------------------------------------------- 1 | require_relative 'private_api_hooks' 2 | 3 | module Pod 4 | class Podfile 5 | module DSL 6 | 7 | @@enable_user_defined_build_types = false 8 | 9 | def self.enable_user_defined_build_types 10 | @@enable_user_defined_build_types 11 | end 12 | 13 | def enable_user_defined_build_types! 14 | @@enable_user_defined_build_types = true 15 | end 16 | end 17 | end 18 | end 19 | 20 | module CocoapodsUserDefinedBuildTypes 21 | PLUGIN_NAME = 'cocoapods-user-defined-build-types' 22 | LATEST_SUPPORTED_COCOAPODS_VERSION = '1.9.1' 23 | 24 | @@plugin_enabled = false 25 | @@verbose_logging = false 26 | 27 | def self.plugin_enabled 28 | @@plugin_enabled 29 | end 30 | 31 | def self.verbose_logging 32 | @@verbose_logging 33 | end 34 | 35 | def self.verbose_log(str) 36 | if @@verbose_logging || ENV["CP_DEV"] 37 | Pod::UI.puts "🔥 [#{PLUGIN_NAME}] #{str}".blue 38 | end 39 | end 40 | 41 | Pod::HooksManager.register(PLUGIN_NAME, :pre_install) do |installer_context, options| 42 | if options['verbose'] != nil 43 | @@verbose_logging = options['verbose'] 44 | end 45 | 46 | if Pod::Podfile::DSL.enable_user_defined_build_types 47 | @@plugin_enabled = true 48 | else 49 | Pod::UI.warn "#{PLUGIN_NAME} is installed but the enable_user_defined_build_types! was not found in the Podfile. No build types were changed." 50 | end 51 | end 52 | end -------------------------------------------------------------------------------- /lib/cocoapods-user-defined-build-types/podfile_options.rb: -------------------------------------------------------------------------------- 1 | is_version_1_9_x = Pod.const_defined?(:BuildType) # CP v1.9.x 2 | 3 | # Assign BuildType to proper module definition dependent on CP version. 4 | BuildType = is_version_1_9_x ? Pod::BuildType : Pod::Target::BuildType 5 | 6 | module Pod 7 | class UserOption 8 | 9 | def self.keyword 10 | :build_type 11 | end 12 | 13 | # [Hash{String, BuildType}] mapping of Podfile keyword to a BuildType 14 | def self.keyword_mapping 15 | { 16 | :dynamic_framework => BuildType.dynamic_framework, 17 | :dynamic_library => BuildType.dynamic_library, 18 | :static_framework => BuildType.static_framework, 19 | :static_library => BuildType.static_library 20 | } 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/cocoapods-user-defined-build-types/private_api_hooks.rb: -------------------------------------------------------------------------------- 1 | require_relative 'podfile_options' 2 | 3 | module Pod 4 | 5 | class Podfile 6 | class TargetDefinition 7 | 8 | # [Hash{String, BuildType}] mapping of pod name to preferred build type if specified. 9 | @@root_pod_building_options = Hash.new 10 | 11 | def self.root_pod_building_options 12 | @@root_pod_building_options 13 | end 14 | 15 | # ====================== 16 | # ==== PATCH METHOD ==== 17 | # ====================== 18 | swizzled_parse_subspecs = instance_method(:parse_subspecs) 19 | 20 | define_method(:parse_subspecs) do |name, requirements| 21 | # Update hash map of pod target names and association with their preferred linking & packing 22 | building_options = @@root_pod_building_options 23 | pod_name = Specification.root_name(name) 24 | options = requirements.last 25 | 26 | CocoapodsUserDefinedBuildTypes.verbose_log("Hooked Cocoapods parse_subspecs function to obtain Pod options. #{pod_name}") 27 | 28 | if options.is_a?(Hash) 29 | options.each do |k,v| 30 | next if not options.key?(Pod::UserOption.keyword) 31 | 32 | user_build_type = options.delete(k) 33 | if Pod::UserOption.keyword_mapping.key?(user_build_type) 34 | build_type = Pod::UserOption.keyword_mapping[user_build_type] 35 | building_options[pod_name] = build_type 36 | CocoapodsUserDefinedBuildTypes.verbose_log("#{pod_name} build type set to: #{build_type}") 37 | else 38 | raise Pod::Informative, "#{CocoapodsUserDefinedBuildTypes::PLUGIN_NAME} could not parse a #{Pod::UserOption.keyword} of '#{user_build_type}' on #{pod_name}" 39 | end 40 | end 41 | requirements.pop if options.empty? 42 | end 43 | 44 | # Call old method 45 | swizzled_parse_subspecs.bind(self).(name, requirements) 46 | end 47 | 48 | end 49 | end 50 | end 51 | 52 | module Pod 53 | class Target 54 | # @return [BuildTarget] 55 | attr_accessor :user_defined_build_type 56 | end 57 | 58 | class Installer 59 | 60 | # Walk through pod dependencies and assign build_type from root through all transitive dependencies 61 | def resolve_all_pod_build_types(pod_targets) 62 | root_pod_building_options = Pod::Podfile::TargetDefinition.root_pod_building_options.clone 63 | 64 | pod_targets.each do |target| 65 | next if not root_pod_building_options.key?(target.name) 66 | 67 | build_type = root_pod_building_options[target.name] 68 | dependencies = target.dependent_targets 69 | 70 | # Cascade build_type down 71 | while not dependencies.empty? 72 | new_dependencies = [] 73 | dependencies.each do |dep_target| 74 | dep_target.user_defined_build_type = build_type 75 | new_dependencies.push(*dep_target.dependent_targets) 76 | end 77 | dependencies = new_dependencies 78 | end 79 | 80 | target.user_defined_build_type = build_type 81 | end 82 | end 83 | 84 | # ====================== 85 | # ==== PATCH METHOD ==== 86 | # ====================== 87 | 88 | # Store old method reference 89 | swizzled_analyze = instance_method(:analyze) 90 | 91 | # Swizzle 'analyze' cocoapods core function to finalize build settings 92 | define_method(:analyze) do |analyzer = create_analyzer| 93 | if !CocoapodsUserDefinedBuildTypes.plugin_enabled 94 | return swizzled_analyze.bind(self).(analyzer) 95 | end 96 | 97 | CocoapodsUserDefinedBuildTypes.verbose_log("patching build types...") 98 | 99 | # Run original method 100 | swizzled_analyze.bind(self).(analyzer) 101 | 102 | # Set user assigned build types on Target objects 103 | resolve_all_pod_build_types(pod_targets) 104 | 105 | 106 | # Update each of @pod_targets private @build_type variable. 107 | # Note: @aggregate_targets holds a reference to @pod_targets under it's pod_targets variable. 108 | pod_targets.each do |target| 109 | next if not target.user_defined_build_type.present? 110 | 111 | new_build_type = target.user_defined_build_type 112 | current_build_type = target.send :build_type 113 | 114 | CocoapodsUserDefinedBuildTypes.verbose_log("#{target.name}: #{current_build_type} ==> #{new_build_type}") 115 | 116 | # Override the target's build time for user provided one 117 | target.instance_variable_set(:@build_type, new_build_type) 118 | 119 | # Verify patching status 120 | if (target.send :build_type).to_s != new_build_type.to_s 121 | raise Pod::Informative, "WARNING: Method injection failed on `build_type` of target #{target.name}. Most likely you have a version of cocoapods which is greater than the latest supported by this plugin (#{CocoapodsUserDefinedBuildTypes::LATEST_SUPPORTED_COCOAPODS_VERSION})" 122 | end 123 | end 124 | 125 | CocoapodsUserDefinedBuildTypes.verbose_log("finished patching user defined build types") 126 | Pod::UI.puts "#{CocoapodsUserDefinedBuildTypes::PLUGIN_NAME} updated build options" 127 | end 128 | end 129 | end -------------------------------------------------------------------------------- /lib/cocoapods_plugin.rb: -------------------------------------------------------------------------------- 1 | require 'cocoapods-user-defined-build-types/main' 2 | -------------------------------------------------------------------------------- /spec/command/frameworks_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../spec_helper', __FILE__) 2 | 3 | module Pod 4 | describe Command::Frameworks do 5 | describe 'CLAide' do 6 | it 'registers it self' do 7 | Command.parse(%w{ frameworks }).should.be.instance_of Command::Frameworks 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 | --------------------------------------------------------------------------------