A few months ago, we decided to make a small revolution in our stack – to change the dependency manager from Cocoapods to Carthage. This decision, however, gave us one big disadvantage: it became necessary to build all the dependencies before development. The amount of time developers spend on building all of the dependencies is very high, therefore it is necessary to reduce it. So let us begin, and see how we can accomplish that.

Carthage

Carthage is a decentralized dependency manager. When you face the task of integrating third party libraries into your project, you have a few possibilities, two of the most popular being Carthage and Cocoapods, of which we have already covered, so check out our blog post to help you with your choice.

What is troubling us

The first few days after transitioning from Cocoapods to Carthage was great. Build time rapidly decreased, and the code that had been built by Xcode was really the code that was inside the project, and the syntax highlight stopped crashing. The only issue we faced was when one of our colleagues updated one of the dependencies and all the other developers on the same project had to build the dependency again.

Another issue happened on the CI (Continuous Integration). The command cathage bootstrap was executed twice, once for the tests, and once for the build itself and the following distribution. Our CI is Mac Mini, which isn't the fastest Mac available, building a project with Realm for instance take almost one hour in total. That's inadmissible.

Frameworks on the Github

This problem was partially solved by uploading the final framework to the GitHub. The command cathage bootstrap  was then able to download it and use it. Using this "trick" we could speed up the build of some of the dependencies, but not all of them. This solution became unusable when the new version of Swift came out. It was necessary for all developers to upload the new version of the framework that was built using the new Swift version. Most developers didn’t do that, and creating new issues on the GitHub in which we begged for the new version wasn't successful.

The cache

Those were the reasons why we chose to create our custom solution to this problem. And that’s when we found Rome. Rome is a Carthage caching framework which can cache frameworks locally on your machine and also remotely. The tool itself supports services that use Amazon S3 as a storage. You can find the tutorial how to setup the storage on Rome's GitHub page.

Romefile

The cache uses one config file named Romefile that is located in the project's root. In this file you define the name of the bucket to which frameworks are uploaded and the path where the frameworks are locally stored. If you don't define the bucket's name, Rome will use the local cache only by default.

Mapping

A very crucial thing for the correct work of the cache is mapping. Mapping specifies how to map a repository name to the framework name. In some cases, these names can vary. Another mapping problem appears when you build multiple frameworks from one repository. In both cases you have to specify the custom mapping in the MappingRepository section in the Romefile.

Working with the cache

Working with the cache is simple. There are two roles – producer and consumer. The producer builds the framework and then uploads it onto the cache. Thanks to this, all other developers (the consumers) can easily download the built framework and use it in the project. To upload a framework you must use the rome upload {FRAMEWORK} command, where{FRAMEWORK} is the name of the framework. When you want to upload all your frameworks you use rome upload.

Rome uses Cartfile.resolved to look up frameworks and their versions, so to be able to use the cache you have to have this file in the project's root. You can obtain it with carthage update --no-build or carthage bootstrap --no-build based on your needs. After that, the consumer can download all dependencies from the cache using rome download. If you want to download only one dependency then you use rome download {FRAMEWORK}.

If there are one or more dependencies that are not stored in the cache you should upload them to the cache. That's when you become the producer. To list all the missing frameworks use rome list --missing. This command lists all the missing dependencies.

With parameter --platform iOS you can specify which platform you are interested in. The parameter can be used with all three commands shown above. For example, when it's used in rome list --missing --platform iOS, the command will list only frameworks that are for the iOS and that are missing. Other possible platforms are watchOS, Mac and tvOS.

Conclusion

Thanks to the custom cache we were able to reduce the processing time of the project in the CI from one hour to 10 minutes. That's only because on every build the CI doesn't need to build all the dependencies.

The current setup isn't perfect. To have the cache working properly when Apple will release the new version of the Swift, the current dependencies will be overridden by the version that will be built for the newer version of Swift. To solve this you can use the parameter --cache-prefix that specifies the namespace to which the frameworks are uploaded. So with the newer version of Swift the cache will support the old one as well as the new one.

Do you want to see more from our stack? Check out our other blog posts and also our Github page of course!

Lukáš Hromadník
Lukáš Hromadník
iOS Developer

Are you interested in working together? Let’s discuss it in person!