mirror of
https://github.com/cirruslabs/macos-image-templates.git
synced 2026-09-30 03:42:04 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
106c086ffa | ||
|
|
795a125e78 | ||
|
|
4849509b2d | ||
|
|
210e02922f | ||
|
|
d6ee41d6d5 | ||
|
|
741758b9ba | ||
|
|
f03344599b | ||
|
|
16e9ad7d00 | ||
|
|
85b77e41bc | ||
|
|
9be4b9dc86 | ||
|
|
46050816d8 | ||
|
|
d358da6f50 | ||
|
|
b4926493fb |
@@ -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
|
||||
@@ -34,9 +34,11 @@ jobs:
|
||||
fail-fast: false
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
macos_version:
|
||||
- tahoe
|
||||
- sequoia
|
||||
# Xcode 27 requires macOS Tahoe 26.4 or later.
|
||||
macos_version: >-
|
||||
${{ startsWith(github.event.release.tag_name || inputs.xcode_version, '27')
|
||||
&& fromJSON('["tahoe"]')
|
||||
|| fromJSON('["tahoe", "sequoia"]') }}
|
||||
env:
|
||||
MACOS_VERSION: ${{ matrix.macos_version }}
|
||||
XCODE_COMPONENTS: '"MetalToolchain"'
|
||||
@@ -105,7 +107,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: '"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"'
|
||||
|
||||
@@ -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,21 @@ 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"
|
||||
|
||||
- name: Validate templates
|
||||
run: |
|
||||
@@ -89,6 +101,11 @@ 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"]'
|
||||
|
||||
build-vanilla:
|
||||
name: Build Vanilla Image (${{ matrix.macos_version }})
|
||||
needs: packer-validate
|
||||
@@ -100,6 +117,7 @@ jobs:
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
macos_version:
|
||||
- golden-gate
|
||||
- tahoe
|
||||
- sequoia
|
||||
- sonoma
|
||||
@@ -164,6 +182,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 }}
|
||||
@@ -185,7 +205,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
|
||||
|
||||
@@ -232,7 +252,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: '"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"'
|
||||
@@ -328,8 +348,8 @@ jobs:
|
||||
|
||||
candidate=""
|
||||
case "$version" in
|
||||
27-beta-2)
|
||||
candidate="$HOME/XcodesCache/Xcode_27_beta_2.xip"
|
||||
27-beta-6)
|
||||
candidate="$HOME/XcodesCache/Xcode_27_beta_6.xip"
|
||||
;;
|
||||
27-beta)
|
||||
candidate="$HOME/XcodesCache/Xcode_27_beta.xip"
|
||||
|
||||
@@ -13,6 +13,7 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- golden-gate
|
||||
- tahoe
|
||||
- sequoia
|
||||
- sonoma
|
||||
@@ -46,6 +47,7 @@ jobs:
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
macos_version:
|
||||
- golden-gate
|
||||
- tahoe
|
||||
- sequoia
|
||||
- sonoma
|
||||
|
||||
@@ -5,13 +5,42 @@ GitHub Actions runners, [Cirrus Runners](https://cirrus-runners.app/) or [any ot
|
||||
|
||||
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-{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}-vanilla` — a vanilla macOS installation with helpful tweaks such as auto-login, but no additional software preinstalled
|
||||
* `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-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.
|
||||
|
||||
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
|
||||
|
||||
@@ -6,10 +6,10 @@ 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
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
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
@@ -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",
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
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/2026SummerSeed/7b1c2bd9-7617-426d-92e5-ef204407ffaa/UniversalMac_27.0_26A5416b_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>",
|
||||
]
|
||||
|
||||
// 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\"",
|
||||
// Enable Screen Sharing for "tart run --vnc"
|
||||
"sudo launchctl enable system/com.apple.screensharing",
|
||||
// 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 FileVault remains disabled by default
|
||||
"sudo fdesetup status | grep -q 'FileVault is Off'",
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -235,8 +235,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",
|
||||
|
||||
Reference in New Issue
Block a user