preface

At present, the known binary open source components are implemented by Cocoapods Plugin, such as binary plug-in Cocoapods-bin, based on plug-in ability and some characteristics of Ruby language, so that it can be easy to make around Cocoapods source code. The Cocoapods plugin needs to be written in Ruby. If you are unfamiliar with Cocoapods source code and Ruby language features (and I understand that most iOS students are probably not familiar with them), you will find it difficult to understand.

Binary solutions may vary from company to company due to different development environments and habits, so customization of binary plug-ins is inevitable. This article mainly focuses on the gem Cocapods-bin, from the point of view of iOS programmers for detailed analysis, and strive to let readers understand what is behind the binary plug-in after reading this article. This can be helpful if your company also needs to do component binaries.

Component privatization

In terms of component-based architectures, most are based on the Cocoapods tools for code isolation and versioning, so if you are familiar with component privatization you can skip this section and go straight to binary solutions. We initially isolated the code locally using :path =>, which only achieved component splitting, but also brought some benefits, such as reduced code conflicts between different lines of business. In addition, The file management method of Cocoapods makes.xCodeProJ no longer cause unnecessary conflicts when someone adds or deletes files.

Although the native code between the lines of business can be isolated, but did not make the code implement isolation warehouse, all people are still under the same code warehouse development, as the company’s business growth, the warehouse code quantity are also increasing rapidly, components in more than a year of time breakthrough in more than 40, consider the promotion code compilation speed and work efficiency, The binary work of the component needs to be done.

There is still a lot of work to be done before binaries can be implemented, starting with privatizing components. Go to ~/. Cocoapods /repos and install the cocoapods tool for the first time. After installing the cocoapods tool for the first time, the user will go to the domestic source and pull out a master. All open source code, if supported by POD management, will have its PodSpec files in it (if not, a POD update will have them). So let’s review what you need to do to open source your code on Git:

  1. Commit the last change.
  2. Add a tag and push it to remote.
  3. performpod spec lintCommand to verify podSpec validity.
  4. performpod trunk pushSubmit podspec.

When you perform a POD Trunk push, you actually commit your PodSpec to Cocoapods’ official source. The private component works the same way, except that it commits the PodSpec file to a repository it has created, which is often called the private source. There are many tutorials on privatizing components online, but the process can be described as follows:

  1. Prepare two Git repositories, one for the submitted PodSpec files, which will be referred to as private sources later. One is used to hold real components, which will be called component repository later.
  2. performpod lib createCommand to build the template project and improve component functionality.
  3. After the component is complete, edit the PodSpec and specifyversion.sourcesource_filesThe podspec is a description of the component (it’s actually a Ruby script that podSpec reads later), including the component version, location, code location, and so on. Someone else can get your PodSpec and install your code through Cocoapods.
  4. performgit taggit push --tagsThe remote Git repository will build a zip package for the tag version of the repository that you want to tag in your podfile.
  5. performpod spec lint --allow-warningsThe podSpec command validates the podspec file, usually with an allow-warnings option.
  6. performpod repo pushThe podSpec command pushes podSpec to a private source repository.

If the rest of the team relies on your component, perform pod repo update (if the component’s podfile has a private source configured, it will not be found because Cocoapods will internally iterate through all sources, including public sources, to find the pod Spec.) If you do not specify the source, you will not be able to find it. If you do not specify the source, you can also use a custom Pod Plugin. The pod Update process is to update the latest version of your podSpec to a local private source, update podfile.lock, and execute pod Install.

When components were privatized, code warehouse isolation was truly implemented, and the engineering architecture evolved as follows:

Each line of business will develop in their own business components. After the requirements are developed, submit the PodSpec to the private source. The shell engineering can perform pod Update to update the newly developed business components, and then directly package and test them. In fact, component splitting is kind of a manual labor, but it’s the basis of binary, and if this structure doesn’t fit well, binary won’t work. Let’s get to the subject of exploring binary.

Off topic: There are two solutions to the problem of whether to specify dependent component versions in podfiles and PodSpecs:

  1. Podfiles and PodSpecs are version-logged, and when a component releases a new version, it notifies its dependencies for updates.
  2. Podfiles keep version records. Podspec does not keep version records. All dependent components, including all child component versions, are defined in the Podfile and are known by version updates.

The second advantage is that you can modify only the Podfile without modifying the associated PodSpec.

Single private source scheme

Binary at present, there are two feasible schemes of single private source and double private source in the market. The following are simple explanations for these two schemes:

A single private source means that there is only one private repository that holds the PodSpec, the PrivateRepo repository in the figure above. So how does a repository switch between source code and binary? It’s easy to configure environment variables in your PodSpec, as follows:

  1. Create a Lib folder in the Class level directory, copy the binary framework into it, and push it to the remote repository.
wangkai@192 HelloMoto % tree. ├── Assets ├─ ├─ download.txt │ ├─ │ ├─ wangkai@192 HelloMoto % tree │ ├─ [email protected] │ ├─ [email protected] │ ├─ Classes │ ├─ PHHelloMoto HelloMoto. Framework ├ ─ ─ Headers - > Versions/Current/Headers ├ ─ ─ HelloMoto - > Versions/Current/HelloMoto ├ ─ ─ Resources - > Versions/Current/Resources └ ─ ─ Versions ├ ─ ─ A │ ├ ─ ─ Headers │ │ ├ ─ ─ PHHelloMoto. H │ │ ├ ─ ─ PHHelloMotoYellowView. H │ │ ├ ─ ─ PHTestView. H │ │ └ ─ ─ PHtest. H │ ├ ─ ─ HelloMoto │ └ ─ ─ Resources │ └ ─ ─ HelloMoto. Bundle │ ├ ─ ─ Assets. The car │ └ ─ ─ Info. ├ ── Current -> ACopy the code
  1. Modify podSpec sourcefile to point to the environment variable:
