mirror of
https://github.com/outbackdingo/firezone.git
synced 2026-01-27 10:18:54 +00:00
ci: Add standalone macOS build support (#7581)
The CI swift workflow needs to be updated to accommodate the macOS standalone build. This required a decent amount of refactoring to make the Apple build process more maintainable. Unfortunately this PR ended up being a giant ball of yarn where pulling on one thread tended to unravel things elsewhere, since building the Apple artifacts involve multiple interconnected systems. Combined with the slow iteration of running in CI, I wasn't able to split this PR into easier to digest commits, so I've annotated the PR as much as I can to explain what's changed. The good news is that Apple release artifacts can now be easily built from a developer's machine with simply `scripts/build/macos-standalone.sh`. The only thing needed is the proper provisioning profiles and signing certs installed. Since this PR is so big already, I'll save the swift/apple/README.md updates for another PR.
This commit is contained in:
BIN
scripts/build/dmg_background.png
Normal file
BIN
scripts/build/dmg_background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
82
scripts/build/ios-appstore.sh
Executable file
82
scripts/build/ios-appstore.sh
Executable file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Builds the Firezone iOS client for submitting to the App Store
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "./scripts/build/lib.sh"
|
||||
|
||||
# Define needed variables
|
||||
app_profile_id=8da59aa3-e8da-4a8c-9902-2d540324d92c
|
||||
ne_profile_id=0fccb78a-97c0-41b9-8c54-9c995280ea8e
|
||||
temp_dir=$(mktemp -d)
|
||||
archive_path="$temp_dir/Firezone.xcarchive"
|
||||
export_options_plist_path="$temp_dir/ExportOptions.plist"
|
||||
git_sha=${GITHUB_SHA:-$(git rev-parse HEAD)}
|
||||
project_file=swift/apple/Firezone.xcodeproj
|
||||
code_sign_identity="Apple Distribution: Firezone, Inc. (47R2M6779T)"
|
||||
|
||||
if [ "${CI:-}" = "true" ]; then
|
||||
# Configure the environment for building, signing, and packaging in CI
|
||||
setup_runner \
|
||||
"$IOS_APP_PROVISIONING_PROFILE" \
|
||||
"$app_profile_id.mobileprovision" \
|
||||
"$IOS_NE_PROVISIONING_PROFILE" \
|
||||
"$ne_profile_id.mobileprovision"
|
||||
fi
|
||||
|
||||
# Build and sign app
|
||||
set_project_build_version "$project_file/project.pbxproj"
|
||||
|
||||
echo "Building and signing app..."
|
||||
xcodebuild archive \
|
||||
GIT_SHA="$git_sha" \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
CODE_SIGN_IDENTITY="$code_sign_identity" \
|
||||
APP_PROFILE_ID="$app_profile_id" \
|
||||
NE_PROFILE_ID="$ne_profile_id" \
|
||||
-project "$project_file" \
|
||||
-skipMacroValidation \
|
||||
-archivePath "$archive_path" \
|
||||
-configuration Release \
|
||||
-scheme Firezone \
|
||||
-sdk iphoneos \
|
||||
-destination 'generic/platform=iOS'
|
||||
|
||||
# iOS requires a separate export step; write out the export options plist
|
||||
# here so we can inject the provisioning profile IDs
|
||||
cat <<EOF >"$export_options_plist_path"
|
||||
<?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>method</key>
|
||||
<string>app-store</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>dev.firezone.firezone</key>
|
||||
<string>$app_profile_id</string>
|
||||
|
||||
<key>dev.firezone.firezone.network-extension</key>
|
||||
<string>$ne_profile_id</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# Export the archive
|
||||
# -exportPath MUST be a directory; the Firezone.ipa will be written here
|
||||
xcodebuild \
|
||||
-exportArchive \
|
||||
-archivePath "$archive_path" \
|
||||
-exportPath "$temp_dir" \
|
||||
-exportOptionsPlist "$export_options_plist_path"
|
||||
|
||||
package_path="$temp_dir/Firezone.ipa"
|
||||
|
||||
echo "Package created at $package_path"
|
||||
|
||||
# Move to final location the uploader expects
|
||||
if [[ -n "${ARTIFACT_PATH:-}" ]]; then
|
||||
mv "$package_path" "$ARTIFACT_PATH"
|
||||
fi
|
||||
95
scripts/build/lib.sh
Executable file
95
scripts/build/lib.sh
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
# See https://docs.github.com/en/actions/use-cases-and-examples/deploying/installing-an-apple-certificate-on-macos-runners-for-xcode-development
|
||||
function setup_runner() {
|
||||
local app_profile="$1"
|
||||
local app_profile_file="$2"
|
||||
local ne_profile="$3"
|
||||
local ne_profile_file="$4"
|
||||
profiles_path="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
||||
keychain_pass=$(openssl rand -base64 32)
|
||||
keychain_path="$(mktemp -d)/app-signing.keychain-db"
|
||||
|
||||
# Select Xcode specified by the workflow
|
||||
sudo xcode-select -s "/Applications/Xcode_$XCODE_VERSION.app"
|
||||
|
||||
# Install provisioning profiles
|
||||
mkdir -p "$profiles_path"
|
||||
base64_decode "$app_profile" "$profiles_path/$app_profile_file"
|
||||
base64_decode "$ne_profile" "$profiles_path/$ne_profile_file"
|
||||
|
||||
# Create a keychain to use for signing
|
||||
security create-keychain -p "$keychain_pass" "$keychain_path"
|
||||
|
||||
# Set it as the default keychain so Xcode can find the signing certs
|
||||
security default-keychain -s "$keychain_path"
|
||||
|
||||
# Ensure it stays unlocked during the build
|
||||
security set-keychain-settings -lut 21600 "$keychain_path"
|
||||
|
||||
# Unlock the keychain for use
|
||||
security unlock-keychain -p "$keychain_pass" "$keychain_path"
|
||||
|
||||
# Install signing certs
|
||||
install_cert \
|
||||
"$BUILD_CERT" \
|
||||
"$BUILD_CERT_PASS" \
|
||||
"$keychain_pass" \
|
||||
"$keychain_path"
|
||||
install_cert \
|
||||
"$INSTALLER_CERT" \
|
||||
"$INSTALLER_CERT_PASS" \
|
||||
"$keychain_pass" \
|
||||
"$keychain_path"
|
||||
install_cert \
|
||||
"$STANDALONE_BUILD_CERT" \
|
||||
"$STANDALONE_BUILD_CERT_PASS" \
|
||||
"$keychain_pass" \
|
||||
"$keychain_path"
|
||||
}
|
||||
|
||||
function base64_decode() {
|
||||
local input_stdin="$1"
|
||||
local output_path="$2"
|
||||
|
||||
echo -n "$input_stdin" | base64 --decode -o "$output_path"
|
||||
}
|
||||
|
||||
function install_cert() {
|
||||
local cert_path
|
||||
local cert="$1"
|
||||
local pass="$2"
|
||||
local keychain_pass="$3"
|
||||
local keychain_path="$4"
|
||||
|
||||
cert_path="$(mktemp -d)/cert.p12"
|
||||
|
||||
base64_decode "$cert" "$cert_path"
|
||||
|
||||
# Import cert into keychain
|
||||
security import "$cert_path" \
|
||||
-P "$pass" \
|
||||
-A \
|
||||
-t cert \
|
||||
-f pkcs12 \
|
||||
-k "$keychain_path"
|
||||
|
||||
# Prevent the keychain from asking for password to access the cert
|
||||
security set-key-partition-list \
|
||||
-S apple-tool:,apple: \
|
||||
-k "$keychain_pass" \
|
||||
"$keychain_path"
|
||||
|
||||
# Clean up
|
||||
rm "$cert_path"
|
||||
}
|
||||
|
||||
function set_project_build_version() {
|
||||
local project_file="$1"
|
||||
|
||||
seconds_since_epoch=$(date +%s)
|
||||
sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9]/CURRENT_PROJECT_VERSION = $seconds_since_epoch/" \
|
||||
"$project_file"
|
||||
}
|
||||
59
scripts/build/macos-appstore.sh
Executable file
59
scripts/build/macos-appstore.sh
Executable file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Builds the Firezone macOS client for submitting to the App Store
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "./scripts/build/lib.sh"
|
||||
|
||||
# Define needed variables
|
||||
app_profile_id=2bf20e38-81ea-40d0-91e5-330cf58f52d9
|
||||
ne_profile_id=2c683d1a-4479-451c-9ee6-ae7d4aca5c93
|
||||
temp_dir=$(mktemp -d)
|
||||
package_path="$temp_dir/Firezone.pkg"
|
||||
git_sha=${GITHUB_SHA:-$(git rev-parse HEAD)}
|
||||
project_file=swift/apple/Firezone.xcodeproj
|
||||
code_sign_identity="Apple Distribution: Firezone, Inc. (47R2M6779T)"
|
||||
installer_code_sign_identity="3rd Party Mac Developer Installer: Firezone, Inc. (47R2M6779T)"
|
||||
|
||||
if [ "${CI:-}" = "true" ]; then
|
||||
# Configure the environment for building, signing, and packaging in CI
|
||||
setup_runner \
|
||||
"$MACOS_APP_PROVISIONING_PROFILE" \
|
||||
"$app_profile_id.provisionprofile" \
|
||||
"$MACOS_NE_PROVISIONING_PROFILE" \
|
||||
"$ne_profile_id.provisionprofile"
|
||||
fi
|
||||
|
||||
# Build and sign
|
||||
set_project_build_version "$project_file/project.pbxproj"
|
||||
|
||||
echo "Building and signing app..."
|
||||
xcodebuild build \
|
||||
GIT_SHA="$git_sha" \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
CODE_SIGN_IDENTITY="$code_sign_identity" \
|
||||
CONFIGURATION_BUILD_DIR="$temp_dir" \
|
||||
APP_PROFILE_ID="$app_profile_id" \
|
||||
NE_PROFILE_ID="$ne_profile_id" \
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
-project "$project_file" \
|
||||
-skipMacroValidation \
|
||||
-configuration Release \
|
||||
-scheme Firezone \
|
||||
-sdk macosx \
|
||||
-destination 'platform=macOS'
|
||||
|
||||
# Mac App Store requires a signed installer package
|
||||
productbuild \
|
||||
--sign "$installer_code_sign_identity" \
|
||||
--component "$temp_dir/Firezone.app" \
|
||||
/Applications \
|
||||
"$package_path"
|
||||
|
||||
echo "Installer package created at $package_path"
|
||||
|
||||
# Move to final location the uploader expects
|
||||
if [[ -n "${ARTIFACT_PATH:-}" ]]; then
|
||||
mv "$package_path" "$ARTIFACT_PATH"
|
||||
fi
|
||||
127
scripts/build/macos-standalone.sh
Executable file
127
scripts/build/macos-standalone.sh
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Builds the Firezone macOS client for standalone distribution
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "./scripts/build/lib.sh"
|
||||
|
||||
# Define needed variables
|
||||
app_profile_id=c5d97f71-de80-4dfc-80f8-d0a4393ff082
|
||||
ne_profile_id=153db941-2136-4d6c-96ef-52f748521e78
|
||||
notarize=${NOTARIZE:-"false"}
|
||||
temp_dir=$(mktemp -d)
|
||||
dmg_dir="$temp_dir/dmg"
|
||||
dmg_path="$temp_dir/Firezone.dmg"
|
||||
package_path="$temp_dir/package.dmg"
|
||||
git_sha=${GITHUB_SHA:-$(git rev-parse HEAD)}
|
||||
project_file=swift/apple/Firezone.xcodeproj
|
||||
codesign_identity="Developer ID Application: Firezone, Inc. (47R2M6779T)"
|
||||
|
||||
if [ "${CI:-}" = "true" ]; then
|
||||
# Configure the environment for building, signing, and packaging in CI
|
||||
setup_runner \
|
||||
"$STANDALONE_MACOS_APP_PROVISIONING_PROFILE" \
|
||||
"$app_profile_id.provisionprofile" \
|
||||
"$STANDALONE_MACOS_NE_PROVISIONING_PROFILE" \
|
||||
"$ne_profile_id.provisionprofile"
|
||||
fi
|
||||
|
||||
# Build and sign
|
||||
set_project_build_version "$project_file/project.pbxproj"
|
||||
|
||||
echo "Building and signing app..."
|
||||
xcodebuild build \
|
||||
GIT_SHA="$git_sha" \
|
||||
CODE_SIGN_STYLE=Manual \
|
||||
CODE_SIGN_IDENTITY="$codesign_identity" \
|
||||
PACKET_TUNNEL_PROVIDER_SUFFIX=-systemextension \
|
||||
OTHER_CODE_SIGN_FLAGS="--timestamp" \
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO \
|
||||
CONFIGURATION_BUILD_DIR="$temp_dir" \
|
||||
APP_PROFILE_ID="$app_profile_id" \
|
||||
NE_PROFILE_ID="$ne_profile_id" \
|
||||
ONLY_ACTIVE_ARCH=NO \
|
||||
-project "$project_file" \
|
||||
-skipMacroValidation \
|
||||
-configuration Release \
|
||||
-scheme Firezone \
|
||||
-sdk macosx \
|
||||
-destination 'platform=macOS'
|
||||
|
||||
# Notarize app before embedding within disk image
|
||||
if [ "$notarize" = "true" ]; then
|
||||
# Notary service expects a single file, not app bundle
|
||||
ditto -c -k "$temp_dir/Firezone.app" "$temp_dir/Firezone.zip"
|
||||
|
||||
private_key_path="$temp_dir/firezone-api-key.p8"
|
||||
base64_decode "$API_KEY" "$private_key_path"
|
||||
|
||||
# Submit app bundle to be notarized. Can take a few minutes.
|
||||
# Notarizes embedded app bundle as well.
|
||||
xcrun notarytool submit "$temp_dir/Firezone.zip" \
|
||||
--key "$private_key_path" \
|
||||
--key-id "$API_KEY_ID" \
|
||||
--issuer "$ISSUER_ID" \
|
||||
--wait
|
||||
|
||||
# Clean up private key
|
||||
rm "$private_key_path"
|
||||
|
||||
# Staple notarization ticket to app bundle
|
||||
xcrun stapler staple "$temp_dir/Firezone.app"
|
||||
fi
|
||||
|
||||
# Create disk image
|
||||
mkdir -p "$dmg_dir/.background"
|
||||
mv "$temp_dir/Firezone.app" "$dmg_dir/Firezone.app"
|
||||
cp "scripts/build/dmg_background.png" "$dmg_dir/.background/background.png"
|
||||
ln -s /Applications "$dmg_dir/Applications"
|
||||
hdiutil create \
|
||||
-volname "Firezone Installer" \
|
||||
-srcfolder "$dmg_dir" \
|
||||
-ov \
|
||||
-format UDRW \
|
||||
"$package_path"
|
||||
|
||||
# Mount disk image for customization
|
||||
mount_dir=$(hdiutil attach "$package_path" -readwrite -noverify -noautoopen | grep -o "/Volumes/.*")
|
||||
|
||||
# Embed background image to instruct user to drag app to /Applications
|
||||
osascript <<EOF
|
||||
tell application "Finder"
|
||||
tell disk "Firezone Installer"
|
||||
open
|
||||
set current view of container window to icon view
|
||||
set toolbar visible of container window to false
|
||||
set statusbar visible of container window to false
|
||||
set bounds of container window to {100, 100, 800, 400}
|
||||
set viewOptions to the icon view options of container window
|
||||
set arrangement of viewOptions to not arranged
|
||||
set icon size of viewOptions to 128
|
||||
set background picture of viewOptions to file ".background:background.png"
|
||||
set position of item "Firezone.app" of container window to {200, 128}
|
||||
set position of item "Applications" of container window to {500, 128}
|
||||
close
|
||||
open
|
||||
update without registering applications
|
||||
delay 2
|
||||
end tell
|
||||
end tell
|
||||
EOF
|
||||
|
||||
# Unmount disk image
|
||||
hdiutil detach "$mount_dir"
|
||||
|
||||
# Convert to read-only
|
||||
hdiutil convert "$package_path" -format UDZO -o "$dmg_path"
|
||||
|
||||
# Sign disk image
|
||||
codesign --force --sign "$codesign_identity" "$dmg_path"
|
||||
|
||||
echo "Disk image created at $dmg_path"
|
||||
|
||||
# Move to final location the uploader expects
|
||||
if [[ -n "${ARTIFACT_PATH:-}" ]]; then
|
||||
mv "$package_path" "$ARTIFACT_PATH"
|
||||
fi
|
||||
31
scripts/upload/app-store-connect.sh
Executable file
31
scripts/upload/app-store-connect.sh
Executable file
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Pushes iOS and macOS builds to App Store Connect
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "./scripts/build/lib.sh"
|
||||
|
||||
# xcrun altool requires private keys to be files in a specific naming format
|
||||
temp_dir=$(mktemp -d)
|
||||
private_key_dir="$temp_dir/private_keys"
|
||||
private_key_file="AuthKey_$API_KEY_ID.p8"
|
||||
|
||||
mkdir -p "$private_key_dir"
|
||||
base64_decode "$API_KEY" "$private_key_dir/$private_key_file"
|
||||
|
||||
cur_dir=$(pwd)
|
||||
cd "$temp_dir"
|
||||
|
||||
# Submit app to App Store Connect
|
||||
xcrun altool \
|
||||
--upload-app \
|
||||
-f "$ARTIFACT_PATH" \
|
||||
-t "$PLATFORM" \
|
||||
--apiKey "$API_KEY_ID" \
|
||||
--apiIssuer "$ISSUER_ID"
|
||||
|
||||
# Clean up private key
|
||||
rm -rf "$private_key_dir"
|
||||
|
||||
cd "$cur_dir"
|
||||
21
scripts/upload/github-release.sh
Executable file
21
scripts/upload/github-release.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Uploads built packages to a GitHub release
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Only clobber existing release assets if the release is a draft
|
||||
is_draft=$(gh release view "$RELEASE_NAME" --json isDraft --jq '.isDraft' | tr -d '\n')
|
||||
if [[ "$is_draft" == "true" ]]; then
|
||||
clobber="--clobber"
|
||||
else
|
||||
clobber=""
|
||||
fi
|
||||
|
||||
sha256sum "$ARTIFACT_PATH" >"$ARTIFACT_PATH.sha256sum.txt"
|
||||
|
||||
gh release upload "$RELEASE_NAME" \
|
||||
"$ARTIFACT_PATH" \
|
||||
"$ARTIFACT_PATH.sha256sum.txt" \
|
||||
$clobber \
|
||||
--repo "$GITHUB_REPOSITORY"
|
||||
Reference in New Issue
Block a user