Skip to content

Commit

Permalink
Merge branch 'use-packer-to-create-vm-image-for-release-build-system-…
Browse files Browse the repository at this point in the history
…ios-355'
  • Loading branch information
buggmagnet committed Aug 9, 2024
2 parents 184068b + 592bc65 commit e289112
Show file tree
Hide file tree
Showing 13 changed files with 450 additions and 0 deletions.
66 changes: 66 additions & 0 deletions ci/ios/create-vm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Creating new macOS VMs to build MullvadVPN iOS in a CI environment
This guide assumes you are running on macOS.
## Prerequisites
In order to create VMs on the fly, we decided to use [tart](https://tart.run/) and [packer](https://developer.hashicorp.com/packer).

The various scripts that run in the VM are written in bash with the help of [shellcheck](shellcheck.net).

## VM requirements
- You will need at least 60GB of available space on your VM host
- You will need at least 8GB of available RAM on your VM host
- You will need at least 4 CPU cores available on your VM host

## How to install Tart
- brew install `cirruslabs/cli/tart`

## How to install Packer
- brew tap `hashicorp/tap`
- brew install `hashicorp/tap/packer`

## How to install shellcheck
- brew install `shellcheck`

> [!IMPORTANT]
> # Prerequisite setup before running packer
> - Get a copy of the Xcode version you want to install on the VM in a xip format
> - Copy that file into the folder named `vm_shared_folder`
> - Open the file named `variables.pkrvars.hcl`
> - Edit the variables named `xcode_version` and `xcode_xip_name`
Here is an example of what to expect
```bash
% ls vm_shared_folder
Xcode_15.0.1.xip
% head -2 variables.pkrvars.hcl
xcode_version = "15.1"
xcode_xip_name = "Xcode_15.1.xip"
```

### Sanity checks before running packer
It is a good idea to keep logs, the `logs` folder is provided to that effect.
Enable packer logs by setting the following environment variables (assuming your are running with `zsh`)
- export `PACKER_LOG=1`
- export `PACKER_LOG_PATH="logs/packer_logs.txt"`

> [!NOTE]
> The logs will be overwritten with each packer command you issue.
You can then check that the templates are valid before running `packer`
- packer inspect `-var-file="variables.pkrvars.hcl" install-build-dependencies.pkr.hcl`
- packer validate `-var-file="variables.pkrvars.hcl" install-build-dependencies.pkr.hcl`

You can make sure you are not missing any dependencies with the `init` command
- packer init `install-vanilla-ventura.pkr.hcl`

### Create the VM image via Packer
Once your setup is ready, you just need one command to create a VM. And one more to install Xcode on it.
- packer build `-var-file="variables.pkrvars.hcl" install-vanilla-ventura.pkr.hcl`

### Install Xcode on the VM image via Packer
- packer build `-var-file="variables.pkrvars.hcl" install-build-dependencies.pkr.hcl`

> [!IMPORTANT]
> At the time of writing this, `tart` does not support VM snapshotting. This means that any action taken by packer will be **permanent** on the VM.
Make sure to properly clean up the VM before running packer commands again if something went wrong.
You can look at the `cleanup.sh` script in the `scripts` folder to see what type of cleanup is ran in case things go wrong.
113 changes: 113 additions & 0 deletions ci/ios/create-vm/install-build-dependencies.pkr.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
packer {
required_plugins {
tart = {
version = ">= 1.2.0"
source = "github.com/cirruslabs/tart"
}
}
}

variable "shared_folder_path" { type = string }

variable "xcode_version" {
type = string

validation {
condition = can(regex("(\\d)+(\\.)?((\\d)+)?(\\.)?((\\d)+)?", var.xcode_version))
error_message = "Invalid Xcode version number. Example of a valid number: '15.0.1'."
}
}

variable "vm_name" { type = string }

variable "user_name" { type = string }

variable "xcode_xip_name" {
type = string

validation {
condition = can(regex("Xcode_(\\d)+(\\.)?((\\d)+)?(\\.)?((\\d)+)?\\.xip", var.xcode_xip_name))
error_message = "Invalid Xcode file name. Example of a valid file name: 'Xcode_15.0.1.xip'."
}
}

source "tart-cli" "tart" {
vm_name = "${var.vm_name}"
ssh_password = "admin"
ssh_username = "admin"
ssh_timeout = "120s"
disk_size_gb = 80
}

build {
sources = ["source.tart-cli.tart"]


// Create a symlink for bash compatibility
provisioner "shell" {
script = "scripts/link-zprofile.sh"
}

// Install brew
provisioner "shell" {
environment_vars = [
"USER=${var.user_name}"
]
script = "scripts/install-brew.sh"
}


// Install required brew dependencies
provisioner "shell" {
script = "scripts/install-brew-dependencies.sh"
}

// Install rustup
provisioner "shell" {
script = "scripts/install-rustup.sh"
}

// Install go
provisioner "shell" {
script = "scripts/install-go.sh"
}

// Copy the local Xcode xip file to the VM
provisioner "file" {
source = "${var.shared_folder_path}/${var.xcode_xip_name}"
destination = "/tmp/${var.xcode_xip_name}"
}

// Install Xcode via xcodes.app
provisioner "shell" {

environment_vars = [
"XCODE_VERSION=${var.xcode_version}",
"XCODE_XIP_NAME=${var.xcode_xip_name}",
"XCODE_SHARED_PATH=/tmp",
]
script = "scripts/install-xcode.sh"
}

// Delete the Xcode xip file to save some space
provisioner "shell" {
inline = [
"rm -f /tmp/${var.xcode_xip_name}"
]
}

// Run the xcodebuild first launch prompt to automatically accept terms and conditions, and download the iOS runtime simulator
provisioner "shell" {
script = "scripts/run-xcode-first-launch.sh"
}

// Add Apple root certs
provisioner "shell" {
script = "scripts/add-apple-certs.sh"
}

// Remove everything in case of error
error-cleanup-provisioner "shell" {
script = "scripts/cleanup.sh"
}
}
129 changes: 129 additions & 0 deletions ci/ios/create-vm/install-vanilla-ventura.pkr.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
packer {
required_plugins {
tart = {
version = ">= 1.2.0"
source = "github.com/cirruslabs/tart"
}
}
}

variable "vm_name" { type = string }

source "tart-cli" "tart" {
# You can find macOS IPSW URLs on various websites like https://ipsw.me/
# and https://www.theiphonewiki.com/wiki/Beta_Firmware/Mac/13.x
from_ipsw = "https://updates.cdn-apple.com/2023SummerFCS/fullrestores/042-43686/945D434B-DA5D-48DB-A558-F6D18D11AD69/UniversalMac_13.5.2_22G91_Restore.ipsw"
vm_name = "${var.vm_name}"
cpu_count = 4
memory_gb = 8
disk_size_gb = 60
ssh_password = "admin"
ssh_username = "admin"
ssh_timeout = "120s"
boot_command = [
# hello, hola, bonjour, etc.
"<wait60s><spacebar>",
# Language: most of the times we have a list of "English"[1], "English (UK)", etc. with
# "English" language already selected. If we type "english", it'll cause us to switch
# to the "English (UK)", which is not what we want. To solve this, we switch to some other
# language first, e.g. "Italiano" and then switch back to "English". We'll then jump to the
# first entry in a list of "english"-prefixed items, which will be "English".
#
# [1]: should be named "English (US)", but oh well 🤷
"<wait30s>italiano<esc>english<enter>",
# Select Your Country and Region
"<wait30s>united states<leftShiftOn><tab><leftShiftOff><spacebar>",
# Written and Spoken Languages
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Accessibility
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Data & Privacy
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Migration Assistant
"<wait10s><tab><tab><tab><spacebar>",
# Sign In with Your Apple ID
"<wait10s><leftShiftOn><tab><leftShiftOff><leftShiftOn><tab><leftShiftOff><spacebar>",
# Are you sure you want to skip signing in with an Apple ID?
"<wait10s><tab><spacebar>",
# Terms and Conditions
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# I have read and agree to the macOS Software License Agreement
"<wait10s><tab><spacebar>",
# Create a Computer Account
"<wait10s>admin<tab><tab>admin<tab>admin<tab><tab><tab><spacebar>",
# Enable Location Services
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Are you sure you don't want to use Location Services?
"<wait10s><tab><spacebar>",
# Select Your Time Zone
"<wait10s><tab>UTC<enter><leftShiftOn><tab><leftShiftOff><spacebar>",
# Analytics
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Screen Time
"<wait10s><tab><spacebar>",
# Siri
"<wait10s><tab><spacebar><leftShiftOn><tab><leftShiftOff><spacebar>",
# Choose Your Look
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Enable Voice Over
"<wait10s><leftAltOn><f5><leftAltOff><wait5s><enter>",
# Now that the installation is done, open "System Settings"
"<wait10s><leftAltOn><spacebar><leftAltOff>System Settings<enter>",
# Navigate to "Sharing"
"<wait10s><leftAltOn>f<leftAltOff>screen sharing<enter>",
# Navigate to "Screen Sharing" and enable it
"<wait10s><tab><down><spacebar>",
# Navigate to "Remote Login" and enable it
"<wait10s><tab><tab><tab><tab><tab><tab><spacebar>",
# Open "Remote Login" details
"<wait10s><tab><spacebar>",
# Enable "Full Disk Access"
"<wait10s><tab><spacebar>",
# Click "Done"
"<wait10s><leftShiftOn><tab><leftShiftOff><leftShiftOn><tab><leftShiftOff><spacebar>",
# Disable Voice Over
"<leftAltOn><f5><leftAltOff>",
]

// A (hopefully) temporary workaround for Virtualization.Framework's
// installation process not fully finishing in a timely manner
create_grace_time = "30s"
}

build {
sources = ["source.tart-cli.tart"]

provisioner "shell" {
inline = [
// Enable passwordless sudo
"echo admin | sudo -S sh -c \"mkdir -p /etc/sudoers.d/; echo 'admin ALL=(ALL) NOPASSWD: ALL' | EDITOR=tee visudo /etc/sudoers.d/admin-nopasswd\"",
// Enable auto-login
//
// See https://github.com/xfreebird/kcpassword for details.
"echo '00000000: 1ced 3f4a bcbc ba2c caca 4e82' | sudo xxd -r - /etc/kcpassword",
"sudo defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser admin",
// Disable screensaver at login screen
"sudo defaults write /Library/Preferences/com.apple.screensaver loginWindowIdleTime 0",
// Disable screensaver for admin user
"defaults -currentHost write com.apple.screensaver idleTime 0",
// Prevent the VM from sleeping
"sudo systemsetup -setdisplaysleep Off",
"sudo systemsetup -setsleep Off",
"sudo systemsetup -setcomputersleep Off",
// Launch Safari to populate the defaults
"/Applications/Safari.app/Contents/MacOS/Safari &",
"sleep 30",
"kill -9 %1",
// Enable Safari's remote automation and "Develop" menu
"sudo safaridriver --enable",
"defaults write com.apple.Safari.SandboxBroker ShowDevelopMenu -bool true",
"defaults write com.apple.Safari IncludeDevelopMenu -bool true",
// Disable screen lock
//
// Note that this only works if the user is logged-in,
// i.e. not on login screen.
"sysadminctl -screenLock off -password admin",
"defaults -currentHost write com.apple.screensaver idleTime 0"
]
}
}
9 changes: 9 additions & 0 deletions ci/ios/create-vm/scripts/add-apple-certs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
# inspired by https://github.com/actions/runner-images/blob/fb3b6fd69957772c1596848e2daaec69eabca1bb/images/macos/provision/configuration/configure-machine.sh#L33-L61

sudo security delete-certificate -Z FF6797793A3CD798DC5B2ABEF56F73EDC9F83A64 /Library/Keychains/System.keychain

curl -o AppleWWDRCAG3.cer https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer
curl -o DeveloperIDG2CA.cer https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
sudo security add-certificates AppleWWDRCAG3.cer
sudo security add-certificates DeveloperIDG2CA.cer
23 changes: 23 additions & 0 deletions ci/ios/create-vm/scripts/cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/bin/bash

set -euo pipefail

# shellcheck source=/dev/null
source ~/.bash_profile


# Uninstall rust
# shellcheck source=/dev/null
if [[ -f "${HOME}/.cargo/env" ]]
then
source "${HOME}/.cargo/env"
yes | rustup self uninstall
fi

# Uninstall brew (This should also delete all dependencies)
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"
# Clean up folders that were not automatically removed
sudo rm -rf /opt/homebrew

# Remove the custom profiles
rm -f ~/.zprofile ~/.profile ~/.bash_profile
14 changes: 14 additions & 0 deletions ci/ios/create-vm/scripts/install-brew-dependencies.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/bash

set -euo pipefail

# shellcheck source=/dev/null
source ~/.bash_profile

if command -v brew &>/dev/null
then
echo "Installing xcodes"
brew install xcodesorg/made/xcodes
echo "Installing xcodes"
brew install bash
fi
20 changes: 20 additions & 0 deletions ci/ios/create-vm/scripts/install-brew.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/bin/bash

set -euo pipefail

if command -v brew &>/dev/null
then
echo >&1 "brew is already installed, nothing to do here"
exit 0
fi

echo >&1 "installing brew"
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# This is intentionally in single quotes for echo to append properly
# shellcheck disable=SC2016
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.bash_profile
eval "$(/opt/homebrew/bin/brew shellenv)"

# shellcheck source=/dev/null
source ~/.bash_profile
brew update
13 changes: 13 additions & 0 deletions ci/ios/create-vm/scripts/install-go.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/bash

set -euo pipefail

# shellcheck source=/dev/null
source ~/.bash_profile

if command -v brew &>/dev/null
then
echo >&1 "Installing [email protected]"
brew install [email protected]
echo "export PATH='/opt/homebrew/opt/[email protected]/bin:$PATH'" >> ~/.bash_profile
fi
Loading

0 comments on commit e289112

Please sign in to comment.