Files

317 lines
13 KiB
YAML

name: macOS build
on:
workflow_dispatch:
inputs:
device_repository:
description: Device repository as owner/name
required: true
default: UniXNU/UniXNU-sheng
device_ref:
description: Device repository ref
required: true
default: main
defconfig:
description: Defconfig name (configs/ of the device repository)
required: true
default: sheng_defconfig
steps:
description: Steps to build (nothing is uploaded except text logs; userdata needs about 16 GB of disk)
type: choice
options:
- kernel
- kernel payload
- kernel userdata
- kernel userdata payload
default: kernel
xcode:
description: Xcode version of the macos-26 image (/Applications/Xcode_<version>.app)
required: true
default: "26.6"
ipsw_url:
description: IPSW URL (empty uses board.toml)
required: false
default: ""
pre_build:
description: Optional shell commands run in the UniXNU checkout before the kernel build
required: false
default: ""
trust_cache_url:
description: Trust cache bundle URL (optional; masked in the logs)
required: false
default: ""
owner_trust_cache:
description: Treat the trust cache URL as an owner-signed bundle
type: boolean
default: false
data_seed_url:
description: Data seed URL (optional; masked in the logs)
required: false
default: ""
grow_gib:
description: Grow the Data seed to this many GiB (optional)
required: false
default: ""
skip_setup:
description: Mark Setup as done in the Data seed
type: boolean
default: false
permissions:
contents: read
# One build per device configuration and ref. A dispatch for another device or defconfig runs next to it,
# and a running build is never cancelled by a later dispatch.
concurrency:
group: macos-build-${{ github.ref }}-${{ inputs.device_repository }}-${{ inputs.device_ref }}-${{ inputs.defconfig }}
cancel-in-progress: false
env:
HOMEBREW_NO_AUTO_UPDATE: 1
HOMEBREW_NO_INSTALL_CLEANUP: 1
HOMEBREW_NO_ENV_HINTS: 1
defaults:
run:
shell: bash
jobs:
build:
# Pinned: macos-latest moves between images (macos-15 had an Xcode whose clang rejects the warning options the
# build passes with -Werror). macos-26 is arm64 and carries the Xcode 26 line the build is developed with.
runs-on: macos-26
timeout-minutes: 360
env:
DEVELOPER_DIR: /Applications/Xcode_${{ inputs.xcode }}.app/Contents/Developer
UNIXNU_DEVICE: ${{ github.workspace }}/device
UNIXNU_IPSW: ${{ inputs.ipsw_url }}
DEFCONFIG: ${{ inputs.defconfig }}
STEPS: ${{ inputs.steps }}
steps:
- name: Check out UniXNU
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check out the device repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ inputs.device_repository }}
ref: ${{ inputs.device_ref }}
path: device
token: ${{ secrets.UNIXNU_DEVICE_TOKEN || github.token }}
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Check the inputs
id: check
env:
HAS_CHIP_ID: ${{ secrets.UNIXNU_UNIQUE_CHIP_ID != '' }}
OWNER_TRUST_CACHE: ${{ inputs.owner_trust_cache }}
GROW_GIB: ${{ inputs.grow_gib }}
SKIP_SETUP: ${{ inputs.skip_setup }}
run: |
# The URLs can carry access tokens and the logs of a public repository are public: read them from the
# event payload, never from step environments, and mask them before any later step prints its env.
trust_cache_url=$(jq -r '.inputs.trust_cache_url // ""' "$GITHUB_EVENT_PATH")
data_seed_url=$(jq -r '.inputs.data_seed_url // ""' "$GITHUB_EVENT_PATH")
if [ -n "$trust_cache_url" ]; then echo "::add-mask::$trust_cache_url"; fi
if [ -n "$data_seed_url" ]; then echo "::add-mask::$data_seed_url"; fi
fail() { echo "::error::$1"; exit 1; }
[ -f device/board.toml ] || fail "the device repository has no board.toml"
[ -f "device/configs/$DEFCONFIG" ] || fail "device/configs/$DEFCONFIG not found: $(cd device/configs 2>/dev/null && printf '%s ' *_defconfig)"
[ -d "$DEVELOPER_DIR" ] || fail "Xcode not found at $DEVELOPER_DIR; installed: $(printf '%s ' /Applications/Xcode_*.app)"
if [ -n "$GROW_GIB" ] && ! [[ "$GROW_GIB" =~ ^[0-9]+$ ]]; then fail "grow_gib must be a whole number of GiB"; fi
if [ -n "$GROW_GIB" ] && [ -z "$data_seed_url" ]; then fail "grow_gib needs data_seed_url"; fi
if [ "$SKIP_SETUP" = true ] && [ -z "$data_seed_url" ]; then fail "skip_setup needs data_seed_url"; fi
if [ "$OWNER_TRUST_CACHE" = true ] && [ -z "$trust_cache_url" ]; then fail "owner_trust_cache needs trust_cache_url"; fi
if [[ "$STEPS" != *payload* ]] && { [ -n "$trust_cache_url" ] || [ -n "$data_seed_url" ] || [ "$SKIP_SETUP" = true ]; }; then
fail "trust_cache_url, data_seed_url and skip_setup only apply to the payload step"
fi
python3 - <<'PY'
import os, sys
sys.path.insert(0, 'scripts')
from unixnu.cli import parser, cmd_defconfig
from unixnu.config import Context
ctx = Context(parser().parse_args(['defconfig', os.environ['DEFCONFIG']]))
cmd_defconfig(ctx)
board = ctx.board
name = board.get('board', {}).get('name')
if not name:
sys.exit('::error::board.toml needs [board] name')
adt = board.get('payload', {}).get('adt', {})
if 'payload' in os.environ['STEPS'] and adt.get('unique_chip_id') == 'required' and os.environ['HAS_CHIP_ID'] != 'true':
sys.exit('::error::this board needs the platform seed for its payload: set the UNIXNU_UNIQUE_CHIP_ID secret')
with open(os.environ['GITHUB_OUTPUT'], 'a') as out:
out.write(f'board={name}\n')
out.write(f'work={ctx.work}\n')
PY
- name: Install host tools
run: |
# cmake and ninja come with the image. Homebrew LLVM is keg-only and stays off PATH: the driver finds it for
# the archive builders, and everything else uses Xcode's clang. LLD is a separate formula since LLVM 23;
# the payload step links the panic screen with ld.lld.
brew install llvm lld mtools dtc
if ! command -v unifdef >/dev/null; then
brew install unifdef
echo "$(brew --prefix unifdef)/bin" >> "$GITHUB_PATH"
fi
- name: Report the runner toolchain
run: |
sw_vers
uname -m
xcodebuild -version
xcrun --sdk iphoneos --show-sdk-version
xcrun --sdk iphoneos --find mig
clang --version | head -n 1
"$(brew --prefix llvm)/bin/clang" --version | head -n 1
python3 --version
missing=0
for tool in git make clang cmake ninja unifdef ld.lld mformat mcopy dtc xcodebuild; do
path=$(command -v "$tool" || true)
printf '%-10s %s\n' "$tool" "${path:-MISSING}"
[ -n "$path" ] || missing=1
done
df -h "$GITHUB_WORKSPACE"
exit "$missing"
- name: Prepare inputs
if: contains(inputs.steps, 'payload')
env:
GROW_GIB: ${{ inputs.grow_gib }}
run: |
trust_cache_url=$(jq -r '.inputs.trust_cache_url // ""' "$GITHUB_EVENT_PATH")
data_seed_url=$(jq -r '.inputs.data_seed_url // ""' "$GITHUB_EVENT_PATH")
if [ -n "$trust_cache_url" ]; then
curl -fsSL --retry 3 "$trust_cache_url" -o "$RUNNER_TEMP/trustcache"
fi
if [ -n "$data_seed_url" ]; then
curl -fsSL --retry 3 "$data_seed_url" -o "$RUNNER_TEMP/data.dbr"
if [ -n "$GROW_GIB" ]; then
# seed-data-grow.py imports drivers/DarwinBoot/Test and keeps its scratch image in build/.
git submodule update --init --depth 1 drivers/DarwinBoot
mkdir -p build
python3 scripts/seed-data-grow.py --gib "$GROW_GIB" "$RUNNER_TEMP/data.dbr" "$RUNNER_TEMP/data-grow.dbr"
mv "$RUNNER_TEMP/data-grow.dbr" "$RUNNER_TEMP/data.dbr"
fi
fi
- name: Run pre-build steps
if: inputs.pre_build != ''
env:
PRE_BUILD: ${{ inputs.pre_build }}
DEVICE_DIR: ${{ github.workspace }}/device
GITHUB_TOKEN: ${{ github.token }}
run: bash -euo pipefail -c "$PRE_BUILD"
- name: Build kernel
id: kernel
env:
# prepare-xnu-source.py lists Apple sources through the GitHub API; anonymous calls from hosted runners hit
# the shared rate limit (HTTP 403).
GITHUB_TOKEN: ${{ github.token }}
run: ./unixnu build kernel
- name: Build userdata
id: userdata
if: contains(inputs.steps, 'userdata')
run: |
./unixnu build userdata
df -h "$GITHUB_WORKSPACE"
- name: Build payload
id: payload
if: contains(inputs.steps, 'payload')
env:
UNIXNU_UNIQUE_CHIP_ID: ${{ secrets.UNIXNU_UNIQUE_CHIP_ID }}
SKIP_SETUP: ${{ inputs.skip_setup }}
OWNER_TRUST_CACHE: ${{ inputs.owner_trust_cache }}
run: |
args=()
if [ -f "$RUNNER_TEMP/trustcache" ]; then
if [ "$OWNER_TRUST_CACHE" = true ]; then
args+=(--owner-trust-cache "$RUNNER_TEMP/trustcache")
else
args+=(--trust-cache "$RUNNER_TEMP/trustcache")
fi
fi
if [ -f "$RUNNER_TEMP/data.dbr" ]; then args+=(--data-ramdisk "$RUNNER_TEMP/data.dbr"); fi
if [ "$SKIP_SETUP" = true ]; then args+=(--skip-setup); fi
./unixnu build payload "${args[@]}"
- name: Collect the text logs
id: logs
if: always() && steps.check.outcome == 'success'
env:
UNIXNU_UNIQUE_CHIP_ID: ${{ secrets.UNIXNU_UNIQUE_CHIP_ID }}
BOARD: ${{ steps.check.outputs.board }}
WORK: ${{ steps.check.outputs.work }}
run: |
# Only *.log files are copied, with the secrets and the input URLs replaced. Kernels, payloads, disk images,
# kernelcaches and IPSW members are never uploaded: they derive from Apple software.
python3 - <<'PY'
import os, re, shutil
from pathlib import Path
source = Path(os.environ['WORK']) / 'logs'
target = Path(os.environ['RUNNER_TEMP']) / 'public-logs'
shutil.rmtree(target, ignore_errors=True)
target.mkdir(parents=True)
event = Path(os.environ['GITHUB_EVENT_PATH']).read_text()
import json
inputs = json.loads(event).get('inputs', {})
secrets = [os.environ.get('UNIXNU_UNIQUE_CHIP_ID', ''), inputs.get('trust_cache_url', ''), inputs.get('data_seed_url', '')]
secrets = [value for value in secrets if value]
count = 0
for path in sorted(source.glob('*.log')) if source.is_dir() else []:
data = path.read_bytes()
if b'\0' in data:
continue
text = data.decode('utf-8', 'replace')
for value in secrets:
text = text.replace(value, '***')
text = re.sub(r'https?://[^\s"\']*[?&](?:token|sig|signature|key|X-Amz-Signature)=[^\s"\']*', '***', text, flags=re.I)
(target / path.name).write_text(text)
count += 1
print(f'{count} text logs staged')
PY
- name: Upload the build logs
if: always() && steps.logs.outcome == 'success'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: unixnu-${{ steps.check.outputs.board }}-logs-${{ github.run_attempt }}
path: ${{ runner.temp }}/public-logs/*.log
if-no-files-found: warn
retention-days: 7
- name: Summary
if: always()
env:
BOARD: ${{ steps.check.outputs.board }}
KERNEL: ${{ steps.kernel.outcome }}
USERDATA: ${{ steps.userdata.outcome }}
PAYLOAD: ${{ steps.payload.outcome }}
run: |
{
echo "## macOS build: ${BOARD:-unknown} (${DEFCONFIG}, ${STEPS})"
echo
echo "| Step | Result |"
echo "| --- | --- |"
echo "| kernel | ${KERNEL:-skipped} |"
echo "| userdata | ${USERDATA:-skipped} |"
echo "| payload | ${PAYLOAD:-skipped} |"
echo
echo "This workflow only checks that the build works. It publishes text logs (7 days) and no images."
xcodebuild -version | head -n 1
} >> "$GITHUB_STEP_SUMMARY"
- name: Remove proprietary inputs
if: always()
run: |
rm -rf out/ipsw out/feeds device/.cache/firmware out/*/tree/.cache/firmware out/*/payload/data-skip-setup-* out/*/*/*/tree/.cache/firmware out/*/*/*/payload/data-skip-setup-*
rm -f "$RUNNER_TEMP/trustcache" "$RUNNER_TEMP/data.dbr" "$RUNNER_TEMP/data-grow.dbr"