macOS CI on a budget

  • 11 min read time
  • |
  • 05 July 2021
Image not Found

As developers, we all love to observe the glow of green indicators and mesmerising dance of technology that builds and tests our apps. Unfortunately, in the iOS world, reality doesn't look so good. CI services are unaffordable for our pet projects, and hiring a dedicated build machine is even more expensive. The problem, of course, is not that these services are greedy, but the platform to which we are tied. An iOS app can only be built on a macOS, that's it.

In this article, I want to offer a workaround to this problem and show how you can use your own Apple computer that you already use for work as a CI machine. We are going to use GitHub Actions as the CI framework and VirtualBox to run our CI machine in the background. The only requirement is to have an x86 architecture Apple machine and at least 100GB of disk space on your computer. As far as I know, VirtualBox doesn't support the M1 architecture. Still, you can use other virtualization solutions, such as Parallels or VMware Fusion, to achieve the same result.

Disclaimer

Running virtualised macOS is a historically touchy subject. As far as I know, everything we do here is legal under Apple's EULA. We're not jailbreaking or running a hackintosh. We're running virtualized macOS on genuine macOS, which is mentioned in the following clause of the EULA.

Quote: (iii) to install, use and run up to two (2) additional copies or instances of the Apple Software within virtual operating system environments on each Mac Computer you own or control that is already running the Apple Software, for purposes of: (a) software development; (b) testing during software development; (c) using macOS Server; or (d) personal, non-commercial use.

Of course, use your best judgement, as I am not a lawyer and cannot give you advice in this area.

What are GitHub Actions

As GitHub states itself, "GitHub Actions is an API for cause and effect on GitHub: orchestrate any workflow, based on any event". I'm going to use this particular platform because it gives you a massive amount of features for free if you provide your own build machine. In our case, it's really exciting because by providing a part of our machine, we can get a full-featured macOS CI.

GitHub Actions extends far beyond just building your project. You can do literally anything with it, including good old Bash scripting. The platform uses YAML files to set up the build process defined by steps. The steps can be anything from your own scripts to the massive library of community written and official steps. Likely, everything you might need is already written for you, making your CI setup extremely versatile.

GitHub also offers its own build servers and even free monthly minutes for you to try it out. Unfortunately, in the iOS world, the problems remain. On GitHub, build time is extremely expensive for us and is calculated in multiples of 10 compared to Linux minutes.

So how do we use our machine as a build server, you might ask. GitHub has made it dead simple. Launch the GitHub Action Runner on your machine ( I have another article on how to run it on a Synology NAS) and link it to your account, done. Once your machine is linked, you specify in the YAML file that you want to use a self-hosted build machine then everything works like a charm.

As I mentioned earlier, we are going to run GitHub Actions Runner on a virtual machine. There are several reasons why you might want to do this. First and foremost, there are security concerns. Since you going to be running third-party code provided by the community, you don't want this code running on your main machine. Secondly, it's convenient. We don't want this process running uncontrollably on our machine, spawning processes and installing all sorts of software. By moving it to a virtual machine we isolate that environment, so we don't need to worry much about what happens there. And of course, the virtual machine will have a manageable amount of allocated memory, running neatly in the background.

Sounds good, right? Now let's try to put this into practice.

Putting it all together

The installation process may be quite cumbersome, so be sure to follow each step without skipping anything "unnecessary". During the installation process, we will create a bootable ISO file which we will use to install macOS on the virtual machine. Next, we'll prepare a build environment sufficient to build an iOS project. When everything is ready, we'll be able to link it to our GitHub account. As a final step, we'll set up the trickiest part and make everything launch automatically in the background when our machine boots up.

Creating a bootable ISO file

The macOS distribution has everything you need to create a bootable ISO image, thus we won't use any unnecessary third-party tools here. All you need is the official macOS distribution and the terminal.

  1. Make sure you have about 30GB of free disk space for this procedure.
  2. Download macOS from the Mac App Store. Yes, if you are new to this, you can download macOS from the Mac App Store even if you are already using this macOS version. I am using Big Sur 11.4 on my host machine and am going to install the same version of macOS on the virtual machine. It would be better if you follow me by using the same version of macOS.
  3. Create a temporary disk image which we will use to create a macOS installation ISO hdiutil create -o /tmp/BigSur -size 16G -layout SPUD -fs HFS+J -type SPARSE
  4. Mount it hdiutil attach /tmp/BigSur.sparseimage -noverify -mountpoint /Volumes/mac_install
  5. Using the createinstallmedia utility from the macOS distribution, make the mounted image a macOS installation image sudo /Applications/Install\ macOS\ Big\ Sur.app/Contents/Resources/createinstallmedia --volume /Volumes/mac_install
  6. Unmount it hdiutil detach /Volumes/Install\ macOS\ Big\ Sur. You may need the -force flag if it is constantly busy.
  7. Convert the Sparse image into an ISO one hdiutil convert /tmp/BigSur.sparseimage -format UDTO -o /tmp/BigSur.iso
  8. Move your ISO file to the desktop, changing the extension from cdr to iso mv /tmp/BigSur.iso.cdr ~/Desktop/BigSur.iso
  9. Delete the temporary image rm /tmp/BigSur.sparseimage

That's it. Now that you have a bootable ISO file to install macOS, we can move on to the next step.

Creating a virtual machine

My version of VirtualBox at the time of writing is 6.1.22, which may be important if you want to follow these steps in the same environment.

  1. Install VirtualBox + VirtualBox Extension Pack. You can get it from the official website and install it by following the instructions. During installation, you will need to allow Oracle extension in System Preferences -> Security & Privacy. No additional settings are required.
  2. Create a new virtual machine by pressing CMD+N and switch this screen to expert mode if you cannot see the memory settings.
  3. On the first screen of the wizard, I used the following settings. Name: BigSurCI, Type: Mac OS X, Version: Mac OS X (64-bit), Memory size: 4096 MB, Hard Disk: Create a virtual hard disk now.
  4. And the following, on the second screen. File Size: 160 GB (This is a lot, you can reduce the size, but I would suggest at least 100GB, Xcode and CI will take up a lot of space), Type: VDI, Storage on physical hard disk: Dynamically allocated (If you don't care about disk space, a fixed size will probably work a little better).
  5. Press the Create button. Now let's make a few small adjustments to this machine before installing macOS.
  6. Press CMD+S to open the virtual machine settings and change the following parameters.
  7. System -> Processor -> Processor(s): 2.
  8. Display -> Screen -> Video Memory: 128.
  9. Audio -> Enable Audio: off.
  10. Network -> Adapter 1 -> Advanced -> Port Forwarding. Add the following rule: Name: SSH Rule, Protocol: TCP, Host IP: 127.0.0.1, Host Port: 2222, Guest IP: leave it blank, Guest Port: 22. With these settings, you should be able to connect to your virtual machine via SSH by connecting to 127.0.0.1:2222. Later you will need to enable the Remote Login feature for it to work.
  11. Storage -> Optical Drive: Select the BigSur ISO file you've created in the previous step by clicking on the disk icon.

Installing macOS

  1. Start the virtual machine and in the dialogue select your ISO to boot the machine. At this stage, you will be asked to permit input monitoring.
  2. After waiting a couple of minutes for the machine to load, and choose your preferred language.
  3. Open Disk Utility.
  4. Select VBOX HARDDISK media and press the Erase button with the following settings. Name: Macintosh HD, Format: Mac OS Extended (Journaled), Scheme: GUID.
  5. Close the Disk Utility and select Install macOS Big Sur.
  6. Go through the licence agreement and select the newly formatted drive. Click "Continue" and wait.
  7. Now that the system is installed, you must complete the ever-growing macOS onboarding process. Performance is likely to be absolutely unusable. Just go through this. It's fine as it will not affect CI performance. Once onboarding is complete, it may take a few minutes for the system to boot up and display the dock. Be patient.
  8. Right-click on the desktop -> Change Desktop Background -> Screen Saver: disable the screen saver.
  9. System Preferences -> Energy Sever: disable all pro-sleep settings and set the sleep timer to Never. If you are still experiencing problems with your computer going to sleep, you can always use this app to get around this problem.
  10. System System Preferences -> Users & Groups -> Login Options enable Automatic Login for your user.
  11. System Preferences -> Sharing: enable Remote Login.

Setting up CI and linking to GitHub

Now that we've gone through all this, we can finally start to reap what we've sown. Let's set up everything we need so that our CI can build an app, and then when we see that it works, we can do the final polishing.

  1. Open the Apple Developer Portal, download and install the current Xcode version on that system. Remember to run it at least once after installation.
  2. Finally, we can leave the UI of our virtual machine and switch to the terminal. Log in to your machine with ssh [email protected] -p 2222. Use your username and password of course. If you wish, you can continue to perform all actions in your virtual machine's terminal.
  3. Do xcode-select sudo xcode-select -s /Applications/Xcode.app/Contents/Developer.
  4. Install Homebrew /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)". This may take some time as the Command Line Tools for Xcode need to be installed along with it. It may get stuck during installation. Rebooting the system and performing this step again is likely to help. If you keep having trouble getting through the Command Line Tools installation step, you can do this beforehand with a separate xcode-select --install command.
  5. Install CocoaPods sudo gem install cocoapods
  6. Install Fastlane brew install fastlane

Since we now have a complete build environment, this should be enough to build almost any native iOS project. It's time to install the GitHub Actions Runner, which will build your projects on this machine.

We are going to add the GitHub Actions Runner to a single repository to simplify the process. Of course, you can add it to your organisation to share across all your projects. To add the GitHub Actions Runner to your repository, on GitHub open your repository and go to Settings -> Actions -> Runners, click Add Runner. You will be presented with instructions on how to download and activate your Runner. By following these instructions, you will have a fully configured and ready to run CI. You can try it out now.

Final Steps

Although your CI is now up and running, it is far from perfect. You still have VirtualBox and a virtual machine running in the dock. What's more, if you reboot your computer, all the magic will instantly disappear. Let's fix this and make this setup as seamless as possible. To achieve this, we will use launchd on both the host and virtual machines. On our host machine, we need to make the virtual machine automatically launch in the background. On the other hand, the virtual machine should automatically start the GitHub Actions Runner at startup. The syntax of the launchd files is quite complicated. If you want to do some customisation I can recommend an excellent resource that has a description of all these settings.

  1. On the virtual machine using sudo privilege, we create a launchd file which will launch the GitHub Actions Runner when the system starts. Additionally we will set KeepAlive to keep it running and restart it in case of failure. Create the file sudo vim /Library/LaunchAgents/org.my.actions.runner.plist and copy the following content into it. Don't forget to update the file path to the correct run.sh location on your virtual machine.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Disabled</key>  <false/>
    <key>RunAtLoad</key> <true/>
    <key>KeepAlive</key> <true/>
    <key>Label</key>     <string>org.my.actions.runner</string>
    <key>EnvironmentVariables</key>
      <dict>
        <key>PATH</key>
	<string>/bin:/usr/bin:/usr/local/bin</string>
        <key>RUNNER_ALLOW_RUNASROOT</key>
        <string>1</string>
        <key>LC_ALL</key>
        <string>en_US.UTF-8</string>
        <key>LANG</key>
        <string>en_US.UTF-8</string>
      </dict>
    <key>ProgramArguments</key>
        <array>
            <string>/Users/test/actions-runner/run.sh</string>
        </array>
</dict>
</plist>
  1. Restart your virtual machine sudo shutdown -r now and make sure the GitHub Actions Runner is up. After restarting, you can verify that it's up by logging back into ssh and doing something like this ps -ax | grep runner. Alternatively, you can see it running on GitHub itself, on the page where you added it. It should show up in the list of Runners with a green indicator next to it. In case it doesn't work properly, I suggest setting up logging paths in that plist we just created and looking for more troubleshooting advice at aforementioned website.

At this point, your virtual machine is fully automated. Our next and final task is to get this machine to run in the background when your computer starts up. We will use the same approach with launchd as with the GitHub Actions Runner.

  1. On the host machine use sudo privilege to create a launchd sudo vim /Library/LaunchAgents/org.github.actions.vm.launch.plist file with the following content. Again, remember to check the path to VirtualBox and the name of your virtual machine.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Disabled</key>  <false/>
    <key>RunAtLoad</key> <true/>
    <key>KeepAlive</key> <true/>
    <key>Label</key>     <string>org.github.actions.vm.launch</string>

    <key>ProgramArguments</key>
        <array>
            <string>/Applications/VirtualBox.app/Contents/MacOS/VBoxManage</string>
            <string>startvm</string>
            <string>BigSurCI</string>
            <string>--type</string>
            <string>headless</string>
        </array>
</dict>
</plist>
  1. Reboot your computer and check if your virtual machine is running in the background by opening the VirtualBox application. Additionally, check that your Runner is still running on GitHub.

I would like to point out that I have found no way to quickly stop this machine in the background other than toggling the Disabled flag in the plist and restarting the computer. If you simply shut it down in VirtualBox, it will restart immediately. If you know of a better way to solve this problem, I would highly appreciate your suggestions.

Conclusion

If you're on a tight budget, this might be a good option for you to get your hands on a real macOS CI. I tried this setup for a month, and from my personal experience, the only problem I encountered was editing videos in DaVinci Resolve, which persisted until I shut down the virtual machine. You also have to sacrifice a small amount of RAM, but you probably won't notice it. Another negative aspect worth mentioning is that the build will fail if you put the machine in sleep mode during the build. These are the only problems I've personally noticed, and frankly, I was expecting many more troubles with this. Anyway, the choice is yours. I am personally leaning towards a bare metal setup and hope that type 1 hypervisors will soon be available on Apple M1 processors. Stay healthy and good luck with your experiments!

Mentioned software versions

Computer: iMac 2019 3,2 GHz 6-Core Intel Core i7 16 GB
MacOS: BigSur 11.4
Virtual Box: 6.1.22
GirHub Actions Runner: actions-runner-osx-x64-2.278.0.tar.gz
CocoaPods: 1.10.1
fastlane: 2.185.1
Xcode: 12.5

You May Also Like