if ENV['IS_SOURCE'] || ENV["#{s.name}_SOURCE"]
  s.source_files = "#{s.name}/Classes/**/*"
else
  s.ios.vendored_frameworks = "#{s.name}/Lib/#{s.name}.framework"
end
Copy the code
  1. Set the preserve_paths
s.preserve_paths = "#{s.name}/Lib/**/*.framework","#{s.name}/Classes/**/*"
Copy the code

Preserve_paths is configured in PodSpec to ensure that both source and binary resources and files exist in the cache. If not set, files will be lost when switching between source and binary files, resulting in unexpected problems when switching.

  1. Review the steps above to publish your PodSpec to PrivateRepo.

To complete the above configuration, install the source code and binaries by entering IS_SOURCE pod Install and pod Install at the terminal. If you want one library to be source code, the other libraries should be binary. For example, HelloMoto can be source code by typing HelloMoto_SOURCE pod install, and other component libraries should be binary.

Although the single-source, single-version solution can achieve source code and binary conversion, we feel that this solution has the following disadvantages:

  1. Switching binary SOURCE code between multiple components can be tedious, and the POD command can become very long because of the need to type SOURCE at the terminal.

  2. Breaking the POD caching mechanism, the POD caching process can be simply understood as follows:

As you can see from the above cache reading process, if the component only exists locally in the form of source code, it will not be able to install the binary, because the local already exists and git will not be able to pull the binary. This problem can also be solved by removing the level 1 and level 2 caches as shown in the figure above, so that pod can directly download components from Git and install them.

See: iOS CocoaPods Component Smooth Binary Solution.

Considering that not everyone on the team would be familiar with these processes, we felt that this would have a major impact on our daily work, since it would be a bit of an intrusion into Cocoapods’ caching mechanism, and git repositories would grow larger as the number of binaries increased, so we further investigated the dual private source solution.

Dual private source scheme

This article focuses on the dual private source solution, which cocoapods-bin uses. This means that there are two repositories for podSpecs, one for the source code, such as the PrivateRepo repository, and one for the binary version. Call it PrivateRepo_Bin for now, and you’ll also need a static server to store binary ZIP packages for others to install.

The two-private source scheme is a bit more complicated than the single-private one, requiring the additional uploading of the binary package to the ZIP server to regenerate a binary version of podSpec and publish it to the binary private source. Having everyone on your team maintain binary podSpec and binary ZIP packages is a major drag on productivity, and plug-ins like Cocoapods-bin are designed to address these issues. The principle behind binary plug-ins will be analyzed through cocoapods-core source code and Cocoapods-bin source code.

The general differences between source podSpec and binary PodSpec are as follows:

{... Omit the "source" : {" git ":" https://github.com/GitWangKai/HelloMoto.git ", "tag" : "0.1.0 from,"}, "resource_bundles" : { "HelloMoto": [ "HelloMoto/Assets/**/*.xcassets" ] }, "source_files": "HelloMoto/Classes/**/*", }Copy the code
{... Omit the "source" : {" HTTP ":" http://localhost:8080/frameworks/HelloMoto/0.1.0/zip ", "type" : "zip"}, "the resources" : [ "HelloMoto.framework/Versions/A/Resources/*.bundle" ], "ios": { "vendored_frameworks": "HelloMoto.framework" }, }Copy the code

Take framework form as an example.

The main differences are in source_files and vendored_frameworks. Publish it to the PrivateRepo_Bin repository with the pod repo push PrivateRepo_Bin hellomoto.podspec command. The architecture diagram for dual private sources is as follows:

Ignore the odd naming of.binary. Podspec for a moment.

All the plug-ins mentioned in the introduction are based on the idea of dual-private source implementation. The following is a look at the implementation of the dual-private source solution based on cocoapods-bin plug-in. Since Cocoapods-bin is a Cocoapods Plugin, it is important to understand the concepts of the Cocoapods Plugin before you do so.

Cocoapods Plugin system is combed

RVM

Cocoapods is written in Ruby, and RVM is a Version manager for Ruby that allows quick switching between multiple Ruby versions. To use the Cocoapods tool, run the RVM List Known command to view all available Ruby versions. You can use RVM install to install a specific Ruby environment.

RubyGems

RubyGems is a Package manager for Ruby that provides a standard format for distributing Ruby programs and libraries, as well as a tool for managing gem installations.

The gem list is executed by the terminal and you can see all managed Ruby packages, including Cocoapods if installed. Developed plug-ins can be installed by the gem install command and uninstalled by the gem uninstall command. The Cocoapods plugin is also a gem that can be published to RubyGems.org for others to install using the gem push command. See RubyGems.org for more details.

Bundler

A tool for managing project dependencies that can isolate differences in Gem versions and dependent environments across projects. It is also a Gem plug-in written in Ruby. After the pod plugin is created, a Gemfile is generated. Like podfiles, gemfiles are configured with various dependencies:

source 'https://rubygems.org' gem 'cocoapods-testplugin' , /cocoapods-testplugin" group :debug do gem 'ruby-debug-ide' debase','0.2.5.beta1' gem 'rake','13.0.0' :path => "./cocoapods-testplugin" group :debug do gem 'ruby-debug-ide' debase','0.2.5.beta1' gem 'rake','13.0.0' Gem "Cocoapods ",' 1.9.3' gem" Cocoapods -generate",'2.0.0' endCopy the code

Gemfile.lock is generated when the dependent Gem is installed by executing bundle Install. In the same way that Bundler manages gems based on the Gemfile file in the project, CocoaPods manages pods through podfiles.

Plugin

To view all installed Cocoapods plugins, type the pod plugins installed command on the terminal. For example, here are all installed cocoapods plugins on my machine:

