mirror of
https://github.com/cirruslabs/macos-image-templates.git
synced 2026-10-21 09:41:48 +02:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebc84e90df | ||
|
|
bfb3e95f08 | ||
|
|
2ff087f9f0 | ||
|
|
6570628f94 | ||
|
|
03a555c22a | ||
|
|
3c620b40ea | ||
|
|
d851dd5a76 | ||
|
|
be4a8e907b | ||
|
|
addef08637 | ||
|
|
106c086ffa | ||
|
|
795a125e78 | ||
|
|
4849509b2d | ||
|
|
210e02922f | ||
|
|
d6ee41d6d5 | ||
|
|
741758b9ba | ||
|
|
f03344599b | ||
|
|
16e9ad7d00 | ||
|
|
85b77e41bc | ||
|
|
9be4b9dc86 | ||
|
|
46050816d8 | ||
|
|
d358da6f50 | ||
|
|
b4926493fb | ||
|
|
c166ec1698 | ||
|
|
3d6445b446 | ||
|
|
f5a7e1e631 | ||
|
|
0a9ad1a419 | ||
|
|
cd2d1c6698 | ||
|
|
65519d3874 | ||
|
|
71b405bd0c |
@@ -0,0 +1,119 @@
|
|||||||
|
name: Base Images
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/base.yml"
|
||||||
|
- "data/github_known_hosts"
|
||||||
|
- "data/limit.maxfiles.plist"
|
||||||
|
- "data/tart-guest-*.plist"
|
||||||
|
- "scripts/automationmodetool.expect"
|
||||||
|
- "scripts/install-actions-runner.sh"
|
||||||
|
- "scripts/update-tcc-database.sh"
|
||||||
|
- "templates/base.pkr.hcl"
|
||||||
|
- "templates/disable-sip*.pkr.hcl"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
macos_version:
|
||||||
|
description: "macOS base image to build"
|
||||||
|
required: true
|
||||||
|
default: all
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- all
|
||||||
|
- golden-gate
|
||||||
|
- tahoe
|
||||||
|
- sequoia
|
||||||
|
- sonoma
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: tart-image-builds
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
TART_REGISTRY_HOSTNAME: ghcr.io
|
||||||
|
TART_REGISTRY_USERNAME: ${{ github.actor }}
|
||||||
|
TART_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-base:
|
||||||
|
name: Update Base Image (${{ matrix.macos_version }})
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
github.event_name == 'workflow_dispatch' ||
|
||||||
|
github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
|
||||||
|
}}
|
||||||
|
runs-on: [self-hosted, macOS, ARM64]
|
||||||
|
timeout-minutes: 180
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 1
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- macos_version: sonoma
|
||||||
|
disable_sip_template: disable-sip.pkr.hcl
|
||||||
|
- macos_version: sequoia
|
||||||
|
disable_sip_template: disable-sip-with-username.pkr.hcl
|
||||||
|
- macos_version: tahoe
|
||||||
|
disable_sip_template: disable-sip-with-username.pkr.hcl
|
||||||
|
- macos_version: golden-gate
|
||||||
|
disable_sip_template: disable-sip-with-username.pkr.hcl
|
||||||
|
env:
|
||||||
|
DISABLE_SIP_TEMPLATE: ${{ matrix.disable_sip_template }}
|
||||||
|
MACOS_VERSION: ${{ matrix.macos_version }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Select image
|
||||||
|
id: select
|
||||||
|
env:
|
||||||
|
INPUT_MACOS_VERSION: ${{ inputs.macos_version || 'all' }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
build=false
|
||||||
|
if [[ "$INPUT_MACOS_VERSION" == "all" || "$INPUT_MACOS_VERSION" == "$MACOS_VERSION" ]]; then
|
||||||
|
build=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "build=$build" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Tool versions
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart --version
|
||||||
|
packer --version
|
||||||
|
|
||||||
|
- name: Pull vanilla image
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart pull "ghcr.io/cirruslabs/macos-$MACOS_VERSION-vanilla:latest"
|
||||||
|
tart clone "ghcr.io/cirruslabs/macos-$MACOS_VERSION-vanilla:latest" "$MACOS_VERSION-base"
|
||||||
|
|
||||||
|
- name: Disable SIP
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
packer init "templates/$DISABLE_SIP_TEMPLATE"
|
||||||
|
packer build -var "vm_name=$MACOS_VERSION-base" "templates/$DISABLE_SIP_TEMPLATE"
|
||||||
|
|
||||||
|
- name: Build base image
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
packer init templates/base.pkr.hcl
|
||||||
|
packer build -var "vm_name=$MACOS_VERSION-base" templates/base.pkr.hcl
|
||||||
|
|
||||||
|
- name: Push base image
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart push "$MACOS_VERSION-base" "ghcr.io/cirruslabs/macos-$MACOS_VERSION-base:latest"
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always() && steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart delete "$MACOS_VERSION-base" || true
|
||||||
@@ -58,6 +58,8 @@ jobs:
|
|||||||
max-parallel: 1
|
max-parallel: 1
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
- macos_version: golden-gate
|
||||||
|
disable_sip_template: disable-sip-with-username.pkr.hcl
|
||||||
- macos_version: sonoma
|
- macos_version: sonoma
|
||||||
disable_sip_template: disable-sip.pkr.hcl
|
disable_sip_template: disable-sip.pkr.hcl
|
||||||
- macos_version: sequoia
|
- macos_version: sequoia
|
||||||
@@ -115,9 +117,17 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- macos_version: tahoe
|
- macos_version: tahoe
|
||||||
xcode_version: "26.2"
|
xcode_version: "27.1"
|
||||||
xcode_components: '"MetalToolchain"'
|
xcode_components: '"MetalToolchain"'
|
||||||
latest: true
|
latest: true
|
||||||
|
- macos_version: tahoe
|
||||||
|
xcode_version: "27"
|
||||||
|
xcode_components: '"MetalToolchain"'
|
||||||
|
latest: false
|
||||||
|
- macos_version: tahoe
|
||||||
|
xcode_version: "26.2"
|
||||||
|
xcode_components: '"MetalToolchain"'
|
||||||
|
latest: false
|
||||||
- macos_version: tahoe
|
- macos_version: tahoe
|
||||||
xcode_version: "26.1.1"
|
xcode_version: "26.1.1"
|
||||||
xcode_components: '"MetalToolchain"'
|
xcode_components: '"MetalToolchain"'
|
||||||
@@ -158,6 +168,14 @@ jobs:
|
|||||||
xcode_version: "15.4"
|
xcode_version: "15.4"
|
||||||
xcode_components: ""
|
xcode_components: ""
|
||||||
latest: false
|
latest: false
|
||||||
|
- macos_version: golden-gate
|
||||||
|
xcode_version: "27.1"
|
||||||
|
xcode_components: '"MetalToolchain"'
|
||||||
|
latest: true
|
||||||
|
- macos_version: golden-gate
|
||||||
|
xcode_version: "27"
|
||||||
|
xcode_components: '"MetalToolchain"'
|
||||||
|
latest: false
|
||||||
env:
|
env:
|
||||||
MACOS_VERSION: ${{ matrix.macos_version }}
|
MACOS_VERSION: ${{ matrix.macos_version }}
|
||||||
XCODE_COMPONENTS: ${{ matrix.xcode_components }}
|
XCODE_COMPONENTS: ${{ matrix.xcode_components }}
|
||||||
@@ -179,6 +197,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Xcode image
|
- name: Build Xcode image
|
||||||
run: |
|
run: |
|
||||||
|
tart clone "ghcr.io/cirruslabs/macos-$MACOS_VERSION-base:latest" "$MACOS_VERSION-xcode:$XCODE_VERSION"
|
||||||
|
tart set "$MACOS_VERSION-xcode:$XCODE_VERSION" --disk-size 140
|
||||||
packer init templates/xcode.pkr.hcl
|
packer init templates/xcode.pkr.hcl
|
||||||
packer build \
|
packer build \
|
||||||
-var "macos_version=$MACOS_VERSION" \
|
-var "macos_version=$MACOS_VERSION" \
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 1
|
max-parallel: 1
|
||||||
matrix:
|
matrix:
|
||||||
macos_version:
|
# Xcode 27 requires macOS Tahoe 26.6 or later.
|
||||||
- tahoe
|
macos_version: >-
|
||||||
- sequoia
|
${{ startsWith(github.event.release.tag_name || inputs.xcode_version, '27')
|
||||||
|
&& fromJSON('["tahoe", "golden-gate"]')
|
||||||
|
|| fromJSON('["tahoe", "sequoia"]') }}
|
||||||
env:
|
env:
|
||||||
MACOS_VERSION: ${{ matrix.macos_version }}
|
MACOS_VERSION: ${{ matrix.macos_version }}
|
||||||
XCODE_COMPONENTS: '"MetalToolchain"'
|
XCODE_COMPONENTS: '"MetalToolchain"'
|
||||||
@@ -64,6 +66,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build release image
|
- name: Build release image
|
||||||
run: |
|
run: |
|
||||||
|
tart clone "ghcr.io/cirruslabs/macos-$MACOS_VERSION-base:latest" "$MACOS_VERSION-xcode:$XCODE_RELEASE_VERSION"
|
||||||
|
tart set "$MACOS_VERSION-xcode:$XCODE_RELEASE_VERSION" --disk-size 140
|
||||||
packer init templates/xcode.pkr.hcl
|
packer init templates/xcode.pkr.hcl
|
||||||
packer build \
|
packer build \
|
||||||
-var "macos_version=$MACOS_VERSION" \
|
-var "macos_version=$MACOS_VERSION" \
|
||||||
@@ -89,7 +93,8 @@ jobs:
|
|||||||
|
|
||||||
release-runner:
|
release-runner:
|
||||||
name: Update Runner Image (${{ matrix.macos_version }})
|
name: Update Runner Image (${{ matrix.macos_version }})
|
||||||
needs: release-xcode
|
# Keep runner refreshes independent from the release-xcode matrix so one
|
||||||
|
# Xcode image failure does not skip the runner matrix.
|
||||||
runs-on: [self-hosted, macOS, ARM64]
|
runs-on: [self-hosted, macOS, ARM64]
|
||||||
timeout-minutes: 180
|
timeout-minutes: 180
|
||||||
strategy:
|
strategy:
|
||||||
@@ -97,6 +102,12 @@ jobs:
|
|||||||
max-parallel: 1
|
max-parallel: 1
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
- macos_version: golden-gate
|
||||||
|
xcode_versions: '"27.1","27"'
|
||||||
|
additional_ios_builds: ""
|
||||||
|
additional_tvos_builds: ""
|
||||||
|
xcode_components: '"MetalToolchain"'
|
||||||
|
disk_size: 240
|
||||||
- macos_version: sequoia
|
- macos_version: sequoia
|
||||||
xcode_versions: '"26.0.1",16.4,16.3,16.2,16.1,16'
|
xcode_versions: '"26.0.1",16.4,16.3,16.2,16.1,16'
|
||||||
additional_ios_builds: "18.5,18.4,18.2,17.5"
|
additional_ios_builds: "18.5,18.4,18.2,17.5"
|
||||||
@@ -104,11 +115,11 @@ jobs:
|
|||||||
xcode_components: '"MetalToolchain"'
|
xcode_components: '"MetalToolchain"'
|
||||||
disk_size: 380
|
disk_size: 380
|
||||||
- macos_version: tahoe
|
- macos_version: tahoe
|
||||||
xcode_versions: '"26.5","27-beta","26.4.1","26.3","26.2","26.1.1","26.0.1"'
|
xcode_versions: '"27.1","26.6","27-beta-6","27","26.5","27-beta","26.4.1","26.3","26.2","26.1.1","26.0.1"'
|
||||||
additional_ios_builds: "18.6"
|
additional_ios_builds: "18.6"
|
||||||
additional_tvos_builds: ""
|
additional_tvos_builds: ""
|
||||||
xcode_components: '"MetalToolchain"'
|
xcode_components: '"MetalToolchain"'
|
||||||
disk_size: 330
|
disk_size: 520
|
||||||
env:
|
env:
|
||||||
ADDITIONAL_IOS_BUILDS: ${{ matrix.additional_ios_builds }}
|
ADDITIONAL_IOS_BUILDS: ${{ matrix.additional_ios_builds }}
|
||||||
ADDITIONAL_TVOS_BUILDS: ${{ matrix.additional_tvos_builds }}
|
ADDITIONAL_TVOS_BUILDS: ${{ matrix.additional_tvos_builds }}
|
||||||
@@ -132,6 +143,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Build runner image
|
- name: Build runner image
|
||||||
run: |
|
run: |
|
||||||
|
tart clone "ghcr.io/cirruslabs/macos-$MACOS_VERSION-base:latest" "$MACOS_VERSION-xcode:runner"
|
||||||
|
tart set "$MACOS_VERSION-xcode:runner" --disk-size "$DISK_SIZE"
|
||||||
packer init templates/xcode.pkr.hcl
|
packer init templates/xcode.pkr.hcl
|
||||||
packer build \
|
packer build \
|
||||||
-var tag=runner \
|
-var tag=runner \
|
||||||
|
|||||||
@@ -0,0 +1,416 @@
|
|||||||
|
name: Template Builds
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- ".github/workflows/base.yml"
|
||||||
|
- ".github/workflows/monthly.yml"
|
||||||
|
- ".github/workflows/release.yml"
|
||||||
|
- ".github/workflows/template-validation.yml"
|
||||||
|
- "data/**"
|
||||||
|
- "scripts/**"
|
||||||
|
- "templates/**"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: template-builds-${{ github.event.pull_request.number || github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
FASTLANE_SESSION: ${{ secrets.FASTLANE_SESSION }}
|
||||||
|
FASTLANE_USER: ${{ secrets.FASTLANE_USER }}
|
||||||
|
HOMEBREW_NO_AUTO_UPDATE: 1
|
||||||
|
HOMEBREW_NO_INSTALL_CLEANUP: 1
|
||||||
|
PACKER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TART_REGISTRY_HOSTNAME: ghcr.io
|
||||||
|
TART_REGISTRY_USERNAME: ${{ github.actor }}
|
||||||
|
TART_REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
packer-validate:
|
||||||
|
name: Packer Validate
|
||||||
|
runs-on: macos-15
|
||||||
|
timeout-minutes: 30
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Packer
|
||||||
|
uses: hashicorp/setup-packer@v3
|
||||||
|
|
||||||
|
- name: Install validation dependencies
|
||||||
|
run: |
|
||||||
|
brew install ansible
|
||||||
|
|
||||||
|
- name: Validate Tart Metal capabilities
|
||||||
|
run: |
|
||||||
|
bash -n scripts/install-tart-metal-capabilities.sh scripts/test-tart-metal-capabilities.sh
|
||||||
|
bash scripts/test-tart-metal-capabilities.sh
|
||||||
|
|
||||||
|
- name: Validate TCC database provisioning
|
||||||
|
run: |
|
||||||
|
bash -n scripts/update-tcc-database.sh scripts/test-update-tcc-database.sh
|
||||||
|
bash scripts/test-update-tcc-database.sh
|
||||||
|
|
||||||
|
- name: Prepare validation inputs
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/XcodesCache"
|
||||||
|
touch "$HOME/XcodesCache/Xcode_26.6.xip"
|
||||||
|
touch "$HOME/XcodesCache/Xcode_27-beta-6.xip"
|
||||||
|
touch "$HOME/XcodesCache/Xcode_27.xip"
|
||||||
|
touch "$HOME/XcodesCache/Xcode_27.1.xip"
|
||||||
|
|
||||||
|
- name: Validate templates
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
validate() {
|
||||||
|
local template="$1"
|
||||||
|
shift
|
||||||
|
|
||||||
|
packer init "$template"
|
||||||
|
packer validate "$@" "$template"
|
||||||
|
}
|
||||||
|
|
||||||
|
for template in templates/vanilla-*.pkr.hcl; do
|
||||||
|
validate "$template"
|
||||||
|
done
|
||||||
|
|
||||||
|
validate templates/xcode.pkr.hcl \
|
||||||
|
-var macos_version=golden-gate \
|
||||||
|
-var 'xcode_version=["27.1","27"]' \
|
||||||
|
-var 'xcode_components=["MetalToolchain"]' \
|
||||||
|
-var expected_runtimes_file=data/expected.golden-gate.runtimes.txt
|
||||||
|
|
||||||
|
validate templates/base.pkr.hcl \
|
||||||
|
-var vm_name=template-validation-base
|
||||||
|
|
||||||
|
validate templates/disable-sip.pkr.hcl \
|
||||||
|
-var vm_name=template-validation-disable-sip
|
||||||
|
|
||||||
|
validate templates/disable-sip-with-username.pkr.hcl \
|
||||||
|
-var vm_name=template-validation-disable-sip-user
|
||||||
|
|
||||||
|
validate templates/exex-script.pkr.hcl \
|
||||||
|
-var vm_name=template-validation-exec \
|
||||||
|
-var script_path=scripts/finalize-tahoe.sh
|
||||||
|
|
||||||
|
validate templates/resolve-macos-number.pkr.hcl \
|
||||||
|
-var vm_base_name=template-validation-base \
|
||||||
|
-var vm_name=template-validation-resolve \
|
||||||
|
-var resolve_file=macos-version.txt
|
||||||
|
|
||||||
|
validate templates/xcode.pkr.hcl \
|
||||||
|
-var macos_version=tahoe \
|
||||||
|
-var 'xcode_version=["26.6"]' \
|
||||||
|
-var expected_runtimes_file=data/expected.tahoe.runtimes.txt
|
||||||
|
|
||||||
|
validate templates/xcode.pkr.hcl \
|
||||||
|
-var macos_version=tahoe \
|
||||||
|
-var 'xcode_version=["27-beta-6"]' \
|
||||||
|
-var 'xcode_components=["MetalToolchain"]'
|
||||||
|
|
||||||
|
validate templates/xcode.pkr.hcl \
|
||||||
|
-var macos_version=tahoe \
|
||||||
|
-var 'xcode_version=["27"]' \
|
||||||
|
-var 'xcode_components=["MetalToolchain"]'
|
||||||
|
|
||||||
|
validate templates/xcode.pkr.hcl \
|
||||||
|
-var macos_version=tahoe \
|
||||||
|
-var 'xcode_version=["27.1"]' \
|
||||||
|
-var 'xcode_components=["MetalToolchain"]'
|
||||||
|
|
||||||
|
build-vanilla:
|
||||||
|
name: Build Vanilla Image (${{ matrix.macos_version }})
|
||||||
|
needs: packer-validate
|
||||||
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
runs-on: [self-hosted, macOS, ARM64]
|
||||||
|
timeout-minutes: 180
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 1
|
||||||
|
matrix:
|
||||||
|
macos_version:
|
||||||
|
- golden-gate
|
||||||
|
- tahoe
|
||||||
|
- sequoia
|
||||||
|
- sonoma
|
||||||
|
- monterey
|
||||||
|
env:
|
||||||
|
MACOS_VERSION: ${{ matrix.macos_version }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Select image
|
||||||
|
id: select
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
|
||||||
|
|
||||||
|
build=false
|
||||||
|
if grep -Fxq "templates/vanilla-$MACOS_VERSION.pkr.hcl" changed-files.txt; then
|
||||||
|
build=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "build=$build" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Tool versions
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart --version
|
||||||
|
packer --version
|
||||||
|
|
||||||
|
- name: Build vanilla image
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
packer init "templates/vanilla-$MACOS_VERSION.pkr.hcl"
|
||||||
|
packer build "templates/vanilla-$MACOS_VERSION.pkr.hcl"
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always() && steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart delete "$MACOS_VERSION-vanilla" || true
|
||||||
|
|
||||||
|
build-base:
|
||||||
|
name: Build Base Image (${{ matrix.macos_version }})
|
||||||
|
needs: packer-validate
|
||||||
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
runs-on: [self-hosted, macOS, ARM64]
|
||||||
|
timeout-minutes: 180
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 1
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- macos_version: sonoma
|
||||||
|
disable_sip_template: disable-sip.pkr.hcl
|
||||||
|
- macos_version: sequoia
|
||||||
|
disable_sip_template: disable-sip-with-username.pkr.hcl
|
||||||
|
- macos_version: tahoe
|
||||||
|
disable_sip_template: disable-sip-with-username.pkr.hcl
|
||||||
|
- macos_version: golden-gate
|
||||||
|
disable_sip_template: disable-sip-with-username.pkr.hcl
|
||||||
|
env:
|
||||||
|
DISABLE_SIP_TEMPLATE: ${{ matrix.disable_sip_template }}
|
||||||
|
MACOS_VERSION: ${{ matrix.macos_version }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Select image
|
||||||
|
id: select
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
|
||||||
|
|
||||||
|
build=false
|
||||||
|
if grep -Eq '^(\.github/workflows/base\.yml|templates/base\.pkr\.hcl|templates/disable-sip.*\.pkr\.hcl|data/(github_known_hosts|limit\.maxfiles\.plist|setup-info-template\.json|tart-guest-.*\.plist|tart-metal-capabilities/)|scripts/(automationmodetool\.expect|(install-actions-runner|install-tart-metal-capabilities|update-tcc-database)\.sh)|ansible/)' changed-files.txt; then
|
||||||
|
build=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "build=$build" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Tool versions
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart --version
|
||||||
|
packer --version
|
||||||
|
|
||||||
|
- name: Pull vanilla image
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart pull "ghcr.io/cirruslabs/macos-$MACOS_VERSION-vanilla:latest"
|
||||||
|
tart clone "ghcr.io/cirruslabs/macos-$MACOS_VERSION-vanilla:latest" "$MACOS_VERSION-base"
|
||||||
|
|
||||||
|
- name: Disable SIP
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
packer init "templates/$DISABLE_SIP_TEMPLATE"
|
||||||
|
packer build -var "vm_name=$MACOS_VERSION-base" "templates/$DISABLE_SIP_TEMPLATE"
|
||||||
|
|
||||||
|
- name: Build base image
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
packer init templates/base.pkr.hcl
|
||||||
|
packer build -var "vm_name=$MACOS_VERSION-base" templates/base.pkr.hcl
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always() && steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart delete "$MACOS_VERSION-base" || true
|
||||||
|
|
||||||
|
build-runner:
|
||||||
|
name: Build Runner Image (${{ matrix.macos_version }})
|
||||||
|
needs: packer-validate
|
||||||
|
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||||
|
runs-on: [self-hosted, macOS, ARM64]
|
||||||
|
timeout-minutes: 180
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
max-parallel: 1
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- macos_version: tahoe
|
||||||
|
xcode_versions: '"27.1","26.6","27-beta-6","27","26.5","27-beta","26.4.1","26.3","26.2","26.1.1","26.0.1"'
|
||||||
|
additional_ios_builds: "18.6"
|
||||||
|
additional_tvos_builds: ""
|
||||||
|
xcode_components: '"MetalToolchain"'
|
||||||
|
disk_size: 520
|
||||||
|
- macos_version: sequoia
|
||||||
|
xcode_versions: '"26.0.1",16.4,16.3,16.2,16.1,16'
|
||||||
|
additional_ios_builds: "18.5,18.4,18.2,17.5"
|
||||||
|
additional_tvos_builds: "17.5"
|
||||||
|
xcode_components: '"MetalToolchain"'
|
||||||
|
disk_size: 380
|
||||||
|
- macos_version: golden-gate
|
||||||
|
xcode_versions: '"27.1","27"'
|
||||||
|
additional_ios_builds: ""
|
||||||
|
additional_tvos_builds: ""
|
||||||
|
xcode_components: '"MetalToolchain"'
|
||||||
|
disk_size: 240
|
||||||
|
env:
|
||||||
|
ADDITIONAL_IOS_BUILDS: ${{ matrix.additional_ios_builds }}
|
||||||
|
ADDITIONAL_TVOS_BUILDS: ${{ matrix.additional_tvos_builds }}
|
||||||
|
DISK_SIZE: ${{ matrix.disk_size }}
|
||||||
|
MACOS_VERSION: ${{ matrix.macos_version }}
|
||||||
|
XCODE_COMPONENTS: ${{ matrix.xcode_components }}
|
||||||
|
XCODE_VERSIONS: ${{ matrix.xcode_versions }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Select image
|
||||||
|
id: select
|
||||||
|
env:
|
||||||
|
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||||
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
|
||||||
|
|
||||||
|
build=false
|
||||||
|
if grep -Fxq ".github/workflows/release.yml" changed-files.txt; then
|
||||||
|
build=true
|
||||||
|
elif grep -Fxq "data/expected.$MACOS_VERSION.runtimes.txt" changed-files.txt; then
|
||||||
|
build=true
|
||||||
|
elif grep -Fxq "scripts/finalize-$MACOS_VERSION.sh" changed-files.txt; then
|
||||||
|
build=true
|
||||||
|
elif grep -Eq '^(templates/xcode\.pkr\.hcl|data/setup-info-template\.json|scripts/install-actions-runner\.sh)$' changed-files.txt; then
|
||||||
|
build=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "build=$build" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Tool versions
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart --version
|
||||||
|
packer --version
|
||||||
|
|
||||||
|
- name: Pull base image
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart pull "ghcr.io/cirruslabs/macos-$MACOS_VERSION-base:latest"
|
||||||
|
|
||||||
|
- name: Prepare Xcode archives
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
timeout-minutes: 30
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
source ~/.zprofile || true
|
||||||
|
|
||||||
|
if ! command -v xcodes >/dev/null; then
|
||||||
|
brew install xcodes
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$HOME/XcodesCache"
|
||||||
|
download_dir="$(mktemp -d "$HOME/XcodesCache/.download.XXXXXX")"
|
||||||
|
trap 'rm -rf "$download_dir"' EXIT
|
||||||
|
IFS=',' read -ra versions <<< "$XCODE_VERSIONS"
|
||||||
|
|
||||||
|
for raw_version in "${versions[@]}"; do
|
||||||
|
version="${raw_version//\"/}"
|
||||||
|
target="$HOME/XcodesCache/Xcode_${version}.xip"
|
||||||
|
|
||||||
|
if [[ -f "$target" ]]; then
|
||||||
|
echo "Using cached Xcode $version at $target"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Downloading Xcode $version"
|
||||||
|
download_args=(download "$version" --directory "$download_dir")
|
||||||
|
if [[ -n "${FASTLANE_SESSION:-}" ]]; then
|
||||||
|
download_args+=(--use-fastlane-auth)
|
||||||
|
elif [[ -n "${FASTLANE_USER:-}" ]]; then
|
||||||
|
# An explicit user reads a cookie file instead of FASTLANE_SESSION.
|
||||||
|
download_args+=(--use-fastlane-auth --fastlane-user "$FASTLANE_USER")
|
||||||
|
fi
|
||||||
|
xcodes "${download_args[@]}" < /dev/null
|
||||||
|
|
||||||
|
archives=("$download_dir"/*.xip)
|
||||||
|
if [[ ${#archives[@]} -ne 1 || ! -f "${archives[0]}" ]]; then
|
||||||
|
echo "::error::Expected exactly one downloaded Xcode archive for $version."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mv "${archives[0]}" "$target"
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Build runner image
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart clone "ghcr.io/cirruslabs/macos-$MACOS_VERSION-base:latest" "$MACOS_VERSION-xcode:runner"
|
||||||
|
tart set "$MACOS_VERSION-xcode:runner" --disk-size "$DISK_SIZE"
|
||||||
|
packer init templates/xcode.pkr.hcl
|
||||||
|
packer build \
|
||||||
|
-var tag=runner \
|
||||||
|
-var "disk_size=$DISK_SIZE" \
|
||||||
|
-var disk_free_mb=100000 \
|
||||||
|
-var "macos_version=$MACOS_VERSION" \
|
||||||
|
-var "xcode_version=[$XCODE_VERSIONS]" \
|
||||||
|
-var "additional_ios_builds=[$ADDITIONAL_IOS_BUILDS]" \
|
||||||
|
-var "additional_tvos_builds=[$ADDITIONAL_TVOS_BUILDS]" \
|
||||||
|
-var "xcode_components=[$XCODE_COMPONENTS]" \
|
||||||
|
-var "expected_runtimes_file=data/expected.$MACOS_VERSION.runtimes.txt" \
|
||||||
|
templates/xcode.pkr.hcl
|
||||||
|
|
||||||
|
- name: Finalize runner image
|
||||||
|
if: steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
if [[ -f "scripts/finalize-$MACOS_VERSION.sh" ]]; then
|
||||||
|
packer build \
|
||||||
|
-var "vm_name=$MACOS_VERSION-xcode:runner" \
|
||||||
|
-var "script_path=scripts/finalize-$MACOS_VERSION.sh" \
|
||||||
|
templates/exex-script.pkr.hcl
|
||||||
|
else
|
||||||
|
echo "Skipping prepare script for $MACOS_VERSION"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Cleanup
|
||||||
|
if: always() && steps.select.outputs.build == 'true'
|
||||||
|
run: |
|
||||||
|
tart delete "$MACOS_VERSION-xcode:runner" || true
|
||||||
@@ -13,6 +13,7 @@ on:
|
|||||||
type: choice
|
type: choice
|
||||||
options:
|
options:
|
||||||
- all
|
- all
|
||||||
|
- golden-gate
|
||||||
- tahoe
|
- tahoe
|
||||||
- sequoia
|
- sequoia
|
||||||
- sonoma
|
- sonoma
|
||||||
@@ -46,6 +47,7 @@ jobs:
|
|||||||
max-parallel: 1
|
max-parallel: 1
|
||||||
matrix:
|
matrix:
|
||||||
macos_version:
|
macos_version:
|
||||||
|
- golden-gate
|
||||||
- tahoe
|
- tahoe
|
||||||
- sequoia
|
- sequoia
|
||||||
- sonoma
|
- sonoma
|
||||||
|
|||||||
@@ -5,17 +5,46 @@ GitHub Actions runners, [Cirrus Runners](https://cirrus-runners.app/) or [any ot
|
|||||||
|
|
||||||
The following image variants are currently available:
|
The following image variants are currently available:
|
||||||
|
|
||||||
* `macos-{tahoe,sequoia,sonoma}-vanilla` — a vanilla macOS installation with helpful tweaks such as auto-login, but no additional software preinstalled
|
* `macos-{golden-gate,tahoe,sequoia,sonoma}-vanilla` — a vanilla macOS installation with helpful tweaks such as auto-login, but no additional software preinstalled
|
||||||
* `macos-{tahoe,sequoia,sonoma}-base` — based on `macos-{tahoe,sequoia,sonoma}-vanilla` image, it comes with `brew` and [other useful software](https://github.com/cirruslabs/macos-image-templates/blob/main/templates/base.pkr.hcl) pre-installed, but without Xcode
|
* `macos-{golden-gate,tahoe,sequoia,sonoma}-base` — based on `macos-{golden-gate,tahoe,sequoia,sonoma}-vanilla` image, it comes with `brew` and [other useful software](https://github.com/cirruslabs/macos-image-templates/blob/main/templates/base.pkr.hcl) pre-installed, but without Xcode
|
||||||
* `macos-{tahoe,sequoia,sonoma}-xcode:N` — based on `macos-{tahoe,sequoia,sonoma}-base` image and has `Xcode N` with [`Flutter`](https://flutter.dev/) pre-installed
|
* `macos-{golden-gate,tahoe,sequoia,sonoma}-xcode:N` — based on `macos-{golden-gate,tahoe,sequoia,sonoma}-base` image and has `Xcode N` with [`Flutter`](https://flutter.dev/) pre-installed
|
||||||
* `macos-runner:{tahoe,sequoia,sonoma}` — a variant of `xcode:N` with several versions of `Xcode` pre-installed and [`xcodes` tool](https://github.com/XcodesOrg/xcodes) to switch between them.
|
* `macos-runner:{golden-gate,tahoe,sequoia,sonoma}` — a variant of `xcode:N` with one or more versions of `Xcode` pre-installed and [`xcodes` tool](https://github.com/XcodesOrg/xcodes) to switch between them.
|
||||||
|
|
||||||
See a full list of VMs available [here](https://github.com/orgs/cirruslabs/packages?tab=packages&q=macos-).
|
See a full list of VMs available [here](https://github.com/orgs/cirruslabs/packages?tab=packages&q=macos-).
|
||||||
|
|
||||||
|
## Metal capabilities
|
||||||
|
|
||||||
|
Base images include the experimental [Tart Metal shim](data/tart-metal-capabilities),
|
||||||
|
based on [the Lume team's work](https://github.com/trycua/cua/blob/main/blog/gpu-passthrough-macos-vms.md).
|
||||||
|
It is installed at `/usr/local/lib/TartMetalCapabilities.dylib` but is not
|
||||||
|
enabled by default, including for the guest agent or `tart exec`.
|
||||||
|
|
||||||
|
Before starting the VM, enable unrestricted virtual-GPU features on the host
|
||||||
|
as the user running Tart:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
defaults write com.apple.gpusw.ParavirtualizedGraphics ForceUnrestrictedDeviceFeatureLevel -bool true
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart an already-running VM after changing that preference. To enable the
|
||||||
|
shim for one command with Apple family 9 and 64 KiB of threadgroup memory:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
tart exec my-vm /usr/bin/env \
|
||||||
|
DYLD_INSERT_LIBRARIES=/usr/local/lib/TartMetalCapabilities.dylib \
|
||||||
|
TART_METAL_APPLE_FAMILY_MAX=1009 \
|
||||||
|
/path/to/workload
|
||||||
|
```
|
||||||
|
|
||||||
|
Omit these environment variables to run normally. Each opted-in process gets
|
||||||
|
its own settings; `TART_METAL_APPLE_FAMILY_MAX=0` disables the capability
|
||||||
|
override. Protected executables may reject injection, and GPU support depends
|
||||||
|
on the host, guest, and workload.
|
||||||
|
|
||||||
## Release Cadence
|
## Release Cadence
|
||||||
|
|
||||||
Once a new version of Xcode is released, we will initiate a GitHub release which will automatically build and push
|
Once a new version of Xcode is released, we will initiate a GitHub release which will automatically build and push
|
||||||
a new version of the `macos-{tahoe,sequoia}-xcode:N`. This generally happens the next weekend after a release.
|
a new version of the `macos-{golden-gate,tahoe,sequoia}-xcode:N` images on supported macOS versions. This generally happens the next weekend after a release.
|
||||||
Please watch this repository releases to get notified about new images.
|
Please watch this repository releases to get notified about new images.
|
||||||
|
|
||||||
## Update Cadence
|
## Update Cadence
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
== Runtimes ==
|
||||||
|
iOS 27.0 (27.0 - 24A434) - com.apple.CoreSimulator.SimRuntime.iOS-27-0
|
||||||
|
iOS 27.1 (27.1 - 24A94425) - com.apple.CoreSimulator.SimRuntime.iOS-27-1
|
||||||
|
tvOS 27.0 (27.0 - 24J360) - com.apple.CoreSimulator.SimRuntime.tvOS-27-0
|
||||||
|
watchOS 27.0 (27.0 - 24R362) - com.apple.CoreSimulator.SimRuntime.watchOS-27-0
|
||||||
|
visionOS 27.0 (27.0 - 24M362) - com.apple.CoreSimulator.SimRuntime.xrOS-27-0
|
||||||
@@ -5,12 +5,16 @@ iOS 26.1 (26.1 - 23B86) - com.apple.CoreSimulator.SimRuntime.iOS-26-1
|
|||||||
iOS 26.3 (26.3.1 - 23D8133) - com.apple.CoreSimulator.SimRuntime.iOS-26-3
|
iOS 26.3 (26.3.1 - 23D8133) - com.apple.CoreSimulator.SimRuntime.iOS-26-3
|
||||||
iOS 26.4 (26.4.1 - 23E254a) - com.apple.CoreSimulator.SimRuntime.iOS-26-4
|
iOS 26.4 (26.4.1 - 23E254a) - com.apple.CoreSimulator.SimRuntime.iOS-26-4
|
||||||
iOS 26.5 (26.5 - 23F77) - com.apple.CoreSimulator.SimRuntime.iOS-26-5
|
iOS 26.5 (26.5 - 23F77) - com.apple.CoreSimulator.SimRuntime.iOS-26-5
|
||||||
tvOS 26.2 (26.2 - 23K51) - com.apple.CoreSimulator.SimRuntime.tvOS-26-2
|
iOS 27.0 (27.0 - 24A5355p) - com.apple.CoreSimulator.SimRuntime.iOS-27-0
|
||||||
tvOS 26.4 (26.4 - 23L243a) - com.apple.CoreSimulator.SimRuntime.tvOS-26-4
|
iOS 27.0 (27.0 - 24A5423a) - com.apple.CoreSimulator.SimRuntime.iOS-27-0
|
||||||
|
iOS 27.0 (27.0 - 24A434) - com.apple.CoreSimulator.SimRuntime.iOS-27-0
|
||||||
|
iOS 27.1 (27.1 - 24A94425) - com.apple.CoreSimulator.SimRuntime.iOS-27-1
|
||||||
tvOS 26.5 (26.5 - 23L470) - com.apple.CoreSimulator.SimRuntime.tvOS-26-5
|
tvOS 26.5 (26.5 - 23L470) - com.apple.CoreSimulator.SimRuntime.tvOS-26-5
|
||||||
watchOS 26.2 (26.2 - 23S303) - com.apple.CoreSimulator.SimRuntime.watchOS-26-2
|
tvOS 27.0 (27.0 - 24J5356a) - com.apple.CoreSimulator.SimRuntime.tvOS-27-0
|
||||||
watchOS 26.4 (26.4 - 23T240b) - com.apple.CoreSimulator.SimRuntime.watchOS-26-4
|
tvOS 27.0 (27.0 - 24J360) - com.apple.CoreSimulator.SimRuntime.tvOS-27-0
|
||||||
watchOS 26.5 (26.5 - 23T570) - com.apple.CoreSimulator.SimRuntime.watchOS-26-5
|
watchOS 26.5 (26.5 - 23T570) - com.apple.CoreSimulator.SimRuntime.watchOS-26-5
|
||||||
visionOS 26.2 (26.2 - 23N301) - com.apple.CoreSimulator.SimRuntime.xrOS-26-2
|
watchOS 27.0 (27.0 - 24R5355a) - com.apple.CoreSimulator.SimRuntime.watchOS-27-0
|
||||||
visionOS 26.4 (26.4.1 - 23O249a) - com.apple.CoreSimulator.SimRuntime.xrOS-26-4
|
watchOS 27.0 (27.0 - 24R362) - com.apple.CoreSimulator.SimRuntime.watchOS-27-0
|
||||||
visionOS 26.5 (26.5 - 23O470) - com.apple.CoreSimulator.SimRuntime.xrOS-26-5
|
visionOS 26.5 (26.5 - 23O470) - com.apple.CoreSimulator.SimRuntime.xrOS-26-5
|
||||||
|
visionOS 27.0 (27.0 - 24M5357a) - com.apple.CoreSimulator.SimRuntime.xrOS-27-0
|
||||||
|
visionOS 27.0 (27.0 - 24M362) - com.apple.CoreSimulator.SimRuntime.xrOS-27-0
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Cua AI, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Tart Metal capabilities
|
||||||
|
|
||||||
|
This is a vendored adaptation of the minimal [Cua/Lume Metal capability shim](https://github.com/trycua/cua/tree/3c1acf27748c3e0f8ff71cd0c9ab072b1e160997/libs/lume/metal-capability-shim)
|
||||||
|
at revision `3c1acf27748c3e0f8ff71cd0c9ab072b1e160997`. The full shim source and
|
||||||
|
Metal capability probe are copied here. The source's `Lume` identifiers,
|
||||||
|
diagnostics, and `LUME_METAL_*` settings are renamed to `Tart` and `TART_METAL_*`.
|
||||||
|
The original Cua copyright and [MIT license](LICENSE) are retained. The upstream
|
||||||
|
source SHA-256 before renaming is
|
||||||
|
`e1371b1e579bca895e6b3a2b581b9f328203d9786bf809912a4a5d1672010cce`.
|
||||||
|
|
||||||
|
The shim changes only Apple-family capability answers and selected memory
|
||||||
|
limits. It does not advertise additional Common, Mac, or Metal families. It
|
||||||
|
uses private, version-sensitive behavior; reported capabilities are not a
|
||||||
|
guarantee that every corresponding GPU operation is supported.
|
||||||
|
|
||||||
|
Configuration is read once, when each process loads the library:
|
||||||
|
|
||||||
|
| Variable | Behavior |
|
||||||
|
| --- | --- |
|
||||||
|
| `TART_METAL_APPLE_FAMILY_MAX` | Required Apple-family ceiling, from 1001 through 1999. Missing, zero, or invalid values disable the shim. |
|
||||||
|
| `TART_METAL_MAX_THREADGROUP_MEMORY` | Memory floor in bytes; defaults to 65536. |
|
||||||
|
| `TART_METAL_RECOMMENDED_WORKING_SET_SIZE` | Optional working-set floor in bytes; unchanged when unset. |
|
||||||
|
|
||||||
|
The old `LUME_METAL_*` names are not recognized. Base images install the library
|
||||||
|
without enabling it for the guest agent or other processes. See the repository's
|
||||||
|
[Metal capabilities instructions](../../README.md#metal-capabilities) for
|
||||||
|
explicit per-command activation and host setup.
|
||||||
|
|
||||||
|
From the repository root, run `bash scripts/test-tart-metal-capabilities.sh` to
|
||||||
|
build and test without installing anything on the host. The installer accepts
|
||||||
|
`DESTDIR` for staging and `TART_METAL_SOURCE_DIR` for Packer's uploaded sources.
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
// Copyright 2026 Cua AI, Inc.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Adapted from the Cua/Lume team's process-scoped Metal capability shim.
|
||||||
|
// Credit to the Lume folks for the idea and original implementation:
|
||||||
|
// https://github.com/trycua/cua/blob/3c1acf27748c3e0f8ff71cd0c9ab072b1e160997/blog/gpu-passthrough-macos-vms.md
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Metal/Metal.h>
|
||||||
|
#import <objc/runtime.h>
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
typedef unsigned long long TartU64;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
BOOL enabled;
|
||||||
|
NSUInteger appleFamilyMax;
|
||||||
|
NSUInteger maxThreadgroupMemory;
|
||||||
|
BOOL hasRecommendedWorkingSetSize;
|
||||||
|
NSUInteger recommendedWorkingSetSize;
|
||||||
|
} TartMetalConfiguration;
|
||||||
|
|
||||||
|
static TartMetalConfiguration gConfiguration = {0};
|
||||||
|
static IMP gOriginalInitGPUFamilySupport = NULL;
|
||||||
|
static IMP gOriginalMaxThreadgroupMemoryLength = NULL;
|
||||||
|
static IMP gOriginalRecommendedMaxWorkingSetSize = NULL;
|
||||||
|
static IMP gOriginalSupportsFamily = NULL;
|
||||||
|
static BOOL gDeviceHooksInstalled = NO;
|
||||||
|
|
||||||
|
static BOOL parseUnsignedEnvironmentValue(const char *name, TartU64 *value) {
|
||||||
|
const char *rawValue = getenv(name);
|
||||||
|
if (!rawValue || !*rawValue) return NO;
|
||||||
|
|
||||||
|
errno = 0;
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long long parsed = strtoull(rawValue, &end, 0);
|
||||||
|
if (errno != 0 || end == rawValue || !end || *end != '\0') return NO;
|
||||||
|
|
||||||
|
*value = parsed;
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL loadConfiguration(void) {
|
||||||
|
TartU64 appleFamilyMax = 0;
|
||||||
|
if (!parseUnsignedEnvironmentValue(
|
||||||
|
"TART_METAL_APPLE_FAMILY_MAX",
|
||||||
|
&appleFamilyMax
|
||||||
|
) || appleFamilyMax < 1001 || appleFamilyMax >= 2000) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
TartU64 maxThreadgroupMemory = 65536;
|
||||||
|
const char *rawThreadgroupMemory = getenv("TART_METAL_MAX_THREADGROUP_MEMORY");
|
||||||
|
if (rawThreadgroupMemory && *rawThreadgroupMemory &&
|
||||||
|
!parseUnsignedEnvironmentValue("TART_METAL_MAX_THREADGROUP_MEMORY", &maxThreadgroupMemory)) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
TartU64 recommendedWorkingSetSize = 0;
|
||||||
|
const char *rawWorkingSetSize = getenv("TART_METAL_RECOMMENDED_WORKING_SET_SIZE");
|
||||||
|
BOOL hasRecommendedWorkingSetSize = rawWorkingSetSize && *rawWorkingSetSize;
|
||||||
|
if (hasRecommendedWorkingSetSize &&
|
||||||
|
!parseUnsignedEnvironmentValue(
|
||||||
|
"TART_METAL_RECOMMENDED_WORKING_SET_SIZE",
|
||||||
|
&recommendedWorkingSetSize
|
||||||
|
)) {
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
gConfiguration.enabled = YES;
|
||||||
|
gConfiguration.appleFamilyMax = (NSUInteger)appleFamilyMax;
|
||||||
|
gConfiguration.maxThreadgroupMemory = (NSUInteger)maxThreadgroupMemory;
|
||||||
|
gConfiguration.hasRecommendedWorkingSetSize = hasRecommendedWorkingSetSize;
|
||||||
|
gConfiguration.recommendedWorkingSetSize = (NSUInteger)recommendedWorkingSetSize;
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSUInteger hookMaxThreadgroupMemoryLength(id self, SEL selector) {
|
||||||
|
NSUInteger original = gOriginalMaxThreadgroupMemoryLength
|
||||||
|
? ((NSUInteger(*)(id, SEL))(void *)gOriginalMaxThreadgroupMemoryLength)(self, selector)
|
||||||
|
: 0;
|
||||||
|
return original < gConfiguration.maxThreadgroupMemory
|
||||||
|
? gConfiguration.maxThreadgroupMemory
|
||||||
|
: original;
|
||||||
|
}
|
||||||
|
|
||||||
|
static NSUInteger hookRecommendedMaxWorkingSetSize(id self, SEL selector) {
|
||||||
|
NSUInteger original = gOriginalRecommendedMaxWorkingSetSize
|
||||||
|
? ((NSUInteger(*)(id, SEL))(void *)gOriginalRecommendedMaxWorkingSetSize)(self, selector)
|
||||||
|
: 0;
|
||||||
|
return original < gConfiguration.recommendedWorkingSetSize
|
||||||
|
? gConfiguration.recommendedWorkingSetSize
|
||||||
|
: original;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL hookSupportsFamily(id self, SEL selector, NSUInteger family) {
|
||||||
|
BOOL original = gOriginalSupportsFamily
|
||||||
|
? ((BOOL(*)(id, SEL, NSUInteger))(void *)gOriginalSupportsFamily)(self, selector, family)
|
||||||
|
: NO;
|
||||||
|
BOOL isConfiguredAppleFamily = family >= 1001 &&
|
||||||
|
family <= gConfiguration.appleFamilyMax;
|
||||||
|
return original || isConfiguredAppleFamily;
|
||||||
|
}
|
||||||
|
|
||||||
|
static BOOL replaceMethod(
|
||||||
|
Class deviceClass,
|
||||||
|
NSString *selectorName,
|
||||||
|
IMP replacement,
|
||||||
|
IMP *original
|
||||||
|
) {
|
||||||
|
SEL selector = NSSelectorFromString(selectorName);
|
||||||
|
Method method = class_getInstanceMethod(deviceClass, selector);
|
||||||
|
if (!method) return NO;
|
||||||
|
|
||||||
|
*original = method_setImplementation(method, replacement);
|
||||||
|
return *original != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void installDeviceHooks(id device) {
|
||||||
|
@synchronized([device class]) {
|
||||||
|
if (gDeviceHooksInstalled) return;
|
||||||
|
|
||||||
|
Class deviceClass = [device class];
|
||||||
|
Method maxThreadgroupMemory = class_getInstanceMethod(
|
||||||
|
deviceClass,
|
||||||
|
NSSelectorFromString(@"maxThreadgroupMemoryLength")
|
||||||
|
);
|
||||||
|
Method supportsFamily = class_getInstanceMethod(
|
||||||
|
deviceClass,
|
||||||
|
NSSelectorFromString(@"supportsFamily:")
|
||||||
|
);
|
||||||
|
Method recommendedWorkingSetSize = gConfiguration.hasRecommendedWorkingSetSize
|
||||||
|
? class_getInstanceMethod(
|
||||||
|
deviceClass,
|
||||||
|
NSSelectorFromString(@"recommendedMaxWorkingSetSize")
|
||||||
|
)
|
||||||
|
: NULL;
|
||||||
|
|
||||||
|
if (!maxThreadgroupMemory || !supportsFamily ||
|
||||||
|
(gConfiguration.hasRecommendedWorkingSetSize && !recommendedWorkingSetSize)) {
|
||||||
|
NSLog(@"[TartMetalCapabilities] Required device methods are unavailable; leaving stock capabilities unchanged");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOL installed = replaceMethod(
|
||||||
|
deviceClass,
|
||||||
|
@"maxThreadgroupMemoryLength",
|
||||||
|
(IMP)hookMaxThreadgroupMemoryLength,
|
||||||
|
&gOriginalMaxThreadgroupMemoryLength
|
||||||
|
);
|
||||||
|
installed = installed && replaceMethod(
|
||||||
|
deviceClass,
|
||||||
|
@"supportsFamily:",
|
||||||
|
(IMP)hookSupportsFamily,
|
||||||
|
&gOriginalSupportsFamily
|
||||||
|
);
|
||||||
|
|
||||||
|
if (installed && gConfiguration.hasRecommendedWorkingSetSize) {
|
||||||
|
installed = replaceMethod(
|
||||||
|
deviceClass,
|
||||||
|
@"recommendedMaxWorkingSetSize",
|
||||||
|
(IMP)hookRecommendedMaxWorkingSetSize,
|
||||||
|
&gOriginalRecommendedMaxWorkingSetSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!installed) {
|
||||||
|
NSLog(@"[TartMetalCapabilities] Capability hook installation was incomplete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
gDeviceHooksInstalled = YES;
|
||||||
|
NSLog(
|
||||||
|
@"[TartMetalCapabilities] Enabled for %@ (appleFamilyMax=%llu maxThreadgroupMemory=%llu)",
|
||||||
|
[NSProcessInfo processInfo].processName,
|
||||||
|
(TartU64)gConfiguration.appleFamilyMax,
|
||||||
|
(TartU64)gConfiguration.maxThreadgroupMemory
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void hookInitGPUFamilySupport(id self, SEL selector) {
|
||||||
|
installDeviceHooks(self);
|
||||||
|
((void(*)(id, SEL))(void *)gOriginalInitGPUFamilySupport)(self, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
__attribute__((constructor))
|
||||||
|
static void initializeTartMetalCapabilities(void) {
|
||||||
|
@autoreleasepool {
|
||||||
|
if (!loadConfiguration()) return;
|
||||||
|
|
||||||
|
Class deviceClass = NSClassFromString(@"_MTLDevice");
|
||||||
|
if (!deviceClass) return;
|
||||||
|
|
||||||
|
Method method = class_getInstanceMethod(
|
||||||
|
deviceClass,
|
||||||
|
NSSelectorFromString(@"initGPUFamilySupport")
|
||||||
|
);
|
||||||
|
if (!method) return;
|
||||||
|
|
||||||
|
gOriginalInitGPUFamilySupport = method_setImplementation(
|
||||||
|
method,
|
||||||
|
(IMP)hookInitGPUFamilySupport
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#import "../Sources/TartMetalCapabilities.m"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static void resetConfiguration(void) {
|
||||||
|
unsetenv("TART_METAL_APPLE_FAMILY_MAX");
|
||||||
|
unsetenv("TART_METAL_MAX_THREADGROUP_MEMORY");
|
||||||
|
unsetenv("TART_METAL_RECOMMENDED_WORKING_SET_SIZE");
|
||||||
|
unsetenv("LUME_METAL_APPLE_FAMILY_MAX");
|
||||||
|
gConfiguration = (TartMetalConfiguration){0};
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, const char *argv[]) {
|
||||||
|
// In this mode, check the configuration loaded by the real constructor
|
||||||
|
// before main(), as it would be for a newly executed workload.
|
||||||
|
if (argc == 2) {
|
||||||
|
NSUInteger expected = (NSUInteger)strtoull(argv[1], NULL, 10);
|
||||||
|
assert(gConfiguration.enabled == (expected != 0));
|
||||||
|
assert(gConfiguration.appleFamilyMax == expected);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
resetConfiguration();
|
||||||
|
assert(!loadConfiguration());
|
||||||
|
setenv("LUME_METAL_APPLE_FAMILY_MAX", "1009", 1);
|
||||||
|
assert(!loadConfiguration());
|
||||||
|
|
||||||
|
const char *invalidFamilies[] = {"", "0", "1000", "2000", "-1", "1009x"};
|
||||||
|
for (size_t i = 0; i < sizeof(invalidFamilies) / sizeof(invalidFamilies[0]); i++) {
|
||||||
|
resetConfiguration();
|
||||||
|
setenv("TART_METAL_APPLE_FAMILY_MAX", invalidFamilies[i], 1);
|
||||||
|
assert(!loadConfiguration());
|
||||||
|
assert(!gConfiguration.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetConfiguration();
|
||||||
|
setenv("TART_METAL_APPLE_FAMILY_MAX", "1009", 1);
|
||||||
|
assert(loadConfiguration());
|
||||||
|
assert(gConfiguration.appleFamilyMax == 1009);
|
||||||
|
assert(gConfiguration.maxThreadgroupMemory == 65536);
|
||||||
|
assert(!gConfiguration.hasRecommendedWorkingSetSize);
|
||||||
|
|
||||||
|
resetConfiguration();
|
||||||
|
setenv("TART_METAL_APPLE_FAMILY_MAX", "1999", 1);
|
||||||
|
setenv("TART_METAL_MAX_THREADGROUP_MEMORY", "32768", 1);
|
||||||
|
setenv("TART_METAL_RECOMMENDED_WORKING_SET_SIZE", "1073741824", 1);
|
||||||
|
assert(loadConfiguration());
|
||||||
|
assert(gConfiguration.appleFamilyMax == 1999);
|
||||||
|
assert(gConfiguration.maxThreadgroupMemory == 32768);
|
||||||
|
assert(gConfiguration.hasRecommendedWorkingSetSize);
|
||||||
|
assert(gConfiguration.recommendedWorkingSetSize == 1073741824);
|
||||||
|
|
||||||
|
resetConfiguration();
|
||||||
|
setenv("TART_METAL_APPLE_FAMILY_MAX", "1009", 1);
|
||||||
|
setenv("TART_METAL_MAX_THREADGROUP_MEMORY", "invalid", 1);
|
||||||
|
assert(!loadConfiguration());
|
||||||
|
unsetenv("TART_METAL_MAX_THREADGROUP_MEMORY");
|
||||||
|
setenv("TART_METAL_RECOMMENDED_WORKING_SET_SIZE", "invalid", 1);
|
||||||
|
assert(!loadConfiguration());
|
||||||
|
|
||||||
|
resetConfiguration();
|
||||||
|
puts("configuration: OK");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#include <assert.h>
|
||||||
|
#include <mach-o/dyld.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
static pid_t startChild(const char *executable, const char *library, const char *family) {
|
||||||
|
pid_t child = fork();
|
||||||
|
assert(child >= 0);
|
||||||
|
if (child == 0) {
|
||||||
|
char *libraryAssignment = NULL;
|
||||||
|
char *familyAssignment = NULL;
|
||||||
|
assert(asprintf(&libraryAssignment, "DYLD_INSERT_LIBRARIES=%s", library) >= 0);
|
||||||
|
assert(asprintf(&familyAssignment, "TART_METAL_APPLE_FAMILY_MAX=%s", family) >= 0);
|
||||||
|
execl("/usr/bin/env", "env", libraryAssignment, familyAssignment,
|
||||||
|
executable, family, "child", NULL);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
assert(argc >= 2);
|
||||||
|
const char *library = getenv("DYLD_INSERT_LIBRARIES");
|
||||||
|
const char *family = getenv("TART_METAL_APPLE_FAMILY_MAX");
|
||||||
|
const char *memory = getenv("TART_METAL_MAX_THREADGROUP_MEMORY");
|
||||||
|
int loaded = 0;
|
||||||
|
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
|
||||||
|
if (strstr(_dyld_get_image_name(i), "/TartMetalCapabilities.dylib")) loaded = 1;
|
||||||
|
}
|
||||||
|
if (strcmp(argv[1], "stock") == 0) {
|
||||||
|
assert(!library && !family && !memory && !loaded);
|
||||||
|
puts("stock process without injection: OK");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
assert(library && family && memory);
|
||||||
|
assert(strcmp(family, argv[1]) == 0);
|
||||||
|
assert(strcmp(memory, "65536") == 0);
|
||||||
|
assert(loaded);
|
||||||
|
if (argc == 3) return 0;
|
||||||
|
|
||||||
|
// Concurrent workloads must get independent overrides while their parent
|
||||||
|
// retains its explicit opt-in. Exercise the documented /usr/bin/env path.
|
||||||
|
pid_t children[] = {
|
||||||
|
startChild(argv[0], library, "1008"),
|
||||||
|
startChild(argv[0], library, "0"),
|
||||||
|
};
|
||||||
|
for (size_t i = 0; i < sizeof(children) / sizeof(children[0]); i++) {
|
||||||
|
int status = 0;
|
||||||
|
assert(waitpid(children[i], &status, 0) == children[i]);
|
||||||
|
assert(WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||||
|
}
|
||||||
|
assert(strcmp(getenv("TART_METAL_APPLE_FAMILY_MAX"), argv[1]) == 0);
|
||||||
|
puts("per-process injection and overrides: OK");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Metal/Metal.h>
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
static BOOL parseFamily(const char *raw, NSUInteger *family) {
|
||||||
|
errno = 0;
|
||||||
|
char *end = NULL;
|
||||||
|
unsigned long long parsed = strtoull(raw, &end, 0);
|
||||||
|
if (errno != 0 || end == raw || !end || *end != '\0') return NO;
|
||||||
|
*family = (NSUInteger)parsed;
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, const char *argv[]) {
|
||||||
|
@autoreleasepool {
|
||||||
|
NSUInteger family = 1009;
|
||||||
|
if (argc > 1 && !parseFamily(argv[1], &family)) {
|
||||||
|
fprintf(stderr, "invalid family: %s\n", argv[1]);
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
id<MTLDevice> device = MTLCreateSystemDefaultDevice();
|
||||||
|
if (!device) {
|
||||||
|
fprintf(stderr, "Metal device unavailable\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("device=%s\n", device.name.UTF8String);
|
||||||
|
printf("family=%llu\n", (unsigned long long)family);
|
||||||
|
printf(
|
||||||
|
"supports_family=%s\n",
|
||||||
|
[device supportsFamily:(MTLGPUFamily)family] ? "true" : "false"
|
||||||
|
);
|
||||||
|
printf(
|
||||||
|
"max_threadgroup_memory=%llu\n",
|
||||||
|
(unsigned long long)device.maxThreadgroupMemoryLength
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Packer supplies the uploaded source directory. Local builds use the vendored
|
||||||
|
# copy next to this script, so building an image needs no upstream download.
|
||||||
|
source_dir=${TART_METAL_SOURCE_DIR:-"$(cd "$(dirname "${BASH_SOURCE[0]}")/../data/tart-metal-capabilities" && pwd)"}
|
||||||
|
work_dir=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$work_dir"' EXIT
|
||||||
|
test -f "$source_dir/LICENSE"
|
||||||
|
|
||||||
|
# One path must work for the native guest agent, arm64e processes, and Rosetta
|
||||||
|
# children. Keep the deployment target compatible with the oldest template.
|
||||||
|
xcrun clang \
|
||||||
|
-arch arm64 -arch arm64e -arch x86_64 \
|
||||||
|
-O3 -Wall -Wextra -Werror -fobjc-arc -fblocks -fvisibility=hidden \
|
||||||
|
-dynamiclib -install_name /usr/local/lib/TartMetalCapabilities.dylib \
|
||||||
|
-mmacosx-version-min=12.0 -framework Foundation -framework Metal \
|
||||||
|
"$source_dir/Sources/TartMetalCapabilities.m" -o "$work_dir/TartMetalCapabilities.dylib"
|
||||||
|
codesign --force --sign - "$work_dir/TartMetalCapabilities.dylib"
|
||||||
|
# Xcode 27's lipo rejects multiple architectures in one -verify_arch call.
|
||||||
|
for architecture in arm64 arm64e x86_64; do
|
||||||
|
lipo "$work_dir/TartMetalCapabilities.dylib" -verify_arch "$architecture"
|
||||||
|
done
|
||||||
|
codesign --verify --strict "$work_dir/TartMetalCapabilities.dylib"
|
||||||
|
|
||||||
|
# DESTDIR allows the exact installer to be exercised without modifying the host.
|
||||||
|
install_root=${DESTDIR:-}
|
||||||
|
install_command=(sudo install -o root -g wheel)
|
||||||
|
if [[ -n "$install_root" ]]; then
|
||||||
|
install_command=(install)
|
||||||
|
fi
|
||||||
|
"${install_command[@]}" -d -m 0755 "$install_root/usr/local/lib" \
|
||||||
|
"$install_root/usr/local/share/licenses/tart-metal-capabilities"
|
||||||
|
"${install_command[@]}" -m 0644 "$work_dir/TartMetalCapabilities.dylib" \
|
||||||
|
"$install_root/usr/local/lib/TartMetalCapabilities.dylib"
|
||||||
|
"${install_command[@]}" -m 0644 "$source_dir/LICENSE" \
|
||||||
|
"$install_root/usr/local/share/licenses/tart-metal-capabilities/LICENSE"
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
source_dir="$script_dir/../data/tart-metal-capabilities"
|
||||||
|
work_dir=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$work_dir"' EXIT
|
||||||
|
|
||||||
|
DESTDIR="$work_dir/root" TART_METAL_SOURCE_DIR="$source_dir" \
|
||||||
|
bash "$script_dir/install-tart-metal-capabilities.sh"
|
||||||
|
library="$work_dir/root/usr/local/lib/TartMetalCapabilities.dylib"
|
||||||
|
cmp "$source_dir/LICENSE" \
|
||||||
|
"$work_dir/root/usr/local/share/licenses/tart-metal-capabilities/LICENSE"
|
||||||
|
|
||||||
|
xcrun clang -O2 -Wall -Wextra -Werror -fobjc-arc \
|
||||||
|
-framework Foundation -framework Metal \
|
||||||
|
"$source_dir/Tests/configuration.m" -o "$work_dir/configuration"
|
||||||
|
/usr/bin/env -u TART_METAL_APPLE_FAMILY_MAX \
|
||||||
|
-u TART_METAL_MAX_THREADGROUP_MEMORY -u TART_METAL_RECOMMENDED_WORKING_SET_SIZE \
|
||||||
|
"$work_dir/configuration"
|
||||||
|
for family in 1009 1008 0; do
|
||||||
|
/usr/bin/env -u TART_METAL_MAX_THREADGROUP_MEMORY \
|
||||||
|
-u TART_METAL_RECOMMENDED_WORKING_SET_SIZE \
|
||||||
|
TART_METAL_APPLE_FAMILY_MAX="$family" "$work_dir/configuration" "$family"
|
||||||
|
done
|
||||||
|
|
||||||
|
xcrun clang -O2 -Wall -Wextra -Werror -arch arm64 -arch x86_64 \
|
||||||
|
-mmacosx-version-min=12.0 "$source_dir/Tests/exec-environment.c" \
|
||||||
|
-o "$work_dir/exec-environment"
|
||||||
|
run_stock() {
|
||||||
|
"$@" /usr/bin/env -u DYLD_INSERT_LIBRARIES -u TART_METAL_APPLE_FAMILY_MAX \
|
||||||
|
-u TART_METAL_MAX_THREADGROUP_MEMORY -u TART_METAL_RECOMMENDED_WORKING_SET_SIZE \
|
||||||
|
"$work_dir/exec-environment" stock
|
||||||
|
}
|
||||||
|
run_injected() {
|
||||||
|
"$@" /usr/bin/env DYLD_INSERT_LIBRARIES="$library" \
|
||||||
|
TART_METAL_APPLE_FAMILY_MAX=1009 TART_METAL_MAX_THREADGROUP_MEMORY=65536 \
|
||||||
|
"$work_dir/exec-environment" 1009
|
||||||
|
}
|
||||||
|
run_stock
|
||||||
|
run_injected
|
||||||
|
if [[ $(uname -m) == arm64 ]]; then
|
||||||
|
if arch -x86_64 /usr/bin/true; then
|
||||||
|
run_stock arch -x86_64
|
||||||
|
run_injected arch -x86_64
|
||||||
|
else
|
||||||
|
echo "Rosetta runtime check skipped: Rosetta is not installed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
for variant in agent daemon; do
|
||||||
|
plist="$script_dir/../data/tart-guest-$variant.plist"
|
||||||
|
plutil -lint "$plist"
|
||||||
|
agent_environment=$(plutil -extract EnvironmentVariables xml1 -o - "$plist")
|
||||||
|
if printf '%s\n' "$agent_environment" \
|
||||||
|
| grep -Eq '<key>(DYLD_INSERT_LIBRARIES|TART_METAL_[^<]+)</key>'; then
|
||||||
|
echo "Unexpected default Metal injection in $plist" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
export TCC_TEST_ROOT
|
||||||
|
TCC_TEST_ROOT=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$TCC_TEST_ROOT"' EXIT
|
||||||
|
export TCC_TEST_SQLITE
|
||||||
|
TCC_TEST_SQLITE=$(command -v sqlite3)
|
||||||
|
export TCC_TEST_SYSTEM_DB='/Library/Application Support/com.apple.TCC/TCC.db'
|
||||||
|
export TCC_TEST_LEGACY_DB="$HOME/Library/Application Support/com.apple.TCC/TCC.db"
|
||||||
|
export TCC_TEST_PROTECTED_DB='/private/var/containers/Data/ProtectedSystem/TEST-USER/Data/Library/Application Support/com.apple.TCC/TCC.db'
|
||||||
|
export TCC_TEST_AGENT='/opt/homebrew/Cellar/tart-guest-agent/0.13.0/bin/tart-guest-agent'
|
||||||
|
|
||||||
|
# Run the real provisioning script against temporary SQLite databases. Every
|
||||||
|
# privileged operation is intercepted; these tests never access the host's TCC.
|
||||||
|
source() {
|
||||||
|
[[ $# == 1 && "$1" == "$HOME/.zprofile" ]]
|
||||||
|
}
|
||||||
|
sw_vers() {
|
||||||
|
[[ $# == 1 && "$1" == -productVersion ]] || return 1
|
||||||
|
printf '%s\n' "$TCC_TEST_MACOS_VERSION"
|
||||||
|
}
|
||||||
|
id() {
|
||||||
|
[[ $# == 1 && "$1" == -u ]] || return 1
|
||||||
|
printf '501\n'
|
||||||
|
}
|
||||||
|
realpath() {
|
||||||
|
[[ $# == 1 && "$1" == /opt/homebrew/bin/tart-guest-agent ]] || return 1
|
||||||
|
[[ "$TCC_TEST_AGENT_EXISTS" == 1 ]] || return 1
|
||||||
|
printf '%s\n' "$TCC_TEST_AGENT"
|
||||||
|
}
|
||||||
|
sudo() {
|
||||||
|
local subcommand="$1" database
|
||||||
|
shift
|
||||||
|
case "$subcommand" in
|
||||||
|
lsof)
|
||||||
|
[[ "$TCC_TEST_EXPECT_LSOF" == 1 && "$*" == '-a -u 501 -c tccd -Fn' ]] || return 1
|
||||||
|
printf '%s\n' "$TCC_TEST_OPEN_FILES"
|
||||||
|
return "$TCC_TEST_LSOF_STATUS"
|
||||||
|
;;
|
||||||
|
test)
|
||||||
|
[[ $# == 2 && "$1" == -f ]] || return 1
|
||||||
|
case "$2" in
|
||||||
|
"$TCC_TEST_LEGACY_DB") [[ "$TCC_TEST_LEGACY_EXISTS" == 1 ]] ;;
|
||||||
|
"$TCC_TEST_PROTECTED_DB") [[ "$TCC_TEST_PROTECTED_EXISTS" == 1 ]] ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
stat)
|
||||||
|
[[ $# == 3 && "$1" == -f && "$2" == %u && "$3" == "$TCC_TEST_PROTECTED_DB" ]] || return 1
|
||||||
|
printf '%s\n' "$TCC_TEST_OWNER"
|
||||||
|
;;
|
||||||
|
sqlite3)
|
||||||
|
[[ $# == 1 ]] || return 1
|
||||||
|
case "$1" in
|
||||||
|
"$TCC_TEST_SYSTEM_DB") database="$TCC_TEST_ROOT/system.db" ;;
|
||||||
|
"$TCC_TEST_LEGACY_DB"|"$TCC_TEST_PROTECTED_DB") database="$TCC_TEST_ROOT/user.db" ;;
|
||||||
|
*) echo "Unexpected database: $1" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
printf '%s\n' "$1" >> "$TCC_TEST_ROOT/writes"
|
||||||
|
"$TCC_TEST_SQLITE" "$database"
|
||||||
|
;;
|
||||||
|
*) echo "Unexpected sudo command: $subcommand" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
export -f source sw_vers id realpath sudo
|
||||||
|
|
||||||
|
reset_case() {
|
||||||
|
export TCC_TEST_MACOS_VERSION=26.6.2 TCC_TEST_AGENT_EXISTS=1
|
||||||
|
export TCC_TEST_LEGACY_EXISTS=1 TCC_TEST_PROTECTED_EXISTS=1 TCC_TEST_OWNER=501
|
||||||
|
export TCC_TEST_EXPECT_LSOF=0 TCC_TEST_LSOF_STATUS=0 TCC_TEST_OPEN_FILES=''
|
||||||
|
: > "$TCC_TEST_ROOT/writes"
|
||||||
|
local database
|
||||||
|
for database in system user; do
|
||||||
|
"$TCC_TEST_SQLITE" "$TCC_TEST_ROOT/$database.db" <<'SQL'
|
||||||
|
DROP TABLE IF EXISTS access;
|
||||||
|
CREATE TABLE access (
|
||||||
|
service TEXT NOT NULL,
|
||||||
|
client_type INTEGER NOT NULL,
|
||||||
|
client TEXT NOT NULL,
|
||||||
|
auth_value INTEGER NOT NULL,
|
||||||
|
auth_reason INTEGER NOT NULL,
|
||||||
|
auth_version INTEGER NOT NULL,
|
||||||
|
indirect_object_identifier_type INTEGER,
|
||||||
|
indirect_object_identifier TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (service, client, client_type, indirect_object_identifier)
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
golden_gate_case() {
|
||||||
|
reset_case
|
||||||
|
export TCC_TEST_MACOS_VERSION=27.0 TCC_TEST_EXPECT_LSOF=1
|
||||||
|
# Include an obsolete legacy copy, the system database, a WAL, and duplicate
|
||||||
|
# descriptors. Only the current user's active protected database may win.
|
||||||
|
TCC_TEST_OPEN_FILES=$(printf 'p123\nn%s\nn%s-wal\nn%s\nn%s\nn%s\n' \
|
||||||
|
"$TCC_TEST_SYSTEM_DB" "$TCC_TEST_PROTECTED_DB" "$TCC_TEST_LEGACY_DB" \
|
||||||
|
"$TCC_TEST_PROTECTED_DB" "$TCC_TEST_PROTECTED_DB")
|
||||||
|
export TCC_TEST_OPEN_FILES
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal() {
|
||||||
|
if [[ "$1" != "$2" ]]; then
|
||||||
|
printf 'Expected: %s\nActual: %s\n' "$1" "$2" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_success() {
|
||||||
|
local user_database="$1" database expected_writes
|
||||||
|
bash "$script_dir/update-tcc-database.sh"
|
||||||
|
bash "$script_dir/update-tcc-database.sh"
|
||||||
|
expected_writes=$(printf '%s\n%s\n%s\n%s' \
|
||||||
|
"$TCC_TEST_SYSTEM_DB" "$user_database" "$TCC_TEST_SYSTEM_DB" "$user_database")
|
||||||
|
assert_equal "$expected_writes" "$(< "$TCC_TEST_ROOT/writes")"
|
||||||
|
for database in system user; do
|
||||||
|
assert_equal 18 "$("$TCC_TEST_SQLITE" "$TCC_TEST_ROOT/$database.db" 'SELECT count(*) FROM access WHERE auth_value=2;')"
|
||||||
|
assert_equal 4 "$("$TCC_TEST_SQLITE" "$TCC_TEST_ROOT/$database.db" "SELECT count(*) FROM access WHERE client='$TCC_TEST_AGENT' AND client_type=1;")"
|
||||||
|
assert_equal 1 "$("$TCC_TEST_SQLITE" "$TCC_TEST_ROOT/$database.db" "SELECT count(*) FROM access WHERE client='org.python.python' AND service='kTCCServiceMicrophone';")"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_failure() {
|
||||||
|
if bash "$script_dir/update-tcc-database.sh" > "$TCC_TEST_ROOT/output" 2>&1; then
|
||||||
|
echo 'Expected provisioning to fail' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -n "$1" ]] && ! grep -Fq "$1" "$TCC_TEST_ROOT/output"; then
|
||||||
|
cat "$TCC_TEST_ROOT/output" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
assert_equal '' "$(< "$TCC_TEST_ROOT/writes")"
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_case
|
||||||
|
expect_success "$TCC_TEST_LEGACY_DB"
|
||||||
|
reset_case
|
||||||
|
TCC_TEST_MACOS_VERSION=12.7.6
|
||||||
|
expect_success "$TCC_TEST_LEGACY_DB"
|
||||||
|
golden_gate_case
|
||||||
|
expect_success "$TCC_TEST_PROTECTED_DB"
|
||||||
|
|
||||||
|
reset_case
|
||||||
|
TCC_TEST_LEGACY_EXISTS=0
|
||||||
|
expect_failure 'User TCC database does not exist'
|
||||||
|
golden_gate_case
|
||||||
|
TCC_TEST_LSOF_STATUS=1
|
||||||
|
expect_failure 'Unable to inspect the user TCC daemon'
|
||||||
|
golden_gate_case
|
||||||
|
TCC_TEST_OPEN_FILES="n$TCC_TEST_SYSTEM_DB"
|
||||||
|
expect_failure 'Unable to find the active user TCC database'
|
||||||
|
golden_gate_case
|
||||||
|
TCC_TEST_OPEN_FILES="$TCC_TEST_OPEN_FILES"$'\n'"n${TCC_TEST_PROTECTED_DB/TEST-USER/OTHER-USER}"
|
||||||
|
expect_failure 'Found multiple active user TCC databases'
|
||||||
|
golden_gate_case
|
||||||
|
TCC_TEST_PROTECTED_EXISTS=0
|
||||||
|
expect_failure 'User TCC database does not exist'
|
||||||
|
golden_gate_case
|
||||||
|
TCC_TEST_OWNER=502
|
||||||
|
expect_failure 'Unexpected owner for user TCC database'
|
||||||
|
golden_gate_case
|
||||||
|
TCC_TEST_AGENT_EXISTS=0
|
||||||
|
expect_failure ''
|
||||||
|
reset_case
|
||||||
|
TCC_TEST_MACOS_VERSION=invalid
|
||||||
|
expect_failure 'Unexpected macOS version'
|
||||||
|
|
||||||
|
echo 'TCC database tests passed'
|
||||||
@@ -12,8 +12,59 @@ source ~/.zprofile
|
|||||||
#
|
#
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
resolve_user_tcc_database() {
|
||||||
|
local macos_version macos_major user_id open_files line candidate database=""
|
||||||
|
macos_version="$(sw_vers -productVersion)"
|
||||||
|
macos_major="${macos_version%%.*}"
|
||||||
|
case "$macos_major" in
|
||||||
|
''|*[!0-9]*) echo "Unexpected macOS version: $macos_version" >&2; return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "$macos_major" -lt 27 ]]; then
|
||||||
|
database="${HOME}/Library/Application Support/com.apple.TCC/TCC.db"
|
||||||
|
else
|
||||||
|
# macOS 27 moved the user database into a per-user ProtectedSystem
|
||||||
|
# container. Inspect the daemon's open files to avoid using a stale copy.
|
||||||
|
# https://developer.apple.com/documentation/macos-release-notes/macos-27-release-notes#TCC
|
||||||
|
user_id="$(id -u)"
|
||||||
|
if ! open_files="$(sudo lsof -a -u "$user_id" -c tccd -Fn)"; then
|
||||||
|
echo "Unable to inspect the user TCC daemon for UID $user_id" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
while IFS= read -r line; do
|
||||||
|
case "$line" in
|
||||||
|
n/private/var/containers/Data/ProtectedSystem/*/Data/Library/Application\ Support/com.apple.TCC/TCC.db)
|
||||||
|
candidate="${line#n}"
|
||||||
|
if [[ -n "$database" && "$database" != "$candidate" ]]; then
|
||||||
|
echo "Found multiple active user TCC databases for UID $user_id" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
database="$candidate"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done <<< "$open_files"
|
||||||
|
if [[ -z "$database" ]]; then
|
||||||
|
echo "Unable to find the active user TCC database for UID $user_id" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! sudo test -f "$database"; then
|
||||||
|
echo "User TCC database does not exist: $database" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [[ "$macos_major" -ge 27 && "$(sudo stat -f %u "$database")" != "$user_id" ]]; then
|
||||||
|
echo "Unexpected owner for user TCC database: $database" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$database"
|
||||||
|
}
|
||||||
|
|
||||||
update_tcc_database() {
|
update_tcc_database() {
|
||||||
sudo sqlite3 "$1" <<-'EOF'
|
local tart_guest_agent_path
|
||||||
|
tart_guest_agent_path="$(realpath /opt/homebrew/bin/tart-guest-agent)"
|
||||||
|
|
||||||
|
sudo sqlite3 "$1" <<-EOF
|
||||||
INSERT OR REPLACE
|
INSERT OR REPLACE
|
||||||
INTO access (
|
INTO access (
|
||||||
service,
|
service,
|
||||||
@@ -36,12 +87,26 @@ update_tcc_database() {
|
|||||||
('kTCCServiceScreenCapture', 1, '/usr/bin/osascript', 2, 0, 1, NULL, 'UNUSED'),
|
('kTCCServiceScreenCapture', 1, '/usr/bin/osascript', 2, 0, 1, NULL, 'UNUSED'),
|
||||||
('kTCCServicePostEvent', 1, '/usr/bin/osascript', 2, 0, 1, NULL, 'UNUSED'),
|
('kTCCServicePostEvent', 1, '/usr/bin/osascript', 2, 0, 1, NULL, 'UNUSED'),
|
||||||
('kTCCServiceAppleEvents', 1, '/usr/bin/osascript', 2, 0, 1, 0, 'com.apple.systemevents'),
|
('kTCCServiceAppleEvents', 1, '/usr/bin/osascript', 2, 0, 1, 0, 'com.apple.systemevents'),
|
||||||
('kTCCServiceAppleEvents', 1, '/usr/bin/osascript', 2, 0, 1, 0, 'com.apple.Safari');
|
('kTCCServiceAppleEvents', 1, '/usr/bin/osascript', 2, 0, 1, 0, 'com.apple.Safari'),
|
||||||
|
-- Direct Python invocation
|
||||||
|
('kTCCServiceAccessibility', 0, 'org.python.python', 2, 0, 1, NULL, 'UNUSED'),
|
||||||
|
('kTCCServiceScreenCapture', 0, 'org.python.python', 2, 0, 1, NULL, 'UNUSED'),
|
||||||
|
('kTCCServiceMicrophone', 0, 'org.python.python', 2, 0, 1, NULL, 'UNUSED'),
|
||||||
|
('kTCCServicePostEvent', 0, 'org.python.python', 2, 0, 1, NULL, 'UNUSED'),
|
||||||
|
-- Commands invoked through the Tart Guest Agent
|
||||||
|
('kTCCServiceAccessibility', 1, '${tart_guest_agent_path}', 2, 0, 1, NULL, 'UNUSED'),
|
||||||
|
('kTCCServiceScreenCapture', 1, '${tart_guest_agent_path}', 2, 0, 1, NULL, 'UNUSED'),
|
||||||
|
('kTCCServiceMicrophone', 1, '${tart_guest_agent_path}', 2, 0, 1, NULL, 'UNUSED'),
|
||||||
|
('kTCCServicePostEvent', 1, '${tart_guest_agent_path}', 2, 0, 1, NULL, 'UNUSED');
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Resolve the user database before making either update. Never create an empty
|
||||||
|
# database at an obsolete path or silently omit user-level grants.
|
||||||
|
user_tcc_database="$(resolve_user_tcc_database)"
|
||||||
|
|
||||||
# Update TCC.db for all users
|
# Update TCC.db for all users
|
||||||
update_tcc_database "/Library/Application Support/com.apple.TCC/TCC.db"
|
update_tcc_database "/Library/Application Support/com.apple.TCC/TCC.db"
|
||||||
|
|
||||||
# Update TCC.db for the current user
|
# Update TCC.db for the current user
|
||||||
update_tcc_database "${HOME}/Library/Application Support/com.apple.TCC/TCC.db"
|
update_tcc_database "$user_tcc_database"
|
||||||
|
|||||||
+23
-3
@@ -64,7 +64,7 @@ build {
|
|||||||
"brew --version",
|
"brew --version",
|
||||||
"brew update",
|
"brew update",
|
||||||
"brew install wget unzip zip ca-certificates cmake gcc git-lfs jq yq gh gitlab-runner",
|
"brew install wget unzip zip ca-certificates cmake gcc git-lfs jq yq gh gitlab-runner",
|
||||||
"brew install buildkite/buildkite/buildkite-agent",
|
"brew install buildkite/buildkite/buildkite-agent@3",
|
||||||
"brew install equinix-labs/otel-cli/otel-cli",
|
"brew install equinix-labs/otel-cli/otel-cli",
|
||||||
"brew install curl || true", // doesn't work on Monterey
|
"brew install curl || true", // doesn't work on Monterey
|
||||||
"brew install --cask git-credential-manager",
|
"brew install --cask git-credential-manager",
|
||||||
@@ -121,8 +121,12 @@ build {
|
|||||||
"echo 'export PATH=\"/opt/homebrew/opt/node@24/bin:$PATH\"' >> ~/.zprofile",
|
"echo 'export PATH=\"/opt/homebrew/opt/node@24/bin:$PATH\"' >> ~/.zprofile",
|
||||||
"source ~/.zprofile",
|
"source ~/.zprofile",
|
||||||
"node --version",
|
"node --version",
|
||||||
"npm install --global yarn",
|
"npm install --global yarn pnpm",
|
||||||
|
"echo 'export PNPM_HOME=\"$HOME/Library/pnpm\"' >> ~/.zprofile",
|
||||||
|
"echo 'export PATH=\"$PNPM_HOME:$PATH\"' >> ~/.zprofile",
|
||||||
|
"source ~/.zprofile",
|
||||||
"yarn --version",
|
"yarn --version",
|
||||||
|
"pnpm --version",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
provisioner "shell" {
|
provisioner "shell" {
|
||||||
@@ -151,6 +155,22 @@ build {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Install the process-scoped Metal shim for opt-in workloads.
|
||||||
|
provisioner "shell" {
|
||||||
|
inline = ["mkdir -p ~/tart-metal-capabilities-src"]
|
||||||
|
}
|
||||||
|
provisioner "file" {
|
||||||
|
source = "data/tart-metal-capabilities/"
|
||||||
|
destination = "~/tart-metal-capabilities-src/"
|
||||||
|
}
|
||||||
|
provisioner "shell" {
|
||||||
|
environment_vars = ["TART_METAL_SOURCE_DIR=/Users/admin/tart-metal-capabilities-src"]
|
||||||
|
script = "scripts/install-tart-metal-capabilities.sh"
|
||||||
|
}
|
||||||
|
provisioner "shell" {
|
||||||
|
inline = ["rm -rf ~/tart-metal-capabilities-src"]
|
||||||
|
}
|
||||||
|
|
||||||
// Guest agent for Tart VMs
|
// Guest agent for Tart VMs
|
||||||
provisioner "file" {
|
provisioner "file" {
|
||||||
source = "data/tart-guest-daemon.plist"
|
source = "data/tart-guest-daemon.plist"
|
||||||
@@ -164,7 +184,7 @@ build {
|
|||||||
inline = [
|
inline = [
|
||||||
# Install Tart Guest Agent
|
# Install Tart Guest Agent
|
||||||
"source ~/.zprofile",
|
"source ~/.zprofile",
|
||||||
"brew install cirruslabs/cli/tart-guest-agent",
|
"brew install openai/tools/tart-guest-agent",
|
||||||
|
|
||||||
# Install daemon variant of the Tart Guest Agent
|
# Install daemon variant of the Tart Guest Agent
|
||||||
"sudo mv ~/tart-guest-daemon.plist /Library/LaunchDaemons/org.cirruslabs.tart-guest-daemon.plist",
|
"sudo mv ~/tart-guest-daemon.plist /Library/LaunchDaemons/org.cirruslabs.tart-guest-daemon.plist",
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
packer {
|
||||||
|
required_plugins {
|
||||||
|
tart = {
|
||||||
|
version = ">= 1.16.0"
|
||||||
|
source = "github.com/cirruslabs/tart"
|
||||||
|
}
|
||||||
|
ansible = {
|
||||||
|
version = "~> 1"
|
||||||
|
source = "github.com/hashicorp/ansible"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
source "tart-cli" "tart" {
|
||||||
|
from_ipsw = "https://updates.cdn-apple.com/2026FallFCS/afcfc88e-bbe6-44bf-a5da-07c56eebc06c/UniversalMac_27.0_26A428_Restore.ipsw"
|
||||||
|
vm_name = "golden-gate-vanilla"
|
||||||
|
cpu_count = 4
|
||||||
|
memory_gb = 8
|
||||||
|
disk_size_gb = 50
|
||||||
|
disk_format = "asif"
|
||||||
|
ssh_password = "admin"
|
||||||
|
ssh_username = "admin"
|
||||||
|
ssh_timeout = "180s"
|
||||||
|
// Requires Tart 2.33.0+ and macOS 27+ on both the host and guest VM
|
||||||
|
run_extra_args = [
|
||||||
|
"--provisioning-opts=${join(",", [
|
||||||
|
"fullName=Managed via Tart",
|
||||||
|
"username=admin",
|
||||||
|
"password=admin",
|
||||||
|
"logsInAutomatically=true",
|
||||||
|
"enablesRemoteLogin=true",
|
||||||
|
])}",
|
||||||
|
]
|
||||||
|
boot_command = [
|
||||||
|
# Wait for first-boot provisioning to finish automatic login
|
||||||
|
"<wait120s>",
|
||||||
|
# Enable Keyboard navigation
|
||||||
|
# This is so that we can navigate the System Settings app using the keyboard
|
||||||
|
"<wait10s><leftAltOn><spacebar><leftAltOff>Terminal<wait10s><enter>",
|
||||||
|
"<wait10s><wait10s>defaults write NSGlobalDomain AppleKeyboardUIMode -int 3<enter>",
|
||||||
|
# Disable Gatekeeper (1/2)
|
||||||
|
"<wait10s>sudo spctl --global-disable<enter>",
|
||||||
|
"<wait10s>admin<enter>",
|
||||||
|
# Disable Gatekeeper (2/2)
|
||||||
|
# On Tahoe opening System Settings through Spotlight is not very reliable, sometimes opens System information
|
||||||
|
"<wait10s>open '/System/Applications/System Settings.app'<enter>",
|
||||||
|
# Wait for System Settings to fully open before navigating with the keyboard
|
||||||
|
"<wait120s>",
|
||||||
|
"<wait10s><leftCtrlOn><f2><leftCtrlOff><right><right><right><down>Privacy & Security<enter>",
|
||||||
|
"<wait10s><leftShiftOn><tab><tab><tab><tab><tab><tab><leftShiftOff>",
|
||||||
|
"<wait10s><down><wait1s><down><wait1s><enter>",
|
||||||
|
"<wait10s>admin<enter>",
|
||||||
|
"<wait10s><leftShiftOn><tab><leftShiftOff><wait1s><spacebar>",
|
||||||
|
# Quit System Settings
|
||||||
|
"<wait10s><leftAltOn>q<leftAltOff>",
|
||||||
|
# Refocus Terminal after quitting System Settings
|
||||||
|
"<wait10s><leftAltOn><spacebar><leftAltOff>Terminal<wait10s><enter>",
|
||||||
|
# Sharing is no longer listed in the View menu on Golden Gate
|
||||||
|
"<wait10s>open 'x-apple.systempreferences:com.apple.Sharing-Settings.extension'<enter>",
|
||||||
|
# Enable Screen Sharing through the UI to grant the required TCC permissions
|
||||||
|
"<wait10s><tab><tab><tab><tab><tab><tab><spacebar>",
|
||||||
|
# Type in the password to allow enabling Screen Sharing
|
||||||
|
"<wait10s>admin<enter>",
|
||||||
|
# Quit System Settings
|
||||||
|
"<wait10s><leftAltOn>q<leftAltOff>",
|
||||||
|
]
|
||||||
|
|
||||||
|
// A (hopefully) temporary workaround for Virtualization.Framework's
|
||||||
|
// installation process not fully finishing in a timely manner
|
||||||
|
create_grace_time = "30s"
|
||||||
|
|
||||||
|
// Keep the recovery partition, otherwise it's not possible to "softwareupdate"
|
||||||
|
recovery_partition = "keep"
|
||||||
|
}
|
||||||
|
|
||||||
|
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\"",
|
||||||
|
// Use the same timezone as the previous Setup Assistant flow
|
||||||
|
"sudo systemsetup -settimezone GMT 2>/dev/null",
|
||||||
|
// 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 -setsleep Off 2>/dev/null",
|
||||||
|
// Launch Safari to populate the defaults
|
||||||
|
"/Applications/Safari.app/Contents/MacOS/Safari &",
|
||||||
|
"SAFARI_PID=$!",
|
||||||
|
"disown",
|
||||||
|
"sleep 30",
|
||||||
|
"kill -9 $SAFARI_PID",
|
||||||
|
// Enable Safari's remote automation
|
||||||
|
"sudo safaridriver --enable",
|
||||||
|
// 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",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "shell" {
|
||||||
|
inline = [
|
||||||
|
# Ensure that Gatekeeper is disabled
|
||||||
|
"spctl --status | grep -q 'assessments disabled'",
|
||||||
|
# Ensure that Screen Sharing was enabled
|
||||||
|
"sudo launchctl print system/com.apple.screensharing > /dev/null",
|
||||||
|
# Ensure that FileVault remains disabled by default
|
||||||
|
"sudo fdesetup status | grep -q 'FileVault is Off'",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,13 @@ packer {
|
|||||||
}
|
}
|
||||||
ansible = {
|
ansible = {
|
||||||
version = "~> 1"
|
version = "~> 1"
|
||||||
source = "github.com/hashicorp/ansible"
|
source = "github.com/hashicorp/ansible"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
source "tart-cli" "tart" {
|
source "tart-cli" "tart" {
|
||||||
from_ipsw = "https://updates.cdn-apple.com/2026SpringFCS/fullrestores/122-58869/DFB1CEEF-5619-4591-9924-E20DB2C8FED0/UniversalMac_26.5_25F71_Restore.ipsw"
|
from_ipsw = "https://updates.cdn-apple.com/2026SummerFCS/fullrestores/140-75212/A2A24B94-1FC1-45A3-93F7-C51B02AF1F4D/UniversalMac_26.6.2_25G83_Restore.ipsw"
|
||||||
vm_name = "tahoe-vanilla"
|
vm_name = "tahoe-vanilla"
|
||||||
cpu_count = 4
|
cpu_count = 4
|
||||||
memory_gb = 8
|
memory_gb = 8
|
||||||
|
|||||||
+39
-31
@@ -16,39 +16,39 @@ variable "xcode_version" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
variable "additional_ios_builds" {
|
variable "additional_ios_builds" {
|
||||||
type = list(string)
|
type = list(string)
|
||||||
default = []
|
default = []
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "additional_tvos_builds" {
|
variable "additional_tvos_builds" {
|
||||||
type = list(string)
|
type = list(string)
|
||||||
default = []
|
default = []
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "xcode_components" {
|
variable "xcode_components" {
|
||||||
type = list(string)
|
type = list(string)
|
||||||
default = []
|
default = []
|
||||||
description = "Additional Xcode components to download."
|
description = "Additional Xcode components to download."
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "expected_runtimes_file" {
|
variable "expected_runtimes_file" {
|
||||||
type = string
|
type = string
|
||||||
default = ""
|
default = ""
|
||||||
description = "Path to file containing expected simulator runtimes. If empty, runtime verification is skipped."
|
description = "Path to file containing expected simulator runtimes. If empty, runtime verification is skipped."
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "tag" {
|
variable "tag" {
|
||||||
type = string
|
type = string
|
||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "disk_size" {
|
variable "disk_size" {
|
||||||
type = number
|
type = number
|
||||||
default = 140
|
default = 140
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "disk_free_mb" {
|
variable "disk_free_mb" {
|
||||||
type = number
|
type = number
|
||||||
default = 15000
|
default = 15000
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +58,12 @@ variable "android_sdk_tools_version" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
source "tart-cli" "tart" {
|
source "tart-cli" "tart" {
|
||||||
vm_base_name = "ghcr.io/cirruslabs/macos-${var.macos_version}-base:latest"
|
// The workflow sizes the VM before boot; only the guest daemon resizes APFS.
|
||||||
|
recovery_partition = "keep"
|
||||||
// use tag or the last element of the xcode_version list
|
// use tag or the last element of the xcode_version list
|
||||||
vm_name = "${var.macos_version}-xcode:${var.tag != "" ? var.tag : var.xcode_version[0]}"
|
vm_name = "${var.macos_version}-xcode:${var.tag != "" ? var.tag : var.xcode_version[0]}"
|
||||||
cpu_count = 4
|
cpu_count = 4
|
||||||
memory_gb = 8
|
memory_gb = 8
|
||||||
disk_size_gb = var.disk_size
|
|
||||||
headless = true
|
headless = true
|
||||||
ssh_password = "admin"
|
ssh_password = "admin"
|
||||||
ssh_username = "admin"
|
ssh_username = "admin"
|
||||||
@@ -95,7 +95,15 @@ build {
|
|||||||
sources = ["source.tart-cli.tart"]
|
sources = ["source.tart-cli.tart"]
|
||||||
|
|
||||||
provisioner "shell" {
|
provisioner "shell" {
|
||||||
|
// Allow 6 GB for boot/recovery partitions and metadata; Tart uses decimal GB.
|
||||||
inline = [
|
inline = [
|
||||||
|
"minimum_kb=${floor((var.disk_size - 6) * 1000000000 / 1024)}",
|
||||||
|
"for ((attempt = 0; attempt < 60; attempt++)); do",
|
||||||
|
" size_kb=$(df -k /System/Volumes/Data | awk 'NR == 2 {print $2}')",
|
||||||
|
" [[ $size_kb -ge $minimum_kb ]] && break",
|
||||||
|
" sleep 2",
|
||||||
|
"done",
|
||||||
|
"[[ $size_kb -ge $minimum_kb ]] || { echo 'APFS did not expand to ${var.disk_size} GB within 120 seconds.' >&2; exit 1; }",
|
||||||
"source ~/.zprofile",
|
"source ~/.zprofile",
|
||||||
"brew --version",
|
"brew --version",
|
||||||
"brew update",
|
"brew update",
|
||||||
@@ -146,7 +154,7 @@ build {
|
|||||||
}
|
}
|
||||||
|
|
||||||
provisioner "file" {
|
provisioner "file" {
|
||||||
sources = [ for version in var.xcode_version : pathexpand("~/XcodesCache/Xcode_${version}.xip")]
|
sources = [for version in var.xcode_version : pathexpand("~/XcodesCache/Xcode_${version}.xip")]
|
||||||
destination = "/Users/admin/Downloads/"
|
destination = "/Users/admin/Downloads/"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +169,7 @@ build {
|
|||||||
// select the latest one as the default
|
// select the latest one as the default
|
||||||
dynamic "provisioner" {
|
dynamic "provisioner" {
|
||||||
for_each = local.xcode_install_provisioners
|
for_each = local.xcode_install_provisioners
|
||||||
labels = ["shell"]
|
labels = ["shell"]
|
||||||
content {
|
content {
|
||||||
inline = provisioner.value.inline
|
inline = provisioner.value.inline
|
||||||
}
|
}
|
||||||
@@ -169,11 +177,11 @@ build {
|
|||||||
|
|
||||||
dynamic "provisioner" {
|
dynamic "provisioner" {
|
||||||
for_each = length(var.xcode_version) > 2 ? [2] : []
|
for_each = length(var.xcode_version) > 2 ? [2] : []
|
||||||
labels = ["shell"]
|
labels = ["shell"]
|
||||||
content {
|
content {
|
||||||
inline = [
|
inline = [
|
||||||
"source ~/.zprofile",
|
"source ~/.zprofile",
|
||||||
"sudo xcodes select '${var.xcode_version[2]}'",
|
"sudo xcode-select -s /Applications/Xcode_${var.xcode_version[2]}.app/Contents/Developer",
|
||||||
"xcodebuild -downloadAllPlatforms",
|
"xcodebuild -downloadAllPlatforms",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -181,24 +189,17 @@ build {
|
|||||||
|
|
||||||
dynamic "provisioner" {
|
dynamic "provisioner" {
|
||||||
for_each = length(var.xcode_version) > 1 ? [1] : []
|
for_each = length(var.xcode_version) > 1 ? [1] : []
|
||||||
labels = ["shell"]
|
labels = ["shell"]
|
||||||
content {
|
content {
|
||||||
inline = [
|
inline = [
|
||||||
"source ~/.zprofile",
|
"source ~/.zprofile",
|
||||||
"sudo xcodes select '${var.xcode_version[1]}'",
|
"sudo xcode-select -s /Applications/Xcode_${var.xcode_version[1]}.app/Contents/Developer",
|
||||||
"xcodebuild -downloadAllPlatforms",
|
"xcodebuild -downloadAllPlatforms",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provisioner "shell" {
|
// Download legacy iOS runtimes before selecting the newest Xcode.
|
||||||
inline = [
|
|
||||||
"source ~/.zprofile",
|
|
||||||
"sudo xcodes select '${var.xcode_version[0]}'",
|
|
||||||
"xcodebuild -downloadAllPlatforms",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
provisioner "shell" {
|
provisioner "shell" {
|
||||||
inline = concat(
|
inline = concat(
|
||||||
["source ~/.zprofile"],
|
["source ~/.zprofile"],
|
||||||
@@ -208,6 +209,14 @@ build {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provisioner "shell" {
|
||||||
|
inline = [
|
||||||
|
"source ~/.zprofile",
|
||||||
|
"sudo xcode-select -s /Applications/Xcode_${var.xcode_version[0]}.app/Contents/Developer",
|
||||||
|
"xcodebuild -downloadAllPlatforms",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
provisioner "shell" {
|
provisioner "shell" {
|
||||||
inline = concat(
|
inline = concat(
|
||||||
["source ~/.zprofile"],
|
["source ~/.zprofile"],
|
||||||
@@ -232,10 +241,9 @@ build {
|
|||||||
"brew install libimobiledevice ideviceinstaller ios-deploy carthage",
|
"brew install libimobiledevice ideviceinstaller ios-deploy carthage",
|
||||||
"brew install xcbeautify swiftformat swiftlint swiftgen licenseplist",
|
"brew install xcbeautify swiftformat swiftlint swiftgen licenseplist",
|
||||||
"brew install mint",
|
"brew install mint",
|
||||||
"brew tap tuist/tuist",
|
"git clone --depth 1 https://github.com/tuist/homebrew-tuist.git \"$(brew --repository)/Library/Taps/tuist/homebrew-tuist\"",
|
||||||
"brew install --formula tuist",
|
"rm -rf \"$(brew --repository)/Library/Taps/tuist/homebrew-tuist/Casks\"",
|
||||||
"rbenv install 3.3.10",
|
"tuist_version=$(ruby -ne 'if $_ =~ %r{/download/([^/]+)/}; puts $1; exit; end' \"$(brew --repository)/Library/Taps/tuist/homebrew-tuist/Aliases/tuist\") && brew trust --formula \"tuist/tuist/tuist@$tuist_version\" && brew install --formula \"tuist/tuist/tuist@$tuist_version\"",
|
||||||
"rbenv global 3.3.10", # fastlane conflicts with 3.4.0+ https://github.com/fastlane/fastlane/issues/29527
|
|
||||||
"gem update",
|
"gem update",
|
||||||
"gem install fastlane",
|
"gem install fastlane",
|
||||||
"gem install cocoapods",
|
"gem install cocoapods",
|
||||||
@@ -247,7 +255,7 @@ build {
|
|||||||
// Copy expected runtimes file if provided
|
// Copy expected runtimes file if provided
|
||||||
dynamic "provisioner" {
|
dynamic "provisioner" {
|
||||||
for_each = var.expected_runtimes_file != "" ? [1] : []
|
for_each = var.expected_runtimes_file != "" ? [1] : []
|
||||||
labels = ["file"]
|
labels = ["file"]
|
||||||
content {
|
content {
|
||||||
source = var.expected_runtimes_file
|
source = var.expected_runtimes_file
|
||||||
destination = "/Users/admin/runtimes.expected.txt"
|
destination = "/Users/admin/runtimes.expected.txt"
|
||||||
@@ -257,7 +265,7 @@ build {
|
|||||||
// Verify simulator runtimes match expected list if file was provided
|
// Verify simulator runtimes match expected list if file was provided
|
||||||
dynamic "provisioner" {
|
dynamic "provisioner" {
|
||||||
for_each = var.expected_runtimes_file != "" ? [1] : []
|
for_each = var.expected_runtimes_file != "" ? [1] : []
|
||||||
labels = ["shell"]
|
labels = ["shell"]
|
||||||
content {
|
content {
|
||||||
inline = [
|
inline = [
|
||||||
"source ~/.zprofile",
|
"source ~/.zprofile",
|
||||||
|
|||||||
Reference in New Issue
Block a user