Rome + Carthage = Build Your Dependencies Faster

Photo of Łukasz Pikor

Łukasz Pikor

Updated Oct 19, 2023 • 7 min read
Lukasz Pikor, Lisbon 2018

As you may know, at Netguru we work on many different applications at the same time. It’s not uncommon for multiple projects to use the same dependencies, such as Kingfisher, PromiseKit, etc.

What is more, not every library provides itself as a zip file. In that situation, Carthage has to compile the dependency, and that of course takes time. Multiple this by X, where X is the number of projects in your organization that use the same library and, in case the cache is empty on your CI server, you lost quite a lot of time. But there is a way to make it faster. Omnes viae Romam ducunt.

Rome is a cache tool for Carthage. It means it can download already built dependencies so that Carthage doesn't have to compile them.

There are three main benefits of using central a cache repository:

  • faster CI builds,

  • shorter CI queue, which is a result of the first point,

  • faster checkout builds on developer’s machines.

Producer/Consumer

There are two workflows when using Rome: producer and consumer, and they are very simple to understand.

Producer

  • Creates a Cartfile or updates it if it's already created

  • Runs carthage update && rome upload

In general: Producer’s goal is to produce dependencies by compiling them on a machine with Carthage and upload it to the central cache using Rome.

Consumer

  • Downloads dependencies built by Producer using rome download.

Note: Consumer can also be a Producer. It may happen that some dependencies are missing from the cache. Rome allows us to list and update those.

From Rome’s readme:

rome list --missing --platform ios | awk '{print $1}' | xargs carthage update --platform ios --cache-builds # list what is missing and update/build if needed
rome list --missing --platform ios | awk '{print $1}' | xargs rome upload --platform ios # upload what is missing

You can read more about Rome workflows here.

Setup

After installing Rome through Homebrew, you can use the rome command in your terminal. Alternatively, it’s possible to set up Rome as a Fastlane plugin. We’ll use both approaches, mixed together.

Romefile

A Romefile to Rome is what a Fastfile is to Fastlane, a Cartfile is to Carthage, a Podfile is to CocoaPods, and a Gemfile is to Bundler.

Also, it's in the YAML format. In its simplest form, it would look like this:

cache:
  s3Bucket: carthage-rome

cache is the only required keyword. You can read more about Romefiles here.

Ignore map

If we want, we can ignore some dependencies. Like this:

ignoreMap:
  - xcconfigs:
      - name: xcconfigs

The ignore map will be very helpful and crucial in some situations. We’ll get back to it in a minute. More about the ignore map here.

Repository map

This is an interesting Rome feature. To understand it well, we'll use the RxSwift repo. As you probably know, the RxSwift project has a few targets in it. Apart from the obvious one (RxSwift) it contains RxBlocking and RxCocoa. A simple carthage update && rome upload will only upload RxSwift into cache. Thankfully, there’s the repository map which allows us to "map repository and framework names", but also helps us specify which frameworks should be copied into the cache.

repositoryMap:
  - RxSwift:
	  - name: RxSwift
	  - name: RxCocoa
	  - name: RxBlocking

More about the repository map.

As you can see, Romefiles are rather short and easy to grasp. We only need the required cache setting; ignoreMap and repositoryMap can be used if needed, per project-specific requirements.

Using Rome with Fastlane

At least three different lanes will be needed to handle Rome in Fastlane. Here they are.

lane :carthage_update do
  carthage(
    command: "update",
    platform: "iOS",
    cache_builds: true, # This is Carthage's cache command, it has nothing to do with Rome cache.
  )
  rome(
    command: "upload",
    platform: "iOS",
  )
end
 
lane :carthage_bootstrap do
  carthage(
    command: "bootstrap",
    platform: "iOS",
    no_build: true,
  )
  rome(
    command: "download",
    platform: "iOS",
  )
end
 
lane :carthage_install_missing do
  sh("(cd ..; rome download --platform iOS)")
  sh("(cd ..; rome list --missing --platform ios | awk '{print $1}' | xargs carthage update --platform ios --cache-builds)")
  sh("(cd ..; rome list --missing --platform ios | awk '{print $1}' | xargs rome upload --platform ios)")  

end

carthage_install_missing

You may be wondering why lane carthage_install_missing calls Rome directly, using the sh command, and not by using the Fastlane command or plugin.

Rome uses two different commands to list missing dependencies (rome list --missing) and to build and upload them (carthage update + rome upload).

Since it's not possible to pass the output of one Fastlane command to another, we have to use Rome through sh.

Using Rome within bitrise.yml

At Netguru we use bitrise.io, so bitrise.yml is a natural habitat for scripts we use in the build system. Because there is no Bitrise step for Rome, we need to use it in the same way as any other script. There will be one additional step, which is installing Rome via Homebrew. So if we wanted want to define a run-rome step that lists the missing dependencies, it would look like this:

run-rome:
    before_run:
      - install-rome
      - rome-list-missing

#We also need to define two steps, install-rome & rome-list-missing: 
  install-rome:
    steps:
    - script:
           inputs:
              - content: |
                  #!/bin/bash
                  brew install blender/homebrew-tap/rome

  rome-list-missing:
     steps:
       - script:
          title: 'rome-list-missing'
          inputs:
             - content: rome list --missing

You can pass any other Rome commands as content.

How does Rome work with forked repositories?

Neither Carthage nor Rome recognize the namespaces (GitHub user accounts) which hold dependencies. For Carthage, and thus Rome, github "Alamofire/Alamofire" is the same as github "JaneDoe/Alamofire". This is a potentially dangerous situation where the forked repository will be distributed to other projects. To fix this issue, we must use IgnoreMap to avoid caching forked repositories.

Of course, caching is not a trivial thing, YMMV, but so far it has worked well for us.

Photo: Lukasz Pikor

Photo of Łukasz Pikor

More posts by this author

Łukasz Pikor

His journey with programming started in 2006 when he was still in high school. After taking his...
Lost with AI?  Get the most important news weekly, straight to your inbox, curated by our CEO  Subscribe to AI'm Informed

Read more on our Blog

Check out the knowledge base collected and distilled by experienced professionals.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business