KISS

Keep It Simple Stupid

Jenkins in OSX guest in VirtualBox for iOS jobs – full setup guide

| comments

Hello there!

Updated on 2015-04-18: added a simple command to allow web update.

In this post, I will describe how to setup a sandboxed Jenkins server in a virtual machine where you can play and try your iOS (and not only) jobs. I’m using the latest OS X Yosemite 10.10.2 for the host and the guest on a MacBook Pro. Nota Bene: according to the Apple’s EULA for OS X, you are allowed to run the OS on the Apple hardware only.

This how-to assumes you know what a command-line is, how to run commands, and related stuff.

Installing VirtualBox

Since you’re working/developing on OS X, you must already know about the Homebrew package manager to install some essential software that Apple doesn’t bundle with OS X. So I am left to mention the Homebrew Cask that extends brew to install (and update) GUI applications.

As a note, Homebrew requires the OS X Command-Line Tools or, even better, Xcode installed. You can either go to the Mac App Store or download the Xcode installer from https://developer.apple.com/downloads (Apple ID required), as you’ll need it later as well.

1
2
3
4
5
6
7
8
# install brew and follow the instructions
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

# install brew cask
brew install caskroom/cask/brew-cask

# install VirtualBox
brew cask info virtualbox

I’m not sure if it’s required for the purpose of this guide, but I always install the VirtualBox Extension Pack as well. For this, you’ll have to download it from here: https://www.virtualbox.org/wiki/Downloads.

Installing Guest OS X

This section is mostly based on these posts: http://apple.stackexchange.com/questions/106476/how-to-install-osx-mavericks-in-virtualbox/106840#106840 and https://ntk.me/2012/09/07/os-x-on-os-x/.

Download the Install OS X Yosemite.app from the Mac App Store, it will appear in the /Applications/ directory.

Install iESD — a ruby package to work with OS X install images. I don’t like to mess with the system packages (Ruby, Python, etc.) and since I need ruby for octopress and cocoapods too, I use rbenv to build and install a custom ruby version in my home directory. Therefore, I can easily run gem install iesd without sudo. The guide how to install and use rbenv is here: Installing CocoaPods with rbenv.

1
2
# prepare an image
iesd -i '/Applications/Install OS X Yosemite.app' -o ~/Yosemite.dmg -t BaseSystem

We’ve got the bootable image to install the OS X! Launch VirtualBox. Create a new virtual machine. Let the name be osx (the VirtualBox will automagically deduct its Version as Mac OS X (64 bit), which is what we need). The default amount of RAM is 2048 MB, let’s set it to 4096 MB for the installation phase (should be faster). Create a virtual hard drive (I’d like a virtual SSD though).

Fix some of the default settings:

1
2
3
4
5
6
7
8
9
# set the PIIX3 chipset
VBoxManage modifyvm osx --chipset piix3
# fake the CPU
VBoxManage modifyvm osx --cpuidset 00000001 000306a9 00020800 80000201 178bfbff
# attach the image to the machine
VBoxManage storageattach osx --storagectl SATA --port 1 --medium ~/Yosemite.dmg

# run!!!
VBoxManage startvm osx

Install the OS X as usual. You’ll need to use Utilities > Disk Utitlity to repartition the hard drive. Select the only disk on the left > Partition tab > Partition Layout: 1 Partition > Name like osx > Apply. Let’s name the user osx as well. Go grab some tea, the installation will take a while.

Finally, hurray!

You can suspend the machine and take a snapshot of a freshly installed OS (do you feel the fresh smell as from a very new printed book?):

1
2
3
4
5
6
7
8
9
# pause the machine
VBoxManage controlvm osx savestate

# disable booting from the image
VBoxManage modifyvm osx --boot1 disk
VBoxManage storageattach osx --storagectl SATA --port 1 --medium none

# snapshotting
VBoxManage snapshot osx take "fresh install"

Guest Tweaking

It’s convenient to have remote access to your guest in order not to muck around with the UI. SSH is the standard here (you can also use RDP):

1
2
3
4
5
# setup port forwarding for SSH
VBoxManage modifyvm osx --natpf1 "guestssh,tcp,,2222,,22"

# run the VM
VBoxManage startvm osx

Install updates in the guest. Unfortunately, there are no guest additions for OS X that would provide graphics acceleration, so the graphics inside is pretty slow. To make it less painful, go to System Settings > Accessibility > Display > check Reduce Transparency. You won’t have to use this slow UI much though.

Enable SSH to the guest: go to System Settings > Sharing > enable Remote Login. Now you can SSH into the VM this way from your host:

1
ssh -p 2222 osx@127.1

Either get the Xcode installer in the guest, or copy it from the host (Note: the port in scp is specified after -P, whereas in ssh it’s -p):

1
scp -P 2222 Downloads/xcode_6.1.1.dmg osx@127.1:~/Desktop

Install Xcode as usual. Launch Xcode and let it install its components. SSH into the VM and install Homebrew:

1
2
ssh -p 2222 osx@127.1
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Jenkins Setup for iOS

Download and install the latest JDK 8.* from oracle.com. You can either download it in the guest, or again copy from the host with scp.

We’re going to install Jenkins with the official installer: http://mirrors.jenkins-ci.org/osx/latest. Download and install the pkg file. In the current installer (1.598), Jenkins is setup to start as the jenkins user, which is nice, because it will be created for us. A browser will open by the end with a freshly installed Jenkins.

A very important part of the post follows. Jenkins is now run as a daemon, and daemons are not allowed to communicate with the UI, which is the reasons we can’t run iOS tests from a Jenkins job (Types of Background Processes). There is a lot of complaints about that online. So we’ll run it as a launch agent (https://issues.jenkins-ci.org/browse/JENKINS-21237).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# stop the daemon
sudo launchctl unload /Library/LaunchDaemons/org.jenkins-ci.plist
# check it's unloaded; the output should be empty
pgrep -fil jenkins
# set the password for the jenkins user
sudo passwd jenkins

# mess with the jenkins launchd config
sudo mkdir -p ~jenkins/Library/LaunchAgents/
sudo mv /Library/LaunchDaemons/org.jenkins-ci.plist ~jenkins/Library/LaunchAgents/

sudo defaults write ~jenkins/Library/LaunchAgents/org.jenkins-ci.plist GroupName jenkins
sudo plutil -convert xml1 /Users/Shared/Jenkins/Library/LaunchAgents/org.jenkins-ci.plist

sudo chown -R jenkins:jenkins ~jenkins/Library/
sudo chmod 644 ~jenkins/Library/LaunchAgents/org.jenkins-ci.plist

Now you should login as jenkins in the guest UI, otherwise the launchctl doesn’t want to load the agent:

1
2
osxs-mbp:~ jenkins$ launchctl load Library/LaunchAgents/org.jenkins-ci.plist
Could not find domain for

Since you’ve logged in, Jenkins is started automatically. Now we need to setup autologin, so that we don’t bother logging in manually after every restart (it’s not a big deal for a VM, but the same approach should be used on real servers). In the guest, open System Preferences > Users & Groups > unlock the settings by pressing the lock at bottom-left corner and entering an admin’s credentials (osx user in our case). Click Login Options, and in the Automatic login: dropdown pick… an empty line (that’s because the OS X apparently uses user’s Full Name for that list, and the Jenkins installer didn’t bother to set that, we don’t care either). In the opened popup window, enter jenkins’s password. So far so good.

While still here, go to Sharing > Remote Login > add jenkins to the allowed users, so you can SSH in as her directly.

However, it’s a big security hole to setup an automatic password-less login for a system (especially, with real servers). So put this file into ~jenkins/Library/LaunchAgents/ (thanks to http://www.tuaw.com/2011/03/07/terminally-geeky-use-automatic-login-more-securely/):

(org.jenkins.loginhook.plist) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//APPLE//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>AbandonProcessGroup</key>
        <true/>
        <key>Label</key>
        <string>org.jenkins.loginhook</string>
        <key>ProgramArguments</key>
        <array>
            <string>/System/Library/CoreServices/Menu Extras/User.menu/Contents/Resources/CGSession</string>
            <string>-suspend</string>
        </array>
        <key>RunAtLoad</key>
        <true/>
    </dict>
</plist>

This agent runs a command that locks the current account and presents the system Login Window. Since it’s setup to autoload, the OS X will automatically login as jenkins, and then go back to the Login Window, as if nothing happened. On a real server, that happens pretty fast, but in VirtualBox it can take a few seconds. Go try it now – restart the guest! After the restart, you should see the login window with a checkmark at the jenkins user, which means it’s logged in.

Don’t worry, we’re almost done. Now it’s a good idea to take a snapshot just in case:

1
2
VBoxManage controlvm osx savestate
VBoxManage snapshot osx take "jenkins from the jenkins user"

If the default Jenkins’ 8080 port is free on your host, it’s a good idea to forward the guest’s 8080 port to the host, so you can use exactly the same URL on both host and guest:

1
VBoxManage modifyvm osx --natpf1 "guestjenkins,tcp,,8080,,8080"

Now that all the automatic stuff is all setup, we can finally run the VM without the UI:

1
VBoxManage startvm osx --type headless

Wait several seconds. Behold: http://127.1:8080/. You should see the Jenkins UI served by the hidden VM. Congratulations, the installation is done!

Now you can SSH as jenkins or osx to do some stuff in the guest. NB: with the default setup we’ve done, you won’t be able to use brew from the jenkins user. You’ll need to login as osx to do some brewing, which is fine for me (separation of responsibilities).

Allowing web update

Jenkins provides an update button in the web UI if it has found a newer version and the process has permissions to overwrite the jar file. The latter condition is failing by default with this setup, however it’s very easy to fix. Run this command on the Jenkins host:

1
sudo chgrp -R jenkins /Applications/Jenkins/ && sudo chmod -R g+w /Applications/Jenkins/

iOS Tests Setup

You can run XCTest-based tests in the iPhone Simulator on OS X in a virtual machine! Just a little more preparation for that, I promise.

1
2
ssh -p 2222 osx@127.1
brew install xctool

In a browser with your jenkins, go to Jenkins > Configure System > Global properties > check Environment variables, add a PATH variable with the value $PATH:/usr/local/bin. Done.

Here is a sample build command to run tests:

1
2
3
xctool -scheme SuperScheme -sdk iphonesimulator -configuration Release \
    -reporter plain -reporter junit:"build/test_report.xml" \
    -IDECustomDerivedDataLocation="build" clean test
1
2
3
4
5
** TEST FAILED: 65 passed, 2 failed, 0 errored, 67 total ** (65499 ms)

Build step 'Execute shell' marked build as failure
Recording test results
Finished: FAILURE

Hurray! I’m so happy to see this message, which means the tests are run just fine.

The End

I mean, “To Be Continued…” (I remember this phrase from The X-Files series a dozen of years ago). There will be at least a few more posts about Jenkins in the blog. Stay tuned: Jenkins category.

All in all, I’m super happy about the result, because it allows me to test the jobs (and play with plugins) before rolling them out on the real Jenkins server.

Thanks for reading and following along. If some of the steps are unclear or you have any suggestions/improvements/comments, please leave a comment!

Comments