wangkai@192 ~ % pod plugins installed Installed CocoaPods Plugins: - cocoapods-deintegrate : Cocoapods-generate: 2.0.0 -cocoapods-packager: cocoapods-generate: 2.0.0 -cocoapods-packager: 1.5.0 - Cocoapods-testPlugin: 0.2.1 (pre_install and source_Provider hooks) - cocoapods-testplugins: 1.0.0 -cocoapods-search: 1.0.0 -cocoapods-stats: 1.1.0 (post_install hook) -cocoapods-trunk: 1.0.0 -cocoapods-search: 1.0.0 -cocoapods-stats: 1.1.0 (post_install hook) -cocoapods-trunk: 1.5.0-cocoapods-try: 1.2.0 wangkai@192 ~ %Copy the code

Cocoapods-testplugin is a custom binary plug-in demo. Pre_install and source_provider hooks are prompted because the plugin internally hooks the pre_install and source_provider methods, which will be covered later.

Another important plugin, Cocoapods-plugins, is dedicated to managing plug-ins, such as viewing, searching, creating, etc.

To create a plug-in template project, run the pod plugins create testPlugin command through Cocoapods -plugins.

wangkai@192 Cocoapods-testPlugin % tree.├ ── Gemfile ├─ license.txt ├─ readme.md ├─ Rakefile ├─ ├── ├─ ├─ ├─ ├─ ├.├.gen.├ ── ├.gen.├ ── ├.gen.├ ── │ ├─ ├─ class ├─ ├─ ├─ class ├─ ├─ class ├── Testplugin_spec. Rb └ ─ ─ spec_helper. RbCopy the code

Gemfiles are similar to Podfiles in that they contain dependencies on other plug-ins, pod for managing pod dependencies, and gems for managing dependencies between gems.

Cocoapods-testplugin. gemSpec is functionally equivalent to podspec files:

Gem::Specification.new do |spec|
  spec.name          = 'cocoapods-testplugin'
  spec.version       = CocoapodsTestbin::VERSION
  spec.authors       = ['GitWangKai']
  spec.email         = ['[email protected]']
  spec.description   = %q{A short description of cocoapods-testbin.}
  spec.summary       = %q{A longer description of cocoapods-testbin.}
  spec.homepage      = 'https://github.com/EXAMPLE/cocoapods-testbin'
  spec.license       = 'MIT'

  spec.files         = `git ls-files`.split($/)
  spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
  spec.test_files    = spec.files.grep(%r{^(test|spec|features)/})
  spec.require_paths = ['lib']

  spec.add_development_dependency 'bundler', '~> 1.3'
  spec.add_development_dependency  'rake'
end
Copy the code

Manage descriptions of releases, including dependencies on other plug-ins, and so on, consistent with podSpec.

Under lib is the location where we develop the plug-in code and write the command code. All the command codes we write are placed under the command. The most commonly used pod install command execution code is also placed under the command of Cocoapods-core.

Dependency in GemSpec must be versionated, otherwise the terminal will throw a warning when using the plug-in.

CLAide

CLAide is a command line interpreter responsible for parsing common POD commands that we type at the terminal.

Module CLAide # @return [String] # # CLAide's version Following [semver](http://semver.org). # VERSION = '1.0.3'. Freeze require 'claide/ ANSI 'require 'claide/argument' require 'claide/argv' require 'claide/command' require 'claide/help' require 'claide/informative_error' endCopy the code

Pod commands are inherited from the CLAide Command class:

 class Command < CLAide::Command
    ...
    require 'cocoapods/command/init'
    require 'cocoapods/command/install'
    require 'cocoapods/command/update'
    ...
end
Copy the code

Upon receiving the command, CLAide iterates through all subclasses to find the entered Commond and executes the corresponding Ruby script.

To configure the Pod environment, see RubyMine to debug cocoapods.

Once you understand the pod Plugin concept, you can start writing binary plug-ins. Plugin is managed by RubyGems, so it needs to be written in Ruby. If you are not familiar with Ruby, you need a beginner’s tutorial. The tools are simple to use, but the dark magic behind them is worth understanding and thinking about.

binary

There are two types of static libraries in iOS, one with a. A suffix and the other with a. Framework suffix. They are essentially the same. .framework contains header files and resource files that can be used directly. However, references to the. Framework need to use <>, and the. A library can use “” directly, which format is optional.

Binary packing

There are two known schemes for binary packaging:

cocoapods-packager

Podspec = XXX. Podspec = XXX. Podspec = XXX. Run xcodebuild to build the framework. But it has some disadvantages:

  1. When you select the. A form as a product, the.h specified in our Podspec will not be correctly copied to the destination folder.
  2. For example, if I have a component library, the Phone project needs to reference SubSpecA, and the Pad project needs to reference SubSpecB. When using this component to package, SubSpecA and SubSpecB will be merged into a framework/.a, which is obviously not desirable. It is more reasonable to configure whether subspecs will be merged or split.
  3. Cocoapods-packager maintenance has been stopped and updates to new Cocoapods features or Swift support cannot be synchronized.

Copy: Applauses iOS- a binary based compilation strategy for improving performance

cocoapods-generate

Cocoapods-generate is another plugin by the authors of Cocoapods-Packager. It provides the ability to build projects, but lacks the ability to build frameworks compared to Cocoapods-Packager. But it has the advantage of not relying on Git and generating projects locally from podSpec files provided. After the project is generated, you can customize the packaging script and build the corresponding binary using xcodeBuild commands. The Gemfile dependency can be used to generate the Cocoapods Plugin:

Group :debug do gem 'ruby-debug-ide' gem 'debase','0.2.5. Beta1 'gem 'rake','13.0.0' gem "cocoapods", '1.9.3 gem "cocoapods - generate",' 2.0.0 'endCopy the code

After executing the bundle install, you can use it directly within the plug-in script:

require 'cocoapods/generate'
Copy the code
argvs = [
]

gen = Pod::Command::Gen.new(CLAide::ARGV.new(argvs))
gen.validate!
gen.run
Copy the code

Arguments in argvs can be added as needed. Parameter description: readme.md.

In Ruby, Pod is a Module that provides a separate namespace, and Command is the Pod Class, :: Pod::Command:: gen. new means to instantiate an instance of Gen type.

After the project is built, you can run the Xcodebuild command to customize scripts to replace cocoapods-Packager’s packaging function. The core ideas are as follows:

Build the simulator static library file:

Xcodebuild ARCHS='i386 x86_64' OTHER_CFLAGS='-fembed-bitcode -Qunused-arguments' CONFIGURATION_BUILD_DIR=' output path 'clean Build-configuration 'environment' -target 'target'Copy the code

Build a real static library file:

Xcodebuild ARCHS='arm64 armv7' OTHER_CFLAGS='-fembed-bitcode -Qunused-arguments' CONFIGURATION_BUILD_DIR=' output path 'clean Build-configuration 'environment' -target 'target'Copy the code

Merger. A /. Framework:

Lipo-create-output 'output directory' 'various. A paths'Copy the code

After the merging, copy corresponding header files, certificates, and resource files to form a complete Framework library. This can be viewed in conjunction with the cocoapods-bin source code.

Binary upload

Binary upload is mainly to configure the environment:

  1. Before uploading binary files, build a mongodb database to store binary information, such as package name and version. Brew Install [email protected] can be installed directly from Homebrew, and a visual tool for mongodb is recommended: Robo 3T.

  2. Download the binary-server code, and once mongodb is running, CD to binary-server and execute NPM install and NPM start.

As shown above, the Node service is already running.

  1. The terminal executes the upload command (see binary-server/ readme.md for details) :
The curl 'upload url' -f "name = # {@ spec. The name}" -f "version = # {@ spec. Version}" -f "annotate = # # {@ spec. The name} _ {@ spec. Version} _log" - F "file=@#{zip_file}Copy the code

To upload a file, run the curl -f command. Curl is used to request a Web server. -f stands for form-data.

Curl can also be written into the Cocoapods plug-in and automatically uploaded to the binary server after the binary is packaged.

Publish binary PodSpecs

Create a binary PodSpec

Before you can understand binary PodSpec generation, you need to understand how Cocoapods reads podSpec files. After pod Install is executed, Cocoapods builds the Specification (the object defined in Cocoapod-Core to describe the PodSpec) object based on the version specified by Podfile.lock during the dependency resolution process.

# cocoapods-core/specification.rb def self.from_file(path, Subspec_name = nil) # verify that podspec exists unless path.exist? Raise Informative, "No podspec exists at path '#{path}'." end # Convert File to UTF-8 format string = file. open(path, 'r: UTF-8 ', &:read) # Work around for Rubinius incomplete encoding in 1.9 mode if string.respond_to?(:encoding) && string.encoding.name ! = 'UTF-8' string.encode! String froM_STRING (string, path, subspec_name) end (' utF-8 'Copy the code

Convert the file to utF-8 string and pass it to from_string for processing:

# cocoapods-core/specification.rb def self.from_string(spec_contents, path, Subspec_name = nil) path = pathname.new (path).expand_path spec = nil case path.extName # parse. Podspec when '.podspec' Dir.chdir(path.parent.directory? ? path.parent : Dir.pwd) do # evalpod ::Specification::DSL spec = :: pod._eval_podspec (spec_contents, path) unless spec.is_a?(Specification) raise Informative, "Invalid Podspec file at path '#{path} '. "end end # parse. Json when' Specification.from_json(spec_contents) else raise Informative, "Unsupported specification format `#{path.extname}` for spec at `#{path}`." end spec.defined_in_file = path spec.subspec_by_name(subspec_name, true) endCopy the code

As you can see, podSpec files support two extensions, PodSpec and JSON, which are handled differently.

  • Podspec:eval()The podSpec function converts a string to code to executePod::Spec.newStrings are initialized as Specification objects (the Specification class is defined in Cocoapods-Core), as is parsing podfile ideas, as discussed in the binary source switching section below.

The eval function is one of Ruby’s dark magic features, similar to OC’s dynamic implementation of message forwarding. Ruby can execute strings directly as methods and arguments, whereas OC is more complex to implement.

OCEval] (github.com/lilidan/OCE…

  • Json: Execute directlyfrom_jsonInternally converts JSON to hash and stores it in the Specification object:
#cocoapods-core/specification/json.rb def self.from_hash(hash, parent = nil, test_specification: false, app_specification: Spec = spec. New (parent, nil, test_specification, :app_specification => app_specification) subspecs = attributes_hash.delete('subspecs') testspecs = Attributes_hash. Delete ('testspecs') appspecs = attributes_hash. Delete ('appspecs') ## Compatibility with 1.3.0 spec.test_specification = ! attributes_hash['test_type'].nil? spec.attributes_hash = attributes_hash spec.subspecs.concat(subspecs_from_hash(spec, subspecs, false, false)) spec.subspecs.concat(subspecs_from_hash(spec, testspecs, true, false)) spec.subspecs.concat(subspecs_from_hash(spec, appspecs, false, true)) spec endCopy the code

Now that you know the podSpec parsing process, you can understand how to create binary PodSpecs:

  1. throughfrom_fileMethod will be native sourcepodspecThe file is constructed asSpecificationObject.
  2. Remove the source_files attribute from Specification and add the vendored_frameworks attribute.

If it is in. Framework form, add “resources”, “public_header_files”, “vendored_libraries”, replace “source”, “source_files”

  1. Save the Specification object to JSON file locally (as seen abovebinary.podspec.jsonFile).

Cocoapods-bin /helpers/spec_creator. Rb

Note that the binary podspec file name is defined in cocoapods-bin as follows:

cocoapods-bin/helpers/spec_files_helper.rb

def filename
  @filename ||= "#{spec.name}.binary.podspec.json"
end
Copy the code

Json, Cocoapods returns the podSpec path in the source by calling specification_path during parsing the PodSpec file:

#cocoapods-core/source.rb def specification_path(name, version) raise ArgumentError, 'No name' unless name raise ArgumentError, 'No version' unless version # concatenates file path = pod_path(name) + version.to_s # concatenates podspec.json suffix specification_path = path +  "#{name}.podspec.json" unless specification_path.exist? Podspec suffix specification_path = path + "#{name}. Podspec "end # unless specification_path.exist? raise StandardError, "Unable to find the specification #{name} "\ "(#{version}) in the #{self.name} source." end # specification_path endCopy the code

Json and PodSpec suffixes are the original matching rules inside Cocoapods. If the extension is any other, raise will display an error message indicating that it cannot be found. For the binary version of binary.podspec.json, we need to override the Source class’s specification_path method to change specification_path, Extend VALID_EXTNAME to return the binary podspec path, otherwise an error will be reported: PodSpec cannot be found in binary source:

cocoapods-bin/native/source.rb

module Pod class Source def specification_path(name, version) raise ArgumentError, 'No name' unless name raise ArgumentError, 'No version' unless version path = pod_path(name) + version.to_s # Traverses VALID_EXTNAME to determine if the file has specification_path = Specification::VALID_EXTNAME .map { |extname| "#{name}#{extname}" } .map { |file| path + file } .find(&:exist?) unless specification_path raise StandardError, "Unable to find the specification #{name} " \ "(#{version}) in the #{self.name} source." end specification_path end end endCopy the code

The Open Class. Another Ruby feature that allows it to extend and replace methods in any module. The Ruby based Open Class feature overrides the specification_path method, which podFile will use later to parse custom DSLS.

The verification rules are expanded on the basis of the source code:

VALID_EXTNAME = %w[.binary.podspec.json .binary.podspec .podspec.json .podspec].freeze
Copy the code

When pod Install is executed, the matching rules for podSpecs can be simply described as follows:

  1. Retrieve the pod library configured by the podfile.
  2. Retrieves all versions of the POD within the source.
  3. Find the version of the library that podfile.lock corresponds to.
  4. Retrieve the corresponding PodSpec based on the corresponding version number in podfile.lock.

If you are interested in the detailed process, delve into Cocoapods source code.

Publish binary PodSpecs

Release based on binary Cocoapods Cocoapods/command/repo/push. The rb, it is also a pod repo push command executable files.

cocoapods-bin/command/bin/repo/push.rb

"--sources=#{sources_option(@code_dependencies, @sources)}", *@additional_args] # Argvs << spec_file if spec_file if @loose_options argvs += ['--allow-warnings', '--use-json'] if code_spec&.all_dependencies&.any? Argvs < < '- use - libraries' end end # execution pod repo push Command push = pod: : Command: : repo: : push the new (CLAide: : ARGV. New (argvs)) push.validate! push.runCopy the code

Note that PodSpec pushes to a binary REPO and the Lint validation process is performed by default. The lint procedure executes the Linter object’s Lint method for verification:

# cocoapods - core/specification/linter. Rb def lint @ results = results. The new if spec # check podspec file defined in the s.n ame is matching with the file name. Validate_root_name # Checks that all defined attributes have values. Check_required_attributes # Check requires_ARC check_REQUIres_arc_attribute # Execute hook method run_root_validation_hooks # multiple platforms supported for verification perform_all_specs_analysis else results.add_error('spec', "The specification defined in `#{file}` "\ "could not be loaded.\n\n#{@raise_message}") end results.empty? endCopy the code

In validate_root_name, we check if the s.name defined in the podspec file matches the file name:

#cocoapods-core/specification/linter.rb

def validate_root_name
  if spec.root.name && file
    acceptable_names = [
      spec.root.name + '.podspec',
      spec.root.name + '.podspec.json',
    ]
    names_match = acceptable_names.include?(file.basename.to_s)
    unless names_match
      results.add_error('name', 'The name of the spec should match the ' \
                        'name of the file.')
    end
  end
end
Copy the code

The binary podspec extension is binary. Podspec. json, so this check will not pass. Linter validate_root_name (); Linter validate_root_name ()

cocoapods-bin/native/linter.rb

module Pod
  class Specification
    class Linter
      def validate_root_name
        if spec.root.name && file
          acceptable_names = Specification::VALID_EXTNAME.map { |extname| "#{spec.root.name}#{extname}" }
          names_match = acceptable_names.include?(file.basename.to_s)
          unless names_match
            results.add_error('name', 'The name of the spec should match the ' \
                              'name of the file.')
          end
        end
      end
    end
  end
end
Copy the code

Binary source switching

Podfile parsing

Binary source switching requires you to understand how podfiles are loaded by Cocoapods. You can easily understand how podfiles are loaded if you understand how to create binary PodSpecs above, because they are identical.

After executing pod install, the internal implementation is as follows: go to the install.rb run method:

# cocoapods/command/install. Rb def run # check podfile verify_podfile_exists! Reinstaller = installer_for_config reinstaller = installer_for_config Installer. repo_update = repo_update? (:default => false) installer.update = false installer.deployment = @deployment installer.clean_install = @clean_install # to perform the install! installer.install! endCopy the code

When pod Install or POD Update is executed, verify_podfile_exists! Is first executed inside Cocoapods. :

#cocoapods/command.rb

def verify_podfile_exists!
  unless config.podfile
    raise Informative, "No `Podfile' found in the project directory."
  end
end
Copy the code

Config. podfile is the entry to the podfile:

#cocoapods/config.rb

def podfile
  @podfile ||= Podfile.from_file(podfile_path) if podfile_path
end
Copy the code

| | = is lazy loading of ruby.

Finally, the self.from_file method in the PodFile object is entered:

def self.from_file(path) path = Pathname.new(path) unless path.exist? raise Informative, "No Podfile exists at path '#{path}'." end # Podfile file with extension.podfile.rb. yaml '.podfile', '.rb' Podfile.from_ruby(path) when '.yaml' Podfile.from_yaml(path) else raise Informative, "Unsupported Podfile format `#{path}`." end endCopy the code

Take from_Ruby as an example to explore the podfile parsing process:

# podfile.rb def self.from_ruby(path, contents = nil) ... Podfile = podfile.new (path) do # rubocop:disable Lint/RescueException begin #  rubocop:disable Eval eval(contents, nil, path.to_s) # rubocop:enable Eval rescue Exception => e message = "Invalid `#{path.basename}` file: #{e.message}" raise DSLError.new(message, path, e, contents) end # rubocop:enable Lint/RescueException end podfile endCopy the code

Ruby’s begin Rescue syntax is equivalent to try catch, and everything from begin to rescue is protected. If an exception occurs during the execution of a code block, control passes to the block between Rescue and end, and the logic within Rescue is executed.

The do end statement is a Ruby specific syntax that can be understood as an OC block.

Podfile.new calls the Initialize method of Podfile, where the Podfile path and block are parameters to the initialize method:

# podfile.rb

def initialize(defined_in_file = nil, internal_hash = {}, &block)
  self.defined_in_file = defined_in_file
  @internal_hash = internal_hash
  if block
    default_target_def = TargetDefinition.new('Pods', self)
    default_target_def.abstract = true
    @root_target_definitions = [default_target_def]
    @current_target_definition = default_target_def
    instance_eval(&block)
  else
    @root_target_definitions = []
  end
end
Copy the code

Instance_eval is Ruby’s way of executing code blocks that define methods that become podfile class methods. In addition to instance_eval, Ruby features the class_eval method, which makes methods defined in code blocks instance methods of Podfile objects, as opposed to their semantics, and will be used later in the custom configuration file section.

If the block exists, execution of the block continues through instance_eval, returning to the do End block above. Contents converts the contents of the podfile to a string in UTF-8 format, which is executed through the eval() function.

All methods in the Podfile are defined in dsl.rb. Using platform :ios, ‘8.0’ as an example, after the eval function is executed, the platform function defined in Pod::Podfile::DSL is executed:

# podfile/dsl.rb def platform(name, target = nil) # Support for deprecated options parameter target = target[:deployment_target] if target.is_a? (Hash) current_target_definition.set_platform! (name, target) endCopy the code

Where name is ios and target is 8.0, they are stored in the podfile internal_Hash dictionary. When dependencies are resolved later, the version number will be retrieved from the hash table. Verify pod library version support ios8.0:

def set_hash_value(key, value) unless HASH_KEYS.include? (key) raise StandardError, "Unsupported hash key `#{key}`" end internal_hash[key] = value endCopy the code

Key is platform and value is {ios:8.0}. DSLS defined in podfiles are executed in this manner.

Custom DSL

Since podfile methods are defined in dsl.rb, is it possible to customize the DSLS in podfile by defining an extension to dsl.rb? Yes. Again based on the Open Class feature, you can have it extend and replace methods in any module, including DSL Modules.

Module Pod class Podfile module DSL # Custom DSL for Podfile /dsl.rb extension def set_use_source_PODS (Pod_POds_use_source = get_internal_hash_value(USE_SOURCE_PODS) || [] hash_pods_use_source += Array(pods) set_internal_hash_value(USE_SOURCE_PODS, hash_pods_use_source) end end end endCopy the code
Def set_internal_hash_value(key, value) # key = USE_SOURCE_PODS, value = array of incoming Pods. internal_hash[key] = value endCopy the code

As shown above, if you add set_use_source_Pods [‘testPod’] to your podfile, set_USe_source_Pods can be executed during POD install or POD update.

Understand the above process, you can well understand the switch between binary and source. Podfiles are managed by Git, and it is obviously unreasonable to configure a custom DSL directly in the podfile. Based on the eval and Open Class features, this problem can be solved. Eval not only executes podfiles, it can execute any file, including custom configuration files. You can do this by adding a DSL that switches between source code and binary and adding it to your custom configuration file.

Custom configuration files

During the Pod Install or POD Update phase, load a custom podfile before pod Install is executed using the hook pre_install method inside the plug-in (provided that the plug-in is introduced into the Podfile). The core code is as follows (see source_provider_hook.rb for details) :

Pod::HooksManager.register('cocoapods-testplugin', :pre_install) do |_context, _| project_root = Pod::Config.instance.project_root path = File.join(project_root.to_s, 'BinPodfile') next unless File.exist? (path) contents = File.open(path, 'r:utf-8', &:read) podfile = Pod::Config.instance.podfile podfile.instance_eval do begin eval(contents, nil, path) rescue Exception => e message = "Invalid `#{path}` file: #{e.message}" raise Pod::DSLError.new(message, path, e, contents) end end endCopy the code
# BinPodfile

use_binaries!
set_use_source_pods ['testPod']
Copy the code

BinPodfile is a custom configuration file. Add it to.gitignore, use_binaries! And set_USe_source_PODS are custom DSLS.

This is similar to how Cocoapods loads podfile code above. BinPodfile is a local custom file. After eval is executed, the testPod configuration information is stored in internal_hash hash table.

In the subsequent process of pod install, you can use a customized key to retrieve the configuration of BinPodfile from internal_hash hash table to switch between the source code and binary PodSpec.

Get cocoapods dependency resolution results

Analyzing Dependencies if the console prints Analyzing dependencies, then you are Analyzing internal dependencies. Resolver_specs_by_target (resolver_specs_by_target, resolver_specs_by_target) ¶ Then replace it with the desired one to complete the switch. For this process, take a look at what’s behind the pod Install command.

cocoapods/installer.rb def install! Download_dependencies # verify target validate_targets # Generate the project file if installation_options.skip_pods_project_generation? Show_skip_pods_project_generation_message else integrate end # Write dependency write_lockfiles # end perform_post_install_Actions endCopy the code

The prepare and resolve_dependencies phases of Install are described in 1.9.3.

In the prepare phase, all Plugin pre_install hook methods are executed internally:

def prepare # Raise if pwd is inside Pods if Dir.pwd.start_with? (sandbox.root.to_path) message = 'Command should be run from a directory outside Pods directory.' message << "\n\n\tCurrent directory is #{UI.path(Pathname.pwd)}\n" raise Informative, Message 'Preparing' do deIntegrate_if_different_major_version sandbox. Prepare # Ensure that all plug-ins specified in the podfile are loaded. ensure_plugins_are_installed! Execute the plugin's pre_install hook method. run_plugins_pre_install_hooks end endCopy the code

This is why the previous custom configuration file can be loaded. The process of resolve_dependencies is relatively complex, which is the core of the dependency resolution process:

Install. Rb def resolve_dependencies # as with pre_install, the source_provider hook method is implemented. The source of the POD library can be added dynamically here. Plugin_sources = run_source_PROVIDer_hooks Analyzer = create_Analyzer (plugin_sources) # If pod update is performed, the repO will be updated first UI.section 'Updating local specs repositories' do analyzer.update_repositories end if repo_update? Ui. section 'Analyzing dependencies' do analyze(Analyzer) validate_build_configurations end UI.section 'Verifying no changes' do verify_no_podfile_changes! verify_no_lockfile_changes! end if deployment? analyzer endCopy the code

The run_source_PROVIDer_hooks method internally executes the plug-in’s source_provider hook method to load the repO source configured within the plug-in, as discussed in the source loading section below.

Then it enters the Analyzing Dependencies process, and the Analyzer uses Molinillo (a graph algorithm) to parse a list of dependencies. The main process is as follows:

Analyzer. Rb def analyze(allow_fetches = true) return @result if @result # Podfile validate_podfile! # Validate_lockFILe_version = podfile.lock > podfile.lock If installation_options.integrate_targets? Target_conforms = inspect_targets_to_integrate else verify_platforms_specified! Target_conforms = {} end # Returns pod libraries for different states in podfile by matching them to podfile.lock. # cocoapods will be divided to four kinds of state: the added/changed/does/unchanged. podfile_state = generate_podfile_state store_existing_checkout_options if allow_fetches == :outdated # special-cased -- We're only really resolving for outdated, rather than doing a full analysis elsif allow_fetches == true # > > < p style = "padding-bottom: 0px; line-height: 20px; Local libraries introduced in path are processed by a class called PathSource, which returns the PodSpec address. You can override this class to re-specify the local PodSpec path. # Downloading through git mode is a bit special. We automatically fetch a pre_download method to enter the pre-downloading stage, download the pod library in advance, and then download the PodSpec for analysis. fetch_external_sources(podfile_state) elsif ! dependencies_to_fetch(podfile_state).all? (&:local?) raise Informative, 'Cannot analyze without fetching dependencies since the sandbox is not up-to-date. Run `pod install` to ensure all dependencies have been fetched.' \ "\n The missing dependencies are:\n \t#{dependencies_to_fetch(podfile_state).reject(&:local?) .join("\n \t")}" end # Returns information about the Pods library in the lockfile. The version and dependencies in podfile.lock will be read first after pod install. This is why executing pod Install does not update the local library version. Locked_dependencies = generate_version_locking_dependencies(podfile_state) # return the POD library dependencies configured in podfile # Resolver_specs_by_target = resolve_dependencies(locked_dependencies) Whether the value is greater than or equal to the minimum version supported by the POD library validATE_platforms (resolver_specs_by_target) # hashmap to array specifications = Generate_specifications (resolver_specs_by_target) Not only aggregate_targets in podfile, pod_targets = generate_targets(resolver_specs_by_target, Target_conforms) # state sandbox_state = generate_sandbox_state(specifications) # specs_by_target = resolver_specs_by_target.each_with_object({}) do |rspecs_by_target, Hash | hash [rspecs_by_target [0]] = rspecs_by_target [1]. The map (& : spec) end # according to the source of the source for the spec of the key packet hash specs_by_source = Hash[resolver_specs_by_target.values.flatten(1).group_by(&:source).map do |source, specs| [source, Specs. The map (& : spec). Uniq] end] sources. Each {| s | specs_by_source [s] | | = []} # all the analysis results of pod library storage to @ result = the result attribute of the analyzer  AnalysisResult.new(podfile_state, specs_by_target, specs_by_source, specifications, sandbox_state, aggregate_targets, pod_targets, @podfile_dependency_cache) endCopy the code

During dependency analysis, a hash called resolver_specs_by_target is generated that contains information about all the targets configured in the Podfile and all the pod libraries under those targets. The whole process is complicated, and the entire dependency relationship is determined at the end of the process. Internally, all the Target-dependent POD libraries are built into ResolverSpecification objects based on the generated Molinillo diagram. In [Hash {Podfile: : TargetDefinition = > Array < ResolverSpecification >}] in the form of storage.

Podspec switch

By hook resolver_specs_by_target, all podspecs are traversed to find the pod library marked with binary tags and reassembled to switch between source code and binary. This is the core idea behind Cocoapods-bin:

cocoapods-bin/native/resolver.rb

old_resolver_specs_by_target = instance_method(:resolver_specs_by_target)
define_method(:resolver_specs_by_target) do
  specs_by_target = old_resolver_specs_by_target.bind(self).call
  ...
end
Copy the code

Swizzling is a Method similar to OC. Instance_method temporarily stores the resolver_specs_by_target Method. Define_method is a Method similar to def. Define_method generates a new method, like class_addMethod, which dynamically generates methods based on parameters. There are other Ruby exchange methods alias, alias_method, etc.

After obtaining specs_by_target, extract Specification object Rspec and binary source, and construct binary Specification object through Specification method of source:

cocoapods-bin/native/resolver.rb

begin specification = source.specification(rspec.root.name, spec_version) #... Omit some code # assemble new rSpec, Replace the original rspec rspec = ResolverSpecification. New (specification, used_by_only. Source) rSpec rescue Pod::StandardError => e # Omit some code rspec endCopy the code
# cocoapods-core/source.rb
def specification(name, version)
  Specification.from_file(specification_path(name, version))
end
Copy the code

Ruby’s Class defines methods with self as Class methods, such as self.from_file defined in Specification.

If specification is not obtained, the rescue code block will be entered and rspec will be returned directly.

The whole process can be summarized as follows:

  1. Gets the components of set_USe_source_PODS in a custom file.
  2. Filter out components that require binary.
  3. Gets the source source of the binary REPO.
  4. By the sourcespecification(name, version)Method to return to Specification.
  5. If no corresponding PodSpec is found in the binary source, the source podSpec is returned.
  6. If there is through ResolverSpecification. New method to construct new ResolverSpecification returns.

With a custom DSL and hook resolver_specs_by_target, the configuration and switch between source code and binary can be completed without invading the original podfile.

About Cocoapods source processing

As you can see from the function call stack, Cocoapods uses a method called find_cached_set to find podSpecs:

#cocoapods/resolver.rb def find_cached_set(dependency) name = dependency.root_name cached_sets[name] ||= begin if dependency.external_source spec = sandbox.specification(name) unless spec raise StandardError, '[Bug] Unable to find the specification ' \ "for `#{dependency}`." end set = Specification::Set::External.new(spec) else  set = create_set_from_sources(dependency) end unless set raise Molinillo::NoSuchDependencyError.new(dependency) # rubocop:disable Style/RaiseArgs end set end endCopy the code

Set is all the versions of the POD library in the REPO, where Cocoapods filters podfile.lock for installation. The create_set_from_sources method calls a method called search inside:

#cocoapods-core/source/aggregate.rb

def search(dependency)
  found_sources = sources.select { |s| s.search(dependency) }
  unless found_sources.empty?
    Specification::Set.new(dependency.root_name, found_sources)
  end
end
Copy the code

Sources within the current operating environment all repo, the method will iterate over all repo, find the corresponding pod all versions of find is returned, not found will throw an exception raise, also is the error of common console as follows:

[!]  Unable to find a specification for `HelloMoto` You have either: * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`. * mistyped the name or version. * not added the source repo that hosts the Podspec to your Podfile.Copy the code

So where does sources, where all the REPO’s come from? This code can be cocoapods/installer/analyzer. The rb found in:

# cocoapods/installer/analyzer. Rb def sources # lazy loading. # @ sources | | = begin podfile reads. Sources = podfile.sources # Plugin defines the source source. Plugin_sources = @ plugin_sources | | [] # podspec use: source is introduced into the source. dependency_sources = podfile_dependencies.map(&:podspec_repo).compact ... Omit some logic result end endCopy the code

From the above code, you can see that the source has three sources:

  1. Source ‘XXX’ defined in podfile.
  2. The source returned by plug-in source_provider hook.
  3. The POD library is introduced through :source.

The run_source_PROVIDer_hooks method, which internally executes the plug-in’s source_provider hook method, returns the plugin-defined repO to @plugin_sources:

cocoapods-bin/source_provider_hook.rb

Pod::HooksManager.register('cocoapods-bin', :source_provider) do |context, _ | sources_manager = Pod: : Config. The instance. The sources_manager podfile = Pod: : Config. The instance. The podfile if podfile # add binary private sources Added_sources = [sources_manager.code_source, sources_manager.binary_source, sources_manager.trunk_source] if podfile.use_binaries? || podfile.use_binaries_selector added_sources.reverse! end added_sources.each { |source| context.add_source(source) } end endCopy the code

If a private REPO does not want to be defined in the Podfile, it can be returned this way within the plug-in.

conclusion

This article is a long one. At the beginning, it introduces componentized architecture and the process of component privatization. Component communication is not mentioned here, which can be referred to “Thoughts on iOS Component Communication”. Then two binary schemes are analyzed, focusing on the double REPO scheme. This paper introduces the Ruby tool chain system to understand the making principle of Cocoapods Plugin. Cocoapods and Cocoapods-bin source code are analyzed in the following section, including almost all the key nodes involved, in the hope that readers can deeply understand how the binary plug-in is developed around Cocoapods source code. Finally, we introduce the podspec and PodFile loading process, pod Install dependency analysis process, and some Ruby features, such as how to use the Open Class feature for podfile extension DSL and replacement methods. In addition, we recommend a customized version of Cocoapods-IMy-bin based on cocoapods-bin, which is open source in Apple.

Ref:

  1. CocoaPods
  2. cocoapods-bin
  3. IOS CocoaPods component smooth binary solution
  4. Kudos to iOS- Binary-based compilation efficiency strategy
  5. 1. Version management tools and Ruby toolchain environment