Merge nucleic/keen-grove-ferret-3rgz into dev
This commit is contained in:
@@ -97,7 +97,13 @@ extension MacVMEngine {
|
||||
// on exec ("damaged and can't be opened"). Phase 7⅞ then blamed the Xcode license for that
|
||||
// SIGKILL and failed the build. §7½ now also proves a required Xcode RUNS before accepting it.
|
||||
// A v14 base was either never published or holds a damaged Xcode, so force them all.
|
||||
public static let macOSProvisioningRecipe = 15
|
||||
// v16: Xcode 27 allows `xcodebuild -version` before its license is accepted, so v15's alleged
|
||||
// functional license probe returned a false positive, skipped `-license accept`, and then
|
||||
// failed every base at `-runFirstLaunch`. Phase 7⅞ now compares the bundle's declared
|
||||
// LicenseInfo.plist ID/type against the matching system-wide IDELast<Type>LicenseAgreedTo
|
||||
// value. Force any v15 base through the exact license gate (failed bases retry regardless,
|
||||
// while this also corrects any base that happened to get stamped around the bad heuristic).
|
||||
public static let macOSProvisioningRecipe = 16
|
||||
|
||||
/// The current Linux base **provisioning-recipe version**. Bump when the bundled Linux provisioning
|
||||
/// assets change in a way that must re-provision an already-built base — a new step in
|
||||
|
||||
@@ -1699,7 +1699,7 @@ import UniformTypeIdentifiers
|
||||
/// prints "Killed: 9"; Finder says "damaged and can't be opened"). Phase 7⅞ met exactly that and
|
||||
/// reported it as an unaccepted Xcode LICENSE — sending the operator to run `-license accept`
|
||||
/// against a bundle that cannot run at all. A signal death must be diagnosed as damage, at the
|
||||
/// copy, and a plain nonzero exit must still be treated as the license being outstanding.
|
||||
/// copy, while license state must be checked independently against the bundle's declared ID.
|
||||
@Test func macOSGuestProvisionerDiagnosesADamagedXcodeRatherThanBlamingTheLicense() throws {
|
||||
let repoRoot = URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
|
||||
@@ -1711,21 +1711,22 @@ import UniformTypeIdentifiers
|
||||
let installed = try #require(script.range(of: "installed $name is DAMAGED"))
|
||||
#expect(script.contains(#"|| xcodebuild_rc=$?"#))
|
||||
#expect(script.contains(#"if [ "$xcodebuild_rc" -ge 128 ]"#))
|
||||
// …and the later license pass separates a signal from a nonzero exit before it accuses the
|
||||
// license, which it must still go on to accept for the ordinary (exit 1) case.
|
||||
let licenseCheck = try #require(script.range(of: "xcode_run_status()"))
|
||||
// …and the later license pass keeps the signal-only integrity probe separate from the exact
|
||||
// preference-based license check, which still goes on to accept an outstanding license.
|
||||
let licenseCheck = try #require(script.range(of: "xcode_binary_status()"))
|
||||
let damaged = try #require(script.range(of: "is DAMAGED: xcodebuild is killed by signal"))
|
||||
let licenseAccepted = try #require(script.range(of: "-license accept", options: .backwards))
|
||||
#expect(installed.upperBound < licenseCheck.lowerBound)
|
||||
#expect(damaged.upperBound < licenseAccepted.lowerBound)
|
||||
#expect(script.contains("This is NOT a license problem"))
|
||||
#expect(script.contains(#"xcode_license_ok() { [ "$(xcode_run_status)" -eq 0 ]; }"#))
|
||||
#expect(script.contains("XCODE_LICENSE_INFO"))
|
||||
#expect(script.contains("IDELast${XCODE_LICENSE_TYPE}LicenseAgreedTo"))
|
||||
}
|
||||
|
||||
/// A copied-in Xcode is unusable until its license is accepted by root, and an agent exec has no
|
||||
/// TTY to answer the prompt in — so the provisioner must accept it during the base build, verify
|
||||
/// the acceptance the way a build does, and fall back to driving the interactive prompt with its
|
||||
/// confirmations pre-fed rather than trusting `-license accept`'s exit code.
|
||||
/// the exact license ID recorded system-wide, and fall back to driving the interactive prompt
|
||||
/// with its confirmations pre-fed rather than trusting `-license accept`'s exit code.
|
||||
@Test func macOSGuestProvisionerAcceptsTheXcodeLicenseBeforeBuildingAnything() throws {
|
||||
let repoRoot = URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent()
|
||||
@@ -1740,8 +1741,13 @@ import UniformTypeIdentifiers
|
||||
script.range(of: "printf 'q\\nagree\\nagree\\ny\\n'", options: .backwards))
|
||||
#expect(accept.upperBound < interactive.lowerBound)
|
||||
#expect(script.contains("sudo \"$XCODE_DEVDIR/usr/bin/xcodebuild\" -license >/dev/null"))
|
||||
// Verified functionally (what a build hits), not by exit code…
|
||||
// Verified against this bundle's exact declared license, not via `-version` (Xcode 27 allows
|
||||
// that informational command while its license is still outstanding) or the accept exit code.
|
||||
#expect(script.contains("xcode_license_ok()"))
|
||||
#expect(script.contains("Contents/Resources/LicenseInfo.plist"))
|
||||
#expect(script.contains(#"defaults read /Library/Preferences/com.apple.dt.Xcode"#))
|
||||
#expect(script.contains(#"[ "$agreed" = "$XCODE_LICENSE_ID" ]"#))
|
||||
#expect(!script.contains(#"xcode_license_ok() { [ "$(xcode_binary_status)" -eq 0 ]; }"#))
|
||||
// …and everything happens before the Metal download, which needs a licensed xcodebuild.
|
||||
let metal = try #require(
|
||||
script.range(of: "-downloadComponent MetalToolchain", options: .backwards))
|
||||
@@ -2962,7 +2968,7 @@ import UniformTypeIdentifiers
|
||||
provisioned: true, agent: false, grants: true), os: .macOS))
|
||||
#expect(MacVMEngine.baseIsUsable(status(provisioned: true, agent: false), os: .linux))
|
||||
#expect(!MacVMEngine.baseIsUsable(status(provisioned: false, agent: false), os: .linux))
|
||||
#expect(MacVMEngine.macOSProvisioningRecipe == 15)
|
||||
#expect(MacVMEngine.macOSProvisioningRecipe == 16)
|
||||
}
|
||||
|
||||
/// Clone-time admission duplicates the current-recipe publication invariant: neither a stale
|
||||
|
||||
+9
-7
@@ -545,13 +545,15 @@ This is what gives a guest a `metal` compiler, and there is no alternative:
|
||||
(falling back to `-downloadPlatform macOS`), and finally a **verification** — resolve `metal` through
|
||||
`xcrun` and compile a probe kernel, because `-downloadComponent` can exit 0 with an unusable toolchain.
|
||||
|
||||
The license step matters as much as the download: a *copied* Xcode refuses every `xcodebuild`/`xcrun`
|
||||
call with *"You have not agreed to the Xcode license agreements"* until root records the agreement, and
|
||||
an exec-only agent has no TTY to answer that prompt in. §7⅞ tries `sudo xcodebuild -license accept`,
|
||||
then — if the license is still outstanding — drives the interactive `sudo xcodebuild -license` with its
|
||||
confirmations pre-fed on stdin (`q`, `agree`), bounded by `run_timeout`. Acceptance is checked
|
||||
*functionally* after each attempt, by running `xcodebuild -version` as the agent (exactly what a build
|
||||
hits), because `-license accept` can exit 0 having recorded nothing. It also runs
|
||||
The license step matters as much as the download: a *copied* Xcode refuses build/first-launch commands
|
||||
with *"You have not agreed to the Xcode license agreements"* until root records the agreement, and an
|
||||
exec-only agent has no TTY to answer that prompt in. §7⅞ tries `sudo xcodebuild -license accept`, then —
|
||||
if the license is still outstanding — drives the interactive `sudo xcodebuild -license` with its
|
||||
confirmations pre-fed on stdin (`q`, `agree`), bounded by `run_timeout`. Acceptance is checked after
|
||||
each attempt by matching this bundle's `Contents/Resources/LicenseInfo.plist` ID/type to the corresponding
|
||||
system-wide `IDELast<Type>LicenseAgreedTo` preference. `xcodebuild -version` is deliberately not the
|
||||
probe: Xcode 27 prints its version and exits successfully while its license is still outstanding, and
|
||||
`-license accept` can exit 0 having recorded nothing. The provisioner also runs
|
||||
`DevToolsSecurity -enable` and adds the agent to the `_developer` group, so the first debug/test run in
|
||||
a clone doesn't pop the "Developer Tools Access needs to take control" authentication panel.
|
||||
Xcode is a base-publication invariant, not a best-effort extra. Host-side staging errors propagate;
|
||||
|
||||
@@ -1021,27 +1021,49 @@ else
|
||||
|
||||
# ── License agreement ───────────────────────────────────────────────────────────────────────────
|
||||
# A just-copied Xcode.app is NOT usable: until the agreement is recorded (by root, system-wide in
|
||||
# /Library/Preferences/com.apple.dt.Xcode), EVERY xcodebuild/xcrun call — and the GUI — stops at
|
||||
# /Library/Preferences/com.apple.dt.Xcode), build/first-launch commands — and the GUI — stop at
|
||||
# "You have not agreed to the Xcode license agreements", and an agent exec has no TTY to answer
|
||||
# that prompt in. So accept it HERE, once per base, and PROVE it took.
|
||||
#
|
||||
# The proof is functional, not an exit code: `xcodebuild -version` run as the agent (NOT root) is
|
||||
# exactly what a build hits, and it fails while the license is outstanding. Probe with it after
|
||||
# every attempt — `-license accept` can exit 0 having recorded nothing.
|
||||
# Do NOT use `xcodebuild -version` as the proof. Xcode 27 prints its version and exits 0 even when
|
||||
# that Xcode's license is still outstanding; that false positive skips acceptance, then
|
||||
# `-runFirstLaunch` stops at the license gate. Instead, read the license ID/type declared by THIS
|
||||
# bundle and require the corresponding system-wide agreement value to match it exactly. (For
|
||||
# example, Xcode 27 beta declares Beta/EA2002 and records IDELastBetaLicenseAgreedTo=EA2002.) Probe
|
||||
# that exact state after every attempt — `-license accept` can exit 0 having recorded nothing.
|
||||
#
|
||||
# But separate the TWO ways that probe can fail. An outstanding license is a nonzero EXIT; a bundle
|
||||
# whose bytes didn't survive being copied here is a SIGNAL — the kernel's code-signing check kills
|
||||
# xcodebuild on exec ("Killed: 9"), and no amount of `-license accept` will ever change that. Reading
|
||||
# the second as the first is what made a damaged Xcode report itself as an unaccepted license, and
|
||||
# sent the operator to run `xcodebuild -license accept` against a bundle that cannot run at all.
|
||||
xcode_run_status() {
|
||||
# Keep executable integrity as a separate probe. A bundle whose bytes didn't survive being copied
|
||||
# here is killed by a SIGNAL when xcodebuild starts — the kernel's code-signing check prints
|
||||
# "Killed: 9" — and no amount of `-license accept` will ever change that. Reading the signal as a
|
||||
# license failure is what once sent operators to accept a license against a bundle that could not
|
||||
# run at all. A zero/nonzero ordinary exit is irrelevant to the exact preference check below.
|
||||
xcode_binary_status() {
|
||||
local rc=0
|
||||
"$XCODE_DEVDIR/usr/bin/xcodebuild" -version >/dev/null 2>&1 || rc=$?
|
||||
printf '%s' "$rc"
|
||||
}
|
||||
xcode_license_ok() { [ "$(xcode_run_status)" -eq 0 ]; }
|
||||
|
||||
XCODE_RUN_RC="$(xcode_run_status)"
|
||||
XCODE_LICENSE_INFO="$XCODE_APP/Contents/Resources/LicenseInfo.plist"
|
||||
XCODE_LICENSE_ID="$(/usr/libexec/PlistBuddy -c 'Print :licenseID' \
|
||||
"$XCODE_LICENSE_INFO" 2>/dev/null || true)"
|
||||
XCODE_LICENSE_TYPE="$(/usr/libexec/PlistBuddy -c 'Print :licenseType' \
|
||||
"$XCODE_LICENSE_INFO" 2>/dev/null || true)"
|
||||
XCODE_LICENSE_PREF=""
|
||||
# Apple currently ships GM, Beta, and PTR licenses. Derive the preference name instead of fixing
|
||||
# those three in a case statement, but accept only a plain alphabetic type before interpolating it.
|
||||
case "$XCODE_LICENSE_TYPE" in
|
||||
"" | *[!A-Za-z]*) ;;
|
||||
*) XCODE_LICENSE_PREF="IDELast${XCODE_LICENSE_TYPE}LicenseAgreedTo" ;;
|
||||
esac
|
||||
xcode_license_ok() {
|
||||
local agreed
|
||||
[ -n "$XCODE_LICENSE_ID" ] && [ -n "$XCODE_LICENSE_PREF" ] || return 1
|
||||
agreed="$(/usr/bin/defaults read /Library/Preferences/com.apple.dt.Xcode \
|
||||
"$XCODE_LICENSE_PREF" 2>/dev/null || true)"
|
||||
[ "$agreed" = "$XCODE_LICENSE_ID" ]
|
||||
}
|
||||
|
||||
XCODE_RUN_RC="$(xcode_binary_status)"
|
||||
if [ "$XCODE_RUN_RC" -ge 128 ]; then
|
||||
echo " ✗ $XCODE_APP is DAMAGED: xcodebuild is killed by signal $((XCODE_RUN_RC - 128)) on" >&2
|
||||
echo " launch, so its code signature no longer validates — the bundle did not survive being" >&2
|
||||
@@ -1053,6 +1075,11 @@ else
|
||||
if xcode_license_ok; then
|
||||
echo " ✓ Xcode license already accepted."
|
||||
else
|
||||
if [ -z "$XCODE_LICENSE_ID" ] || [ -z "$XCODE_LICENSE_PREF" ]; then
|
||||
echo " ✗ could not read a supported license ID/type from $XCODE_LICENSE_INFO." >&2
|
||||
echo " Refusing to guess whether this Xcode's license is accepted." >&2
|
||||
exit 1
|
||||
fi
|
||||
# 1. The documented non-interactive verb.
|
||||
run_timeout 300 sudo "$XCODE_DEVDIR/usr/bin/xcodebuild" -license accept >/dev/null 2>&1 || true
|
||||
if xcode_license_ok; then
|
||||
|
||||
Reference in New Issue
Block a user