Compare commits

..
Author SHA1 Message Date
Yibo Zhuang 2ff087f9f0 Download legacy iOS runtimes before selecting latest Xcode (#384) 2026-09-21 23:00:39 -07:00
Yibo Zhuang 6570628f94 Focus Terminal before enabling Golden Gate Screen Sharing (#383) 2026-09-21 14:57:48 -07:00
Yibo Zhuang 03a555c22a Fix Golden Gate Settings navigation (#382) 2026-09-21 13:49:47 -07:00
Yibo Zhuang 3c620b40ea Enable Golden Gate Screen Sharing through System Settings (#380) 2026-09-21 12:32:37 -07:00
Yibo Zhuang d851dd5a76 Fix Xcode 27 runtime ordering in Tahoe images (#381) 2026-09-21 12:29:38 -07:00
Yibo Zhuang be4a8e907b Add public Golden Gate Xcode 27 images (#379) 2026-09-21 11:49:34 -07:00
Yibo Zhuang addef08637 Merge pull request #378 from yzhuang-oai/dev/yzhuang/xcode-27-images
Add Xcode 27 to Tahoe images
2026-09-21 11:49:12 -07:00
Yibo Zhuang 106c086ffa Update Tahoe images to Xcode 27 beta 6 (#374) 2026-08-28 16:26:13 -04:00
Fedor Kororkov 795a125e78 Include Metal Capabilities Helper (#372)
* Add Tart Metal capabilities to base images

* Simplify Metal capability usage documentation

* Make Tart Metal capabilities opt-in

* Fix Metal shim architecture checks on Xcode 27

* Support protected TCC databases on macOS 27
2026-08-19 12:48:52 -04:00
Fedor Kororkov 4849509b2d Update Tahoe and Golden Gate IPSWs (#373) 2026-08-18 10:37:02 -04:00
Fedor Kororkov 210e02922f Add dedicated macOS base image publishing workflow (#371) 2026-08-16 19:49:59 -04:00
edi-oai d6ee41d6d5 Install Tart Guest Agent from OpenAI tap (#370) 2026-08-14 22:34:20 -04:00
Fedor Kororkov 741758b9ba Use ASIF for Golden Gate vanilla image (#369) 2026-08-14 14:30:54 -04:00
Jiawen GengandCursor Agent f03344599b Pre-install pnpm alongside yarn in base image (#357)
* Pre-install pnpm alongside yarn in base image

Install pnpm globally in the Node.js provisioner so base images
include both common package managers.

Assisted-by: Cursor Agent

Co-authored-by: Jiawen Geng <technicalcute@gmail.com>

* Install yarn and pnpm in a single npm command

Assisted-by: Cursor Agent

Co-authored-by: Jiawen Geng <technicalcute@gmail.com>

* Configure PNPM_HOME in the base image shell profile

Assisted-by: Cursor Agent

Co-authored-by: Jiawen Geng <technicalcute@gmail.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
2026-08-14 14:02:34 -04:00
Nikolay Edigaryev 16e9ad7d00 Pass "--provisioning-opts" to "tart run" to simplify installation 2026-08-13 15:04:29 +01:00
Nikolay Edigaryev 85b77e41bc Produce macOS Golden Gate vanilla image 2026-08-13 15:04:29 +01:00
Fedor Kororkov 9be4b9dc86 Fix base image automation microphone permissions (#367) 2026-08-11 21:45:23 -04:00
Fedor Kororkov 46050816d8 Update release.yml 2026-08-07 19:55:39 -04:00
Fedor Kororkov d358da6f50 Use base image Ruby for Xcode builds (#366) 2026-08-07 19:54:31 -04:00
Fedor Kororkov b4926493fb Update Ruby for Xcode 27 image builds (#365) 2026-08-07 14:24:09 -04:00
21 changed files with 1051 additions and 118 deletions
+119
View File
@@ -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
+11 -1
View File
@@ -58,6 +58,8 @@ jobs:
max-parallel: 1
matrix:
include:
- macos_version: golden-gate
disable_sip_template: disable-sip-with-username.pkr.hcl
- macos_version: sonoma
disable_sip_template: disable-sip.pkr.hcl
- macos_version: sequoia
@@ -115,9 +117,13 @@ jobs:
matrix:
include:
- macos_version: tahoe
xcode_version: "26.2"
xcode_version: "27"
xcode_components: '"MetalToolchain"'
latest: true
- macos_version: tahoe
xcode_version: "26.2"
xcode_components: '"MetalToolchain"'
latest: false
- macos_version: tahoe
xcode_version: "26.1.1"
xcode_components: '"MetalToolchain"'
@@ -158,6 +164,10 @@ jobs:
xcode_version: "15.4"
xcode_components: ""
latest: false
- macos_version: golden-gate
xcode_version: "27"
xcode_components: '"MetalToolchain"'
latest: true
env:
MACOS_VERSION: ${{ matrix.macos_version }}
XCODE_COMPONENTS: ${{ matrix.xcode_components }}
+12 -4
View File
@@ -34,9 +34,11 @@ jobs:
fail-fast: false
max-parallel: 1
matrix:
macos_version:
- tahoe
- sequoia
# Xcode 27 requires macOS Tahoe 26.6 or later.
macos_version: >-
${{ startsWith(github.event.release.tag_name || inputs.xcode_version, '27')
&& fromJSON('["tahoe", "golden-gate"]')
|| fromJSON('["tahoe", "sequoia"]') }}
env:
MACOS_VERSION: ${{ matrix.macos_version }}
XCODE_COMPONENTS: '"MetalToolchain"'
@@ -98,6 +100,12 @@ jobs:
max-parallel: 1
matrix:
include:
- macos_version: golden-gate
xcode_versions: '"27"'
additional_ios_builds: ""
additional_tvos_builds: ""
xcode_components: '"MetalToolchain"'
disk_size: 240
- 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"
@@ -105,7 +113,7 @@ jobs:
xcode_components: '"MetalToolchain"'
disk_size: 380
- macos_version: tahoe
xcode_versions: '"26.6","27-beta-2","26.5","27-beta","26.4.1","26.3","26.2","26.1.1","26.0.1"'
xcode_versions: '"27","26.6","27-beta-6","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"'
+44 -4
View File
@@ -3,6 +3,7 @@ name: Template Builds
on:
pull_request:
paths:
- ".github/workflows/base.yml"
- ".github/workflows/monthly.yml"
- ".github/workflows/release.yml"
- ".github/workflows/template-validation.yml"
@@ -45,10 +46,22 @@ jobs:
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"
- name: Validate templates
run: |
@@ -66,6 +79,12 @@ jobs:
validate "$template"
done
validate templates/xcode.pkr.hcl \
-var macos_version=golden-gate \
-var 'xcode_version=["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
@@ -89,6 +108,16 @@ jobs:
-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"]'
build-vanilla:
name: Build Vanilla Image (${{ matrix.macos_version }})
needs: packer-validate
@@ -165,6 +194,8 @@ jobs:
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 }}
@@ -186,7 +217,7 @@ jobs:
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
build=false
if grep -Eq '^(templates/base\.pkr\.hcl|templates/disable-sip.*\.pkr\.hcl|data/(github_known_hosts|limit\.maxfiles\.plist|setup-info-template\.json|tart-guest-.*\.plist)|scripts/(install-actions-runner|update-tcc-database)\.sh|ansible/)' changed-files.txt; then
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
@@ -233,7 +264,7 @@ jobs:
matrix:
include:
- macos_version: tahoe
xcode_versions: '"26.6","27-beta-2","26.5","27-beta","26.4.1","26.3","26.2","26.1.1","26.0.1"'
xcode_versions: '"27","26.6","27-beta-6","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"'
@@ -244,6 +275,12 @@ jobs:
additional_tvos_builds: "17.5"
xcode_components: '"MetalToolchain"'
disk_size: 380
- macos_version: golden-gate
xcode_versions: '"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 }}
@@ -329,8 +366,11 @@ jobs:
candidate=""
case "$version" in
27-beta-2)
candidate="$HOME/XcodesCache/Xcode_27_beta_2.xip"
27)
candidate="$HOME/XcodesCache/Xcode_27.0.xip"
;;
27-beta-6)
candidate="$HOME/XcodesCache/Xcode_27_beta_6.xip"
;;
27-beta)
candidate="$HOME/XcodesCache/Xcode_27_beta.xip"
+33 -4
View File
@@ -6,16 +6,45 @@ GitHub Actions runners, [Cirrus Runners](https://cirrus-runners.app/) or [any ot
The following image variants are currently available:
* `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-{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-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-{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-{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:{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-).
## 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
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.
## Update Cadence
+5
View File
@@ -0,0 +1,5 @@
== Runtimes ==
iOS 27.0 (27.0 - 24A434) - com.apple.CoreSimulator.SimRuntime.iOS-27-0
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
+8 -4
View File
@@ -6,10 +6,14 @@ 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.5 (26.5 - 23F77) - com.apple.CoreSimulator.SimRuntime.iOS-26-5
iOS 27.0 (27.0 - 24A5355p) - com.apple.CoreSimulator.SimRuntime.iOS-27-0
iOS 27.0 (27.0 - 24A5370g) - com.apple.CoreSimulator.SimRuntime.iOS-27-0
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
tvOS 26.5 (26.5 - 23L470) - com.apple.CoreSimulator.SimRuntime.tvOS-26-5
tvOS 27.0 (27.0 - 24J5305f) - com.apple.CoreSimulator.SimRuntime.tvOS-27-0
tvOS 27.0 (27.0 - 24J5356a) - com.apple.CoreSimulator.SimRuntime.tvOS-27-0
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 27.0 (27.0 - 24R5305f) - com.apple.CoreSimulator.SimRuntime.watchOS-27-0
watchOS 27.0 (27.0 - 24R5355a) - com.apple.CoreSimulator.SimRuntime.watchOS-27-0
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 27.0 (27.0 - 24M5306g) - com.apple.CoreSimulator.SimRuntime.xrOS-27-0
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
+21
View File
@@ -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.
+31
View File
@@ -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"
+60
View File
@@ -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
+170
View File
@@ -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'
+62 -5
View File
@@ -12,8 +12,59 @@ source ~/.zprofile
#
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() {
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
INTO access (
service,
@@ -40,16 +91,22 @@ update_tcc_database() {
-- 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, '/opt/homebrew/bin/tart-guest-agent', 2, 0, 1, NULL, 'UNUSED'),
('kTCCServiceScreenCapture', 1, '/opt/homebrew/bin/tart-guest-agent', 2, 0, 1, NULL, 'UNUSED'),
('kTCCServicePostEvent', 1, '/opt/homebrew/bin/tart-guest-agent', 2, 0, 1, NULL, 'UNUSED');
('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
}
# 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_database "/Library/Application Support/com.apple.TCC/TCC.db"
# 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"
+22 -2
View File
@@ -121,8 +121,12 @@ build {
"echo 'export PATH=\"/opt/homebrew/opt/node@24/bin:$PATH\"' >> ~/.zprofile",
"source ~/.zprofile",
"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",
"pnpm --version",
]
}
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
provisioner "file" {
source = "data/tart-guest-daemon.plist"
@@ -164,7 +184,7 @@ build {
inline = [
# Install Tart Guest Agent
"source ~/.zprofile",
"brew install cirruslabs/cli/tart-guest-agent",
"brew install openai/tools/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",
+33 -83
View File
@@ -12,101 +12,40 @@ packer {
}
source "tart-cli" "tart" {
from_ipsw = "https://updates.cdn-apple.com/2026SummerSeed/fullrestores/140-55718/5809AFC6-1923-4590-AAFC-904A0283E659/UniversalMac_27.0_26A5388g_Restore.ipsw"
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 = [
# hello, hola, bonjour, etc.
"<wait60s><spacebar>",
# Language: most of the times we have a list of "English"[1], "English (UK)", etc. with
# "English" language already selected. If we type "english", it'll cause us to switch
# to the "English (UK)", which is not what we want. To solve this, we switch to some other
# language first, e.g. "Italiano" and then switch back to "English". We'll then jump to the
# first entry in a list of "english"-prefixed items, which will be "English".
#
# [1]: should be named "English (US)", but oh well 🤷
"<wait30s>italiano<esc>english<enter>",
# Select Your Country or Region
"<wait60s><click 'Select Your Country or Region'><wait5s>united states<leftShiftOn><tab><leftShiftOff><spacebar>",
# Transfer Your Data to This Mac
"<wait30s><tab><tab><tab><spacebar><tab><tab><spacebar>",
# Written and Spoken Languages
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Accessibility
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Data & Privacy
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Create a Mac Account
"<wait10s><tab><tab><tab><tab><tab><tab>Managed via Tart<tab>admin<tab>admin<tab>admin<tab><tab><tab><spacebar>",
# Enable Voice Over
"<wait120s><leftAltOn><f5><leftAltOff>",
# Sign In to Your Apple Account
#
# We choose "Other Sign-In Options" → "Sign in Later in Settings" here.
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar><up><spacebar>",
# Are you sure you want to skip signing in with an Apple Account?
"<wait10s><tab><spacebar>",
# Terms and Conditions
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# I have read and agree to the macOS Software License Agreement
"<wait10s><tab><spacebar>",
# Age Range -> Adult
"<wait10s><tab><tab><tab><spacebar>",
# Choose Your Look
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Enable Location Services
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Are you sure you don't want to use Location Services?
"<wait10s><tab><spacebar>",
# Select Your Time Zone
"<wait10s><tab><tab><tab>UTC<enter><leftShiftOn><tab><leftShiftOff><spacebar>",
# Analytics
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Screen Time
"<wait10s><tab><tab><spacebar>",
# Siri
"<wait10s><tab><spacebar><leftShiftOn><tab><leftShiftOff><spacebar>",
# You Mac is Ready for FileVault
"<wait10s><leftShiftOn><tab><tab><leftShiftOff><spacebar>",
# Mac Data Will Not Be Securely Encrypted
"<wait10s><tab><spacebar>",
# Update Mac Automatically
"<wait10s><tab><tab><spacebar>",
# Liquid Glass
"<wait10s><leftShiftOn><tab><leftShiftOff><spacebar>",
# Welcome to Mac
"<wait30s><spacebar>",
# Disable Voice Over
"<wait10s><leftAltOn><f5><leftAltOff>",
# 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>",
# Now that the installation is done, open "System Settings"
# On Tahoe opening System Settings through Spotlight is not very reliable, sometimes opens System information
"<wait10s>open '/System/Applications/System Settings.app'<enter>",
"<wait120s>",
# Navigate to "Sharing"
"<wait10s><leftShiftOn><tab><tab><tab><tab><tab><leftShiftOff><spacebar>",
# Navigate to "Screen Sharing" and enable it
"<wait10s><tab><tab><tab><tab><tab><spacebar>",
# Type in the password to allow enabling Screen Sharing
"<wait10s>admin<enter>",
# Navigate to "Remote Login" and enable it
"<wait10s><tab><tab><tab><tab><tab><tab><tab><tab><tab><tab><tab><tab><spacebar>",
# Quit System Settings
"<wait10s><leftAltOn>q<leftAltOff>",
# 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>",
@@ -114,6 +53,16 @@ source "tart-cli" "tart" {
"<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
@@ -131,11 +80,8 @@ build {
inline = [
// Enable passwordless sudo
"echo admin | sudo -S sh -c \"mkdir -p /etc/sudoers.d/; echo 'admin ALL=(ALL) NOPASSWD: ALL' | EDITOR=tee visudo /etc/sudoers.d/admin-nopasswd\"",
// Enable auto-login
//
// See https://github.com/xfreebird/kcpassword for details.
"echo '00000000: 1ced 3f4a bcbc ba2c caca 4e82' | sudo xxd -r - /etc/kcpassword",
"sudo defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser admin",
// 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
@@ -161,7 +107,11 @@ build {
provisioner "shell" {
inline = [
# Ensure that Gatekeeper is disabled
"spctl --status | grep -q 'assessments 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'",
]
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ packer {
}
source "tart-cli" "tart" {
from_ipsw = "https://updates.cdn-apple.com/2026SummerFCS/fullrestores/140-83079/25315EF6-AEAB-4588-9774-A3723774C47F/UniversalMac_26.6.1_25G76_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"
cpu_count = 4
memory_gb = 8
+9 -10
View File
@@ -191,14 +191,7 @@ build {
}
}
provisioner "shell" {
inline = [
"source ~/.zprofile",
"sudo xcode-select -s /Applications/Xcode_${var.xcode_version[0]}.app/Contents/Developer",
"xcodebuild -downloadAllPlatforms",
]
}
// Download legacy iOS runtimes before selecting the newest Xcode.
provisioner "shell" {
inline = concat(
["source ~/.zprofile"],
@@ -208,6 +201,14 @@ build {
)
}
provisioner "shell" {
inline = [
"source ~/.zprofile",
"sudo xcode-select -s /Applications/Xcode_${var.xcode_version[0]}.app/Contents/Developer",
"xcodebuild -downloadAllPlatforms",
]
}
provisioner "shell" {
inline = concat(
["source ~/.zprofile"],
@@ -235,8 +236,6 @@ build {
"git clone --depth 1 https://github.com/tuist/homebrew-tuist.git \"$(brew --repository)/Library/Taps/tuist/homebrew-tuist\"",
"rm -rf \"$(brew --repository)/Library/Taps/tuist/homebrew-tuist/Casks\"",
"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 install 3.3.10",
"rbenv global 3.3.10", # fastlane conflicts with 3.4.0+ https://github.com/fastlane/fastlane/issues/29527
"gem update",
"gem install fastlane",
"gem install cocoapods",