Skip to content

Commit e289112

Browse files
committed
Merge branch 'use-packer-to-create-vm-image-for-release-build-system-ios-355'
2 parents 184068b + 592bc65 commit e289112

13 files changed

+450
-0
lines changed

ci/ios/create-vm/README.md

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Creating new macOS VMs to build MullvadVPN iOS in a CI environment
2+
This guide assumes you are running on macOS.
3+
## Prerequisites
4+
In order to create VMs on the fly, we decided to use [tart](https://tart.run/) and [packer](https://developer.hashicorp.com/packer).
5+
6+
The various scripts that run in the VM are written in bash with the help of [shellcheck](shellcheck.net).
7+
8+
## VM requirements
9+
- You will need at least 60GB of available space on your VM host
10+
- You will need at least 8GB of available RAM on your VM host
11+
- You will need at least 4 CPU cores available on your VM host
12+
13+
## How to install Tart
14+
- brew install `cirruslabs/cli/tart`
15+
16+
## How to install Packer
17+
- brew tap `hashicorp/tap`
18+
- brew install `hashicorp/tap/packer`
19+
20+
## How to install shellcheck
21+
- brew install `shellcheck`
22+
23+
> [!IMPORTANT]
24+
> # Prerequisite setup before running packer
25+
> - Get a copy of the Xcode version you want to install on the VM in a xip format
26+
> - Copy that file into the folder named `vm_shared_folder`
27+
> - Open the file named `variables.pkrvars.hcl`
28+
> - Edit the variables named `xcode_version` and `xcode_xip_name`
29+
30+
Here is an example of what to expect
31+
```bash
32+
% ls vm_shared_folder
33+
Xcode_15.0.1.xip
34+
% head -2 variables.pkrvars.hcl
35+
xcode_version = "15.1"
36+
xcode_xip_name = "Xcode_15.1.xip"
37+
```
38+
39+
### Sanity checks before running packer
40+
It is a good idea to keep logs, the `logs` folder is provided to that effect.
41+
Enable packer logs by setting the following environment variables (assuming your are running with `zsh`)
42+
- export `PACKER_LOG=1`
43+
- export `PACKER_LOG_PATH="logs/packer_logs.txt"`
44+
45+
> [!NOTE]
46+
> The logs will be overwritten with each packer command you issue.
47+
48+
You can then check that the templates are valid before running `packer`
49+
- packer inspect `-var-file="variables.pkrvars.hcl" install-build-dependencies.pkr.hcl`
50+
- packer validate `-var-file="variables.pkrvars.hcl" install-build-dependencies.pkr.hcl`
51+
52+
You can make sure you are not missing any dependencies with the `init` command
53+
- packer init `install-vanilla-ventura.pkr.hcl`
54+
55+
### Create the VM image via Packer
56+
Once your setup is ready, you just need one command to create a VM. And one more to install Xcode on it.
57+
- packer build `-var-file="variables.pkrvars.hcl" install-vanilla-ventura.pkr.hcl`
58+
59+
### Install Xcode on the VM image via Packer
60+
- packer build `-var-file="variables.pkrvars.hcl" install-build-dependencies.pkr.hcl`
61+
62+
> [!IMPORTANT]
63+
> 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.
64+
65+
Make sure to properly clean up the VM before running packer commands again if something went wrong.
66+
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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
packer {
2+
required_plugins {
3+
tart = {
4+
version = ">= 1.2.0"
5+
source = "github.com/cirruslabs/tart"
6+
}
7+
}
8+
}
9+
10+
variable "shared_folder_path" { type = string }
11+
12+
variable "xcode_version" {
13+
type = string
14+
15+
validation {
16+
condition = can(regex("(\\d)+(\\.)?((\\d)+)?(\\.)?((\\d)+)?", var.xcode_version))
17+
error_message = "Invalid Xcode version number. Example of a valid number: '15.0.1'."
18+
}
19+
}
20+
21+
variable "vm_name" { type = string }
22+
23+
variable "user_name" { type = string }
24+
25+
variable "xcode_xip_name" {
26+
type = string
27+
28+
validation {
29+
condition = can(regex("Xcode_(\\d)+(\\.)?((\\d)+)?(\\.)?((\\d)+)?\\.xip", var.xcode_xip_name))
30+
error_message = "Invalid Xcode file name. Example of a valid file name: 'Xcode_15.0.1.xip'."
31+
}
32+
}
33+
34+
source "tart-cli" "tart" {
35+
vm_name = "${var.vm_name}"
36+
ssh_password = "admin"
37+
ssh_username = "admin"
38+
ssh_timeout = "120s"
39+
disk_size_gb = 80
40+
}
41+
42+
build {
43+
sources = ["source.tart-cli.tart"]
44+
45+
46+
// Create a symlink for bash compatibility
47+
provisioner "shell" {
48+
script = "scripts/link-zprofile.sh"
49+
}
50+
51+
// Install brew
52+
provisioner "shell" {
53+
environment_vars = [
54+
"USER=${var.user_name}"
55+
]
56+
script = "scripts/install-brew.sh"
57+
}
58+
59+
60+
// Install required brew dependencies
61+
provisioner "shell" {
62+
script = "scripts/install-brew-dependencies.sh"
63+
}
64+
65+
// Install rustup
66+
provisioner "shell" {
67+
script = "scripts/install-rustup.sh"
68+
}
69+
70+
// Install go
71+
provisioner "shell" {
72+
script = "scripts/install-go.sh"
73+
}
74+
75+
// Copy the local Xcode xip file to the VM
76+
provisioner "file" {
77+
source = "${var.shared_folder_path}/${var.xcode_xip_name}"
78+
destination = "/tmp/${var.xcode_xip_name}"
79+
}
80+
81+
// Install Xcode via xcodes.app
82+
provisioner "shell" {
83+
84+
environment_vars = [
85+
"XCODE_VERSION=${var.xcode_version}",
86+
"XCODE_XIP_NAME=${var.xcode_xip_name}",
87+
"XCODE_SHARED_PATH=/tmp",
88+
]
89+
script = "scripts/install-xcode.sh"
90+
}
91+
92+
// Delete the Xcode xip file to save some space
93+
provisioner "shell" {
94+
inline = [
95+
"rm -f /tmp/${var.xcode_xip_name}"
96+
]
97+
}
98+
99+
// Run the xcodebuild first launch prompt to automatically accept terms and conditions, and download the iOS runtime simulator
100+
provisioner "shell" {
101+
script = "scripts/run-xcode-first-launch.sh"
102+
}
103+
104+
// Add Apple root certs
105+
provisioner "shell" {
106+
script = "scripts/add-apple-certs.sh"
107+
}
108+
109+
// Remove everything in case of error
110+
error-cleanup-provisioner "shell" {
111+
script = "scripts/cleanup.sh"
112+
}
113+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
packer {
2+
required_plugins {
3+
tart = {
4+
version = ">= 1.2.0"
5+
source = "github.com/cirruslabs/tart"
6+
}
7+
}
8+
}
9+
10+
variable "vm_name" { type = string }
11+
12+
source "tart-cli" "tart" {
13+
# You can find macOS IPSW URLs on various websites like https://ipsw.me/
14+
# and https://www.theiphonewiki.com/wiki/Beta_Firmware/Mac/13.x
15+
from_ipsw = "https://updates.cdn-apple.com/2023SummerFCS/fullrestores/042-43686/945D434B-DA5D-48DB-A558-F6D18D11AD69/UniversalMac_13.5.2_22G91_Restore.ipsw"
16+
vm_name = "${var.vm_name}"
17+
cpu_count = 4
18+
memory_gb = 8
19+
disk_size_gb = 60
20+
ssh_password = "admin"
21+
ssh_username = "admin"
22+
ssh_timeout = "120s"
23+
boot_command = [
24+
# hello, hola, bonjour, etc.
25+
"<wait60s><spacebar>",
26+
# Language: most of the times we have a list of "English"[1], "English (UK)", etc. with
27+
# "English" language already selected. If we type "english", it'll cause us to switch
28+
# to the "English (UK)", which is not what we want. To solve this, we switch to some other
29+
# language first, e.g. "Italiano" and then switch back to "English". We'll then jump to the
30+
# first entry in a list of "english"-prefixed items, which will be "English".
31+
#
32+
# [1]: should be named "English (US)", but oh well 🤷
33+
"<wait30s>italiano<esc>english<enter>",
34+
# Select Your Country and Region
35+
"<wait30s>united states<leftShiftOn><tab><leftShiftOff><spacebar>",
36+
# Written and Spoken Languages
37+
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
38+
# Accessibility
39+
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
40+
# Data & Privacy
41+
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
42+
# Migration Assistant
43+
"<wait10s><tab><tab><tab><spacebar>",
44+
# Sign In with Your Apple ID
45+
"<wait10s><leftShiftOn><tab><leftShiftOff><leftShiftOn><tab><leftShiftOff><spacebar>",
46+
# Are you sure you want to skip signing in with an Apple ID?
47+
"<wait10s><tab><spacebar>",
48+
# Terms and Conditions
49+
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
50+
# I have read and agree to the macOS Software License Agreement
51+
"<wait10s><tab><spacebar>",
52+
# Create a Computer Account
53+
"<wait10s>admin<tab><tab>admin<tab>admin<tab><tab><tab><spacebar>",
54+
# Enable Location Services
55+
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
56+
# Are you sure you don't want to use Location Services?
57+
"<wait10s><tab><spacebar>",
58+
# Select Your Time Zone
59+
"<wait10s><tab>UTC<enter><leftShiftOn><tab><leftShiftOff><spacebar>",
60+
# Analytics
61+
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
62+
# Screen Time
63+
"<wait10s><tab><spacebar>",
64+
# Siri
65+
"<wait10s><tab><spacebar><leftShiftOn><tab><leftShiftOff><spacebar>",
66+
# Choose Your Look
67+
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
68+
# Enable Voice Over
69+
"<wait10s><leftAltOn><f5><leftAltOff><wait5s><enter>",
70+
# Now that the installation is done, open "System Settings"
71+
"<wait10s><leftAltOn><spacebar><leftAltOff>System Settings<enter>",
72+
# Navigate to "Sharing"
73+
"<wait10s><leftAltOn>f<leftAltOff>screen sharing<enter>",
74+
# Navigate to "Screen Sharing" and enable it
75+
"<wait10s><tab><down><spacebar>",
76+
# Navigate to "Remote Login" and enable it
77+
"<wait10s><tab><tab><tab><tab><tab><tab><spacebar>",
78+
# Open "Remote Login" details
79+
"<wait10s><tab><spacebar>",
80+
# Enable "Full Disk Access"
81+
"<wait10s><tab><spacebar>",
82+
# Click "Done"
83+
"<wait10s><leftShiftOn><tab><leftShiftOff><leftShiftOn><tab><leftShiftOff><spacebar>",
84+
# Disable Voice Over
85+
"<leftAltOn><f5><leftAltOff>",
86+
]
87+
88+
// A (hopefully) temporary workaround for Virtualization.Framework's
89+
// installation process not fully finishing in a timely manner
90+
create_grace_time = "30s"
91+
}
92+
93+
build {
94+
sources = ["source.tart-cli.tart"]
95+
96+
provisioner "shell" {
97+
inline = [
98+
// Enable passwordless sudo
99+
"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\"",
100+
// Enable auto-login
101+
//
102+
// See https://github.com/xfreebird/kcpassword for details.
103+
"echo '00000000: 1ced 3f4a bcbc ba2c caca 4e82' | sudo xxd -r - /etc/kcpassword",
104+
"sudo defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser admin",
105+
// Disable screensaver at login screen
106+
"sudo defaults write /Library/Preferences/com.apple.screensaver loginWindowIdleTime 0",
107+
// Disable screensaver for admin user
108+
"defaults -currentHost write com.apple.screensaver idleTime 0",
109+
// Prevent the VM from sleeping
110+
"sudo systemsetup -setdisplaysleep Off",
111+
"sudo systemsetup -setsleep Off",
112+
"sudo systemsetup -setcomputersleep Off",
113+
// Launch Safari to populate the defaults
114+
"/Applications/Safari.app/Contents/MacOS/Safari &",
115+
"sleep 30",
116+
"kill -9 %1",
117+
// Enable Safari's remote automation and "Develop" menu
118+
"sudo safaridriver --enable",
119+
"defaults write com.apple.Safari.SandboxBroker ShowDevelopMenu -bool true",
120+
"defaults write com.apple.Safari IncludeDevelopMenu -bool true",
121+
// Disable screen lock
122+
//
123+
// Note that this only works if the user is logged-in,
124+
// i.e. not on login screen.
125+
"sysadminctl -screenLock off -password admin",
126+
"defaults -currentHost write com.apple.screensaver idleTime 0"
127+
]
128+
}
129+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#!/bin/bash
2+
# inspired by https://github.com/actions/runner-images/blob/fb3b6fd69957772c1596848e2daaec69eabca1bb/images/macos/provision/configuration/configure-machine.sh#L33-L61
3+
4+
sudo security delete-certificate -Z FF6797793A3CD798DC5B2ABEF56F73EDC9F83A64 /Library/Keychains/System.keychain
5+
6+
curl -o AppleWWDRCAG3.cer https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer
7+
curl -o DeveloperIDG2CA.cer https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
8+
sudo security add-certificates AppleWWDRCAG3.cer
9+
sudo security add-certificates DeveloperIDG2CA.cer

ci/ios/create-vm/scripts/cleanup.sh

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
# shellcheck source=/dev/null
6+
source ~/.bash_profile
7+
8+
9+
# Uninstall rust
10+
# shellcheck source=/dev/null
11+
if [[ -f "${HOME}/.cargo/env" ]]
12+
then
13+
source "${HOME}/.cargo/env"
14+
yes | rustup self uninstall
15+
fi
16+
17+
# Uninstall brew (This should also delete all dependencies)
18+
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/uninstall.sh)"
19+
# Clean up folders that were not automatically removed
20+
sudo rm -rf /opt/homebrew
21+
22+
# Remove the custom profiles
23+
rm -f ~/.zprofile ~/.profile ~/.bash_profile
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
# shellcheck source=/dev/null
6+
source ~/.bash_profile
7+
8+
if command -v brew &>/dev/null
9+
then
10+
echo "Installing xcodes"
11+
brew install xcodesorg/made/xcodes
12+
echo "Installing xcodes"
13+
brew install bash
14+
fi
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
if command -v brew &>/dev/null
6+
then
7+
echo >&1 "brew is already installed, nothing to do here"
8+
exit 0
9+
fi
10+
11+
echo >&1 "installing brew"
12+
NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
13+
# This is intentionally in single quotes for echo to append properly
14+
# shellcheck disable=SC2016
15+
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.bash_profile
16+
eval "$(/opt/homebrew/bin/brew shellenv)"
17+
18+
# shellcheck source=/dev/null
19+
source ~/.bash_profile
20+
brew update
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
3+
set -euo pipefail
4+
5+
# shellcheck source=/dev/null
6+
source ~/.bash_profile
7+
8+
if command -v brew &>/dev/null
9+
then
10+
echo >&1 "Installing go@1.20"
11+
brew install go@1.20
12+
echo "export PATH='/opt/homebrew/opt/go@1.20/bin:$PATH'" >> ~/.bash_profile
13+
fi

0 commit comments

Comments
 (0)