background

Our company mainly focuses on projects. In the process of doing projects, it is inevitable to integrate third-party SDKS, such as face recognition, instant messaging, etc. Third-party SDKS are often relatively large. In order to save SVN hard disk resources, the company does not allow the SVN to submit files larger than 50 MB. However, these SDKS may be about 100MB+, which is not good for the management of third-party SDKS. We store SDKS on the FTP server, which also causes difficulties for the front-line project developers. They cannot run the project successfully through POD install once. You need to execute POD install first, then download the third-party SDK from the FTP server, then move the SDK to the corresponding component directory, and execute POD install again, so that the project can run properly. This will only increase the cost of project development. This is the reason for this research on cocoapods plug-in development.

process

Cocoapods plugin development allows developers to hook one of the lifecycle in the process of pod install, which is exactly what I want, then I need to learn the following techniques:

  • Cocoapods plug-in development process
  • Ruby syntax
  • Ruby file manipulation
  • Ruby FTP operation

First I need to learn the cocoapods plugin development process

Baidu can search most of the articles on the Internet, written very full.

www.jianshu.com/p/5889b25a8…

Step 1: Install the CocoapoDS-Plugins component

gem install cocoapods-plugins

Step 2: Create a Cocoapods Plugins template project

pod plugins create githooks

After performing the above operations, you will get a Cocoapods Plugins project directory, you can choose WebStorm, VSCode to develop, here I use VSCode, after installing the Ruby plug-in, development is more smooth.

Step 3: Modify the cocoapods-githooks.gemspec file

# Modify this line of code
spec.files         = 'git ls-files'.split($/)

# modified to
spec.files = Dir['lib/**/*']
Copy the code

Step 4: Compile the plug-in

Sudo gem build cocoapods-githooks. Gemspec The following information is displayed

WARNING:  no author specified
WARNING:  open-ended dependency on rake (>= 0, development) is not recommended
  if rake is semantically versioned, use:
    add_development_dependency 'rake', '~> 0'
WARNING:  See http://guides.rubygems.org/specification-reference/ for help
  Successfully built RubyGem
  Name: cocoapods-githooks
  Version: 0.0.1
  File: cocoapods-githooks-0.0.1.gem
Copy the code

When compiled successfully, you will get the cocoapods-githooks-0.0.1.gem file

Step 5: Plug-in installation

Sudo gem install cocoapods - githooks - 0.0.1. Gem

The installation is successful if the following information is displayed

Zhudezzhendemacbook-pro :cocoapods-githooks zhudezhen$sudo gem install cocoapoDS-githooks-0.0.1. Gem Successfully Cocoapods-githooks-0.0.1 Installing RI documentation for installed CocoapoDS-Githooks-0.0.1 Cocoapods-githooks 0.0.1 Done installing documentation for cocoapoDS-githooks after 0 seconds 1 gem installedCopy the code

You can verify that the plug-in has been installed successfully by running the following command

pod plugins installed

If your plugin is found in the list, the installation is successful

zhudezzhendeMacBook-Pro:TestWKWebView zhudezhen$ pod plugins installed [!]  The specification of arguments as a string has been deprecated Pod::Command::Vendors: `NAME` [!]  The specification of arguments as a string has been deprecated Pod::Command::Githooks: 'NAME' Installed CocoaPods Plugins: -cocoapods-clean: 0.0.1 - Cocoapods-deintegrate: 1.0.4 - Cocoapods-FTP-vendors: 0.0.1 (pre_install hook) -cocoapods-githooks: 0.0.1 (post_install hook) -cocoapods-plugins: 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.0Copy the code

To grasp the cocoapods plug-in development process, of course, there is no pod install hook.

Hook pod Install

Ruby grammar learning, basically from the novice tutorial to see, here do not expand

www.runoob.com/ruby/ruby-s…

Step 1: Modify the plugin code

Modify the directory ‘lib/cocoapods_plugins.rb’ as follows:

require 'cocoapods-githooks/command'
# Reference the Cocoapods package
require 'cocoapods'

module CocoapodsGithooks
	  # Register the Pod Install hook
    Pod::HooksManager.register('cocoapods-githooks'.:post_install) do |context|
        p "hello world!"
    end
end
Copy the code

Note: The module name is derived from the version field of cocoapods-githooks.gemspec. The hook name must be the same as the component name.

After the modification is complete, recompile and install it. To facilitate debugging, you can run the following command to reexecute each modification

Gem && sudo gem build cocoapods-githooks.gemspec && sudo gem install Cocoapods githooks - 0.0.1. GemCopy the code

Step 2: Change the project Podfile file

You need to add the following code at the top of the project’s Podfile:

platform :ios.'9.3'

# Add code here
plugin 'cocoapods-githooks'

target ....
Copy the code

Ok, let’s execute pod install and give it a try

zhudezzhendeMacBook-Pro:TestWKWebView zhudezhen$ pod install [!]  The specification of arguments as a string has been deprecated Pod::Command::Vendors: `NAME` [!]  The specification of arguments as a string has been deprecated Pod::Command::Githooks: 'NAME' Analyzing Dependencies Downloading dependencies Using NAVIGATION (1.1.0) Using Arcface (1.2.0) Generating Pods project Integrating client project "hello world!" Sending stats Pod installation complete! There is 1 dependency from the Podfile and 2 total pods installed. zhudezzhendeMacBook-Pro:TestWKWebView zhudezhen$Copy the code

We see “Hello World!” .

Ruby FTP connection to the server

Ruby has its own FTP module, you can directly connect to the FTP server, here need to pay attention to, if the connection is Window FTP server, you need to perform utF-8 -> GB2312 conversion parameters.

Tencent cloud document: cloud.tencent.com/developer/s…

The code for Ruby to connect to FTP module is as follows:

require "net/ftp"
ftpclient = Net::FTP.new
ftpclient.connect(host, port)
ftpclient.passive = true
ftpclient.login(username, password)
p ftpclient.nlst
Copy the code

After the above code is executed, you can successfully connect to the FTP server, if you connect to the Window server, you need to convert the encoding, directoryname.encode (” utF-8 “, “GB2312 “), when you need to access the FTP specified directory, Encode (“/my component library/component 1/ component 2/ abcD.framework “.encode(“gb2312”, “utF-8 “)

You can obtain information about the FTP server through the system command

p ftpclient.system
# print Window_NT
Copy the code

To download a file through getBinaryFile, the path problem is the same as above.

Ftpclient. getBinaryFile File pathCopy the code

Since the *. Framework static libraries are special folders in iOS, we need to recursively download the static libraries:

def self.d_f_p(ftp_key = ' ', path = ' ', rootpath = ' ')
  Get FTP client from connection pool
  ftp = FtpClient.ftp_connect_pool[ftp_key]
  if! ftpreturn nil;
  end
  # Obtain file name
  subpaths = path.split("/")
  filename = subpaths[subpaths.size - 1];
  # Create directory first
  subfiles = ftp.nlst(path)
  # Determine whether it is a folder or a file
  if! ((subfiles.size ==1) && (subfiles[0] = ="#{path}/#{filename}"))
      If it is a directory, build the directory
  		buildDir = "#{rootpath}/#{filename}"
  		if! (File.directory? buildDir) Dir.mkdir buildDirend
  
  		# Get subdirectory
  		subfiles = ftp.nlst path
      
      # Get directory subfile
  		for sf in subfiles do  
  			d_f_p(ftp_key, sf, buildDir + "/")
  		end 
  else 
    # Ordinary file, directly download
  	filesize = ftp.size path;
  	ftp.getbinaryfile(path, "#{rootpath}/#{filename}", filesize)
  end
end
Copy the code

With this code, you can automatically download the Framework static library.

Series overall process

After solving the above problems, we began to series the overall process. The general process is as follows:

The configuration file

FTP configuration information. At first, there are two ideas:

  • The configuration is in the podspec file of the corresponding component
  • The configuration is in the corresponding project’s Podfile file

I chose the latter. If I chose the former, all the components that need FTP will be modified, while the latter can be modified in the form of plug-ins.

At first I put the FTP information after the corresponding Podfile component, as follows:

platform :ios.'9.3'
plugin 'cocoapods-githooks'

target 'TestWKWebView' do
  
    pod 'sm'.:path= >'xxxx/sm2'.:ftp= > {"host"= >"xxx.xxx.xxx.xxx"."port"= >"xxxxx"."user"= >"xxxx"."pwd"= >"xxxx"."vendors"= > [{"path"= >"/ XXXXXX/XXXX / ~ 1.0.11 1.0.1. A/XXXX. The framework"."destination"= >"sm2" ,"version"= >"1.0.1"}}]end
Copy the code

This configuration is available in the Cocoapods Plugin and pod install succeeds. However, this configuration fails in the Pod install as follows:

platform :ios.'9.3'
plugin 'cocoapods-githooks'

target 'TestWKWebView' do
  
    pod 'sm'.:svn= >'svn:xxxx:xxx/xxx/xxxx/sm2'.:tag= >'xxx.xx'.:ftp= > {"host"= >"xxx.xxx.xxx.xxx"."port"= >"xxxxx"."user"= >"xxxx"."pwd"= >"xxxx"."vendors"= > [{"path"= >"/ XXXXXX/XXXX / ~ 1.0.11 1.0.1. A/XXXX. The framework"."destination"= >"sm2" ,"version"= >"1.0.1"}}]end
Copy the code

No matter what I do, I will eventually fail in the Pod install, so I had to change my mind and read the FTP information in the form of a configuration file, which is completely isolated from the Podfile.

The contents of the file are as follows:

{
   "arcface": {"host":"Xxx.xxx.xxx. XXX [FTP server address]"."port":"XXXXX [FTP port]"."user":"XXXX [FTP account]"."pwd":"XXXX [FTP password]"."vendors":[
           {
              "path":"/ XXXX XXXX/iOS / / ArcSoftFaceEngine 1.0.1 framework components to the absolute path to the FTP 】 【"."destination":"XXXX/XXX [local storage path for components, relative to engineering Pods/]"."version":"1.0.1 [Component version information]"}]}}Copy the code

After reading from the configuration file, pod install succeeds and the configuration information is obtained from the plug-in.

Cocoapods Context Context

In the hook callback method, there is a context variable. This is obviously the context of the current hook, and you need to understand what is in the context:

Cocoapoads API online documentation: www.rubydoc.info/github/coco… This piece of content, less online, can only read their own documents to learn.

By reading the API, you can obtain the project poDS_project, sandbox, sandbox_root, and umbrella_targets in the context. Post_install Hook, pre_install, :post_integrate, :source_provider Among them: Post_integrate only cocoapods 1.10.0+ version support, due to the characteristics of the current company, we can only maintain the cocoapods 1.4.0 version, which brings components after moving, engineering synthesis problems. Each hook has its own context, and the context carries different information, so you need to be careful here. By now, it’s possible to understand the data carried in the context, so I did a search for “Ruby reflections” and found that in the Runtime phase, calling context.methods gets all the methods of the current Ruby object, which is a critical step in the process.

Engineering synthesis problem

After the process is connected in series, very happy, test, the first pilot large project project, FTP download, component movement, POD install all smoothly, when running the project, found compilation failed, the reason is the third party static library is not referenced, very strange, I have moved the component to the specified directory? I downloaded the source code of Cocoapods 1.4.0 from Github and found that post_install will be executed after the whole project is completed. The static library was not automatically referenced into the Pods Target engineering configuration, causing the above problem.

Next, prepare to analyze the POD Install process. Getting cocoapods from the source code will perform the following process:

def install!
	prepare
	resolve_dependencies
	download_dependencies
	validate_targets
	generate_pods_project
	if installation_options.integrate_targets?
		integrate_user_project
	else
		UI.section 'Skipping User Project Integration'
	end
	perform_post_install_actions
end
Copy the code

The generate_pods_project method is already building the Pods Target project, and our hook executes during perform_post_install_Actions, so it is not referenced in the project. Then we can use :pre_install to Hook? Because the context is different, the code needs to be modified. After the modification is completed, the compiler fails again, but this time the download_dependencies method washes out the component after it has been moved.

Next, start reading the source code of Generate_POds_project

def generate_pods_project(generator = create_generator)
	UI.section 'Generating Pods project' do
		generator.generate!
		@pods_project = generator.project
		run_podfile_post_install_hooks
		generator.write
		generator.share_development_pod_schemes
		write_lockfiles
	end
end
Copy the code

I noticed the run_podfile_post_install_hooks method in the source code, which is a hook, and then look down

# Runs the post install hooks of the installed specs and of the Podfile.
#
# @note   Post install hooks run _before_ saving of project, so that they
# can alter it before it is written to the disk.
#
# @return [void]
#
def run_podfile_post_install_hooks
	UI.message '- Running post install hooks' do
		executed = run_podfile_post_install_hook
		UI.message '- Podfile' if executed
	end
end

# Runs the post install hook of the Podfile
#
# @raise  Raises an informative if the hooks raises.
#
# @return [Boolean] Whether the hook was run.
#
def run_podfile_post_install_hookpodfile.post_install! (self)
	rescue => e
		raise Informative, 'An error occurred while processing the post-install ' \
			'hook of the Podfile.' \
			"\n\n#{e.message}\n\n#{e.backtrace * "\n"}"
end
Copy the code

Post_install: post_install: post_install: post_install: post_install: post_install: post_install: post_install: post_install

pre_install do |installer|
   p "hello world!"
end
Copy the code

You can output successfully here.

How do you get the results of your Cocoapods Plugins calculations here? Here I solve the problem of cross-code communication by generating a.lock file. First, we have to download the component from :pre_install and write the component movement information to the.lock file. Then we read the.lock file information from the hook in the Podfile. Move the component directory and finally add the following code at the end of the Podfile file:

def download_from_ftpandmove
  If the lock file does not exist, the lock file is not executed
  if! File.exists? ("PodfileFtp.lock")
      return ;
  end
  # Read json file
  require "json"
  file = File.open   "PodfileFtp.lock"
  vendor_plugins = JSON.load file
  # Traverse the component and move it to the specified directory
  for item in vendor_plugins do
      from = item["from"]
      to = item["to"]
      # Create directory
      if! File.directory? (to) FileUtils.mkdir_p toend
      # Copy move
      FileUtils.cp_r(from, to)
   end
end

pre_install do |installer|
   download_from_ftpandmove
end
Copy the code

Finally, after re-executing pod install, the project downloads the automatic reference component, compilers and runs successfully!