Getting Started with CocoaPods Plugin Development

Image for post
Image for post

Featured in 2013 CocoaPods plugins became a powerful and elegant tool to extend CocoaPods with useful tweaks and commands. We’ve built a simple CocoaPods plugin that synchronizes git hooks between team members and want to share our development experience so you can make one for your own needs.

Before we start

Image for post
Image for post

CocoaPods plugin is a Ruby gem, you will need Ruby and CocoaPods installed to start development your plugin.

To get started developing a new plugin you also need to install cocoapods-plugins gem by running:

$ gem install cocoapods-plugins

For demo and debug purposes let’s create a new Xcode project with git repository:

Xcode > File > New > Project > Single View Application and don’t forget to check ‘Create git repository’ or create it manually by running:

$ git init

Now we can add Pods to our project:

$ pod init

Or use our demo project we’ve made to develop cocoapods-githooks plugin.

Let’s create CocoaPods plugin

Now is the time to create our new plugin by running

$ pod plugins create githooks

In this tutorial, we use the same name as we used for our plugin. It will not make any difference except you will not be able to publish the plugin with the same name.

This command creates a boilerplate for CocoaPods plugin.

Image for post
Image for post

.gemspec file is the main config file for our plugin. All spec fields are pretty self-describing (you can check out gemspec of our plugin here), but I would like to point out one thing:

By default, spec.files refer to all files in git repository index and working tree. But if you try to build gem when there are no files in git you will get an empty .gem file with no warning or error. My advice is to set the value of spec.files to Dir[‘lib/**/*’], it will refer to all files inside lib directory.

Replace spec.files line with

spec.files = Dir['lib/**/*']

Gemfile contains all gem dependencies required to execute associated
Ruby code. Check Bundler Docs for more information and

Rakefile contains references on test specs and spec folder contains test specifications. For more information about Rake visit official Rake repo.

We won’t need to change Gemfile and Rakefile, so leave them as is.

lib is the main folder we’re going to work with. It contains all ruby files that will be built into a .gem file.

To make sure everything is set correctly, run:

$ gem build cocoapods-githooks.gemspec

You will see ‘Successfully built RubyGem’ message. Cocoapods-githooks-0.0.1.gem file will appear in a root directory as well.

Image for post
Image for post

Time to make it useful

Cocoapods-githooks plugin does one simple thing, it copies all files from .git-hooks directory of your project into .git/hooks/ directory and makes them executable every time you are running ‘pod install’, ‘pod update’ or ‘pod githooks’.

To achieve this we’ll create a class that does all files manipulations and link it to pre-install, post-install CocoaPods hooks and githooks command.

Create githooks-sync.rb file in /lib/cocoapods-githooks

Require ‘cocoapods’ and ‘fileutils’ modules and create GitHooksSync class with sync method inside CocoapodsGitHooks module.

require 'cocoapods'
require 'fileutils'
module CocoapodsGitHooks
class GitHooksSync
def sync

end
end
end

Before copying git-hooks we need to check a few things:

  1. Make sure we’re in a git repository
  2. Make sure .git-hooks directory exists
  3. Make sure .git-hooks directory is not empty

We can achieve this by adding next conditional statements at the beginning of sync method:

if !File.directory?(".git")
Pod::UI.puts "Git repository not found"
return
end
if !File.directory?(".git-hooks")
Pod::UI.puts ".git-hooks directory not found, nothing to sync"
return
end
if Dir['.git-hooks/*'].empty?
Pod::UI.puts ".git-hooks directory is empty, nothing to sync"
return
end

After that, we check if hooks directory exists inside .git and if it is not, force it’s creation:

if !File.directory?(".git/hooks")
FileUtils.mkdir ".git/hooks"
end

Now we are ready to copy our hooks from .git-hooks into .git/hooks:

FileUtils.cp_r(“.git-hooks/.”, “.git/hooks/”)

Next step is to remove shell script file extension (if it is present) and make files executable:

path = ".git/hooks/"
Dir.open(path).each do |p|
filename = File.basename(p, File.extname(p))
if File.extname(p) == ".sh"
FileUtils.mv("#{path}/#{p}", "#{path}/#{filename}")
end
FileUtils.chmod("+x", "#{path}/#{filename}")
end

We’ve also added two UI.puts at the beginning and the end of sync method to show system notifications with sync status. Following all steps you’ll get githooks-sync.rb file:

require 'cocoapods'
require 'fileutils'
module CocoapodsGitHooks
class GitHooksSync
def sync
Pod::UI.puts "Synchronizing git hooks"
if !File.directory?(".git")
Pod::UI.puts "Git repository not found"
return
end
if !File.directory?(".git-hooks")
Pod::UI.puts ".git-hooks folder not found, nothing to sync"
return
end
if Dir['.git-hooks/*'].empty?
Pod::UI.puts ".git-hooks folder is empty, nothing to sync"
return
end
if !File.directory?(".git/hooks")
FileUtils.mkdir ".git/hooks"
end
FileUtils.cp_r(".git-hooks/.", ".git/hooks/")
path = ".git/hooks/"
Dir.open(path).each do |p|
filename = File.basename(p, File.extname(p))
if File.extname(p) == ".sh"
FileUtils.mv("#{path}/#{p}", "#{path}/#{filename}")
end
FileUtils.chmod("+x", "#{path}/#{filename}")
end
Pod::UI.puts "Git hooks synchronized"
end
end
end

Now let’s invoke sync method after every ‘pod install’ or ‘pod update’. To do this we need to register post_install and post_update hooks in Pod HooksManager. Open lib/cocoapods_plugin.rb and write:

require 'command/githooks'
require_relative 'githooks-sync'
module CocoapodsGitHooks
Pod::HooksManager.register('cocoapods-githooks', :post_install)
do |context|
GitHooksSync.new.sync()
end
Pod::HooksManager.register('cocoapods-githooks', :post_update)
do |context|
GitHooksSync.new.sync()
end
end

As you can see it pretty straightforward. We register post_install and post_update hooks in HooksManager that will invoke sync method of GitHooksSync class every time after the user runs ‘pod install’ or ‘pod update’.

Image for post
Image for post

Build cocoapods-githooks gem by running:

$ gem build cocoapods-githooks.gemspec

And install it by running:

$ gem install cocoapods-githooks-0.0.1.gem

If you have problems with permissions, add user-install flag:

$ gem install cocoapods-githooks-0.0.1.gem --user-install

Run:

$ pod plugins installed

to make sure it was installed properly. It should appear in the list of installed plugins:

- cocoapods-githooks    : 0.0.1 (post_install and post_update hooks)

Do not worry if you see deprecation warning, we will fix it in the next steps.

Now let’s go back to our test project. Open Podfile and add at the beginning:

plugin ‘cocoapods-githooks’

Run:

$ pod install

You should see two lines we put in GitHooksSync:

Synchronizing git hooks
.git-hooks directory not found, nothing to sync

It’s because we didn’t create .git-hooks directory. Create an empty pre-commit.sh file and put it into .git-hooks directory (or download it from our demo project).

Run:

$ pod update

You should see that git hooks were synchronized:

Synchronizing git hooks
Git hooks synchronized

Now check .git/hooks directory, it should contain pre-commit executable file.

Image for post
Image for post

Adding new commands is very easy. You just need to create a subclass of the command class. Open lib/cocoapods-githooks/command/githooks.rb and replace it’s content with:

require 'cocoapods'
require 'cocoapods-githooks/githooks-sync'
include CocoapodsGitHooksmodule Pod
class Command
class Githooks < Command
self.summary = <<-SUMMARY
Syncs hooks between team members
SUMMARY
self.description = <<-DESC
CocoaPods plugins that syncs git-hooks placed in .git-hooks directory between team members
DESC
self.arguments = [] def run
CocoapodsGitHooks::GitHooksSync.new.sync()
end
end
end
end

It’s simple githooks command that takes no argument and calls sync method.

Now rebuild and reinstall gem:

$ gem build cocoapods-githooks.gemspec
$ gem install cocoapods-githooks-0.0.1.gem

Remove hooks directory inside .git and run:

$ pod githooks

It’s release time

Please keep RubyGems clean. Avoid publishing test and demo gems.

Releasing CocoaPods plugins is two step process. First, you need to create an account and publish your gem to RubyGems.org. Now you can install your plugin directly from RubyGems:

$ gem install cocoapods-PLUGIN_NAME

If you want your plugin to be listed in official cocoapods plugins list, run:

$ pod plugins publish

It will create an issue in the cocoapods-plugins GitHub repository to ask for your plugin to be added to the official list. To speed up the process feel free to fork cocoapods-plugins, add generated json object as the last object in plugins array in plugins.json file and create a pull request.

Conclusion

Image for post
Image for post

We’ve built a very simple CocoaPods plugin, but there are many more things you can dousing this powerful tool. If you look for inspiration for your future projects, browse more than 50 plugins available right now by running:

$ pod plugins list

Feel free to leave any questions or advices in a comment section below.

iOS Developer at SPD-Ukraine

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store