fix(desktop): preserve named SDK overrides in native builds

Resolve SDKROOT through xcrun before passing a sysroot to clang. Exercise both real universal helper builds with default, absolute and named SDK selection, and retain resolver precedence and fallback coverage.
This commit is contained in:
brooklyn!
2026-09-22 22:35:52 -05:00
parent c940027ade
commit 6a229b522e
4 changed files with 99 additions and 83 deletions

View File

@@ -14,44 +14,28 @@ afterEach(() => {
vi.mocked(execFileSync).mockReset()
})
function stageDistDir(prefix) {
const distDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix))
const staging = path.resolve(distDir, `native/command-screenshot-monitor.${process.pid}.tmp`)
fs.mkdirSync(path.dirname(staging), { recursive: true })
fs.writeFileSync(staging, 'staged')
return distDir
}
// The helper shells out to xcrun; pin the argv contract (not the toolchain),
// so a non-macOS CI host still proves what the macOS build will run.
describe('buildCommandScreenshotMonitor argv', () => {
it('links against the sysroot paired with the active toolchain', () => {
const distDir = stageDistDir('csm-argv-')
it('uses the paired sysroot or delegates SDK selection to xcrun', () => {
for (const sysroot of ['/Developer/SDKs/MacOSX.sdk', null]) {
const distDir = fs.mkdtempSync(path.join(os.tmpdir(), 'csm-argv-'))
const staging = path.resolve(distDir, `native/command-screenshot-monitor.${process.pid}.tmp`)
fs.mkdirSync(path.dirname(staging), { recursive: true })
fs.writeFileSync(staging, 'staged')
const out = buildCommandScreenshotMonitor({
distDir,
platform: 'darwin',
sysroot: '/Developer/SDKs/MacOSX.sdk',
})
expect(execFileSync).toHaveBeenCalledOnce()
const [cmd, argv] = vi.mocked(execFileSync).mock.calls[0]
expect(cmd).toBe('xcrun')
expect(argv.slice(0, 3)).toEqual(['clang', '-isysroot', '/Developer/SDKs/MacOSX.sdk'])
expect(out).toBe(path.resolve(distDir, 'native/command-screenshot-monitor'))
fs.rmSync(distDir, { recursive: true, force: true })
})
it('falls back to the default SDK when no paired sysroot exists', () => {
const distDir = stageDistDir('csm-fallback-')
buildCommandScreenshotMonitor({ distDir, platform: 'darwin', sysroot: null })
const [, argv] = vi.mocked(execFileSync).mock.calls[0]
// `--sdk macosx` must precede the tool name: xcrun options after `clang`
// would be handed to clang instead of resolving the SDK.
expect(argv.slice(0, 3)).toEqual(['--sdk', 'macosx', 'clang'])
fs.rmSync(distDir, { recursive: true, force: true })
try {
const out = buildCommandScreenshotMonitor({ distDir, platform: 'darwin', sysroot })
const [cmd, argv] = vi.mocked(execFileSync).mock.lastCall
expect(cmd).toBe('xcrun')
expect(argv.slice(0, 3)).toEqual(sysroot
? ['clang', '-isysroot', sysroot]
: ['--sdk', 'macosx', 'clang'])
expect(out).toBe(path.resolve(distDir, 'native/command-screenshot-monitor'))
} finally {
fs.rmSync(distDir, { recursive: true, force: true })
}
}
})
it('is a no-op off macOS', () => {

View File

@@ -0,0 +1,39 @@
import { execFileSync } from 'node:child_process'
import { mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { resolve } from 'node:path'
import { expect, test } from 'vitest'
import { macosSysroot } from './macos-sysroot.mjs'
test.skipIf(process.platform !== 'darwin')('builds both universal helpers with default, path and named SDK selection', () => {
const dir = mkdtempSync(resolve(tmpdir(), 'hermes-sdk-builds-'))
const env = { ...process.env, SDKROOT: '' }
const helpers = [
['build-command-screenshot-monitor.mjs', 'native/command-screenshot-monitor'],
['build-hud-modifier-monitor.mjs', 'native/darwin-universal/hud-modifier-monitor']
]
try {
const sdk = macosSysroot(env) ?? execFileSync('xcrun', ['--sdk', 'macosx', '--show-sdk-path'], {
encoding: 'utf8', env
}).trim()
const version = execFileSync('xcrun', ['--sdk', sdk, '--show-sdk-version'], {
encoding: 'utf8', env
}).trim()
for (const [name, SDKROOT] of [['default', ''], ['path', sdk], ['name', `macosx${version}`]]) {
for (const [script, relativeBinary] of helpers) {
const dist = resolve(dir, name, script)
execFileSync(process.execPath, [resolve(import.meta.dirname, script), '--out-dir', dist], {
env: { ...env, SDKROOT }, timeout: 60_000
})
const architectures = execFileSync('xcrun', ['lipo', '-archs', resolve(dist, relativeBinary)], {
encoding: 'utf8', env
}).trim().split(/\s+/).sort()
expect(architectures, `${script} with ${name} SDK selection`).toEqual(['arm64', 'x86_64'])
}
}
} finally {
rmSync(dir, { recursive: true, force: true })
}
}, 120_000)

View File

@@ -3,23 +3,24 @@ import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { resolve } from 'node:path'
// `xcrun --sdk macosx` resolves to the highest-versioned SDK installed, not the
// one the active toolchain ships with. A host carrying an SDK newer than its
// Command Line Tools then links against .tbd stubs whose architectures the
// linker cannot parse (#113708). The MacOSX.sdk symlink under the active
// developer dir names the paired SDK, so target that instead.
// `--sdk macosx` can select a newer SDK than the linker understands (#113708).
// Prefer the active toolchain's MacOSX.sdk alias unless the caller pins an SDK.
export function macosSysroot(env = process.env) {
if (env.SDKROOT) return env.SDKROOT
const developerDir = execFileSync('xcode-select', ['-p'], { encoding: 'utf8' }).trim()
if (env.SDKROOT) {
// SDKROOT accepts SDK names as well as paths; clang's -isysroot only accepts paths.
return execFileSync('xcrun', ['--sdk', env.SDKROOT, '--show-sdk-path'], {
encoding: 'utf8', env
}).trim()
}
const developerDir = execFileSync('xcode-select', ['-p'], { encoding: 'utf8', env }).trim()
return [
resolve(developerDir, 'SDKs/MacOSX.sdk'),
resolve(developerDir, 'Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'),
].find(existsSync) ?? null
}
// clang needs the SDK named before the linker runs; when no paired SDK exists
// fall back to xcrun's own default, which at least keeps driver and linker in
// agreement. `--sdk macosx` must precede the tool name or xcrun hands it to clang.
// Preserve xcrun's previous selection when neither developer layout has a default.
// `--sdk` must precede the tool name or xcrun passes it to clang.
export function xcrunClangArgv(sysroot) {
return sysroot ? ['clang', '-isysroot', sysroot] : ['--sdk', 'macosx', 'clang']
}

View File

@@ -1,7 +1,7 @@
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, expect, it, vi } from 'vitest'
vi.mock('node:child_process', () => ({
execFileSync: vi.fn(),
@@ -14,45 +14,37 @@ afterEach(() => {
vi.mocked(execFileSync).mockReset()
})
describe('macosSysroot', () => {
it('prefers an explicit SDKROOT over the developer dir', () => {
expect(macosSysroot({ SDKROOT: '/pinned/MacOSX.sdk' })).toBe('/pinned/MacOSX.sdk')
expect(execFileSync).not.toHaveBeenCalled()
})
it('resolves explicit SDKs before developer defaults and preserves the no-SDK fallback', () => {
const developerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sysroot-'))
const env = { DEVELOPER_DIR: developerDir }
const sdk = path.join(developerDir, 'SDKs/MacOSX.sdk')
try {
for (const SDKROOT of [sdk, 'macosx']) {
const override = { ...env, SDKROOT }
vi.mocked(execFileSync).mockReturnValue(`${sdk}\n`)
expect(macosSysroot(override)).toBe(sdk)
expect(execFileSync).toHaveBeenLastCalledWith('xcrun', ['--sdk', SDKROOT, '--show-sdk-path'], {
encoding: 'utf8', env: override
})
}
vi.mocked(execFileSync).mockImplementationOnce(() => { throw new Error('SDK not found') })
expect(() => macosSysroot({ ...env, SDKROOT: 'missing-sdk' })).toThrow('SDK not found')
it('names the Command Line Tools SDK paired with the toolchain', () => {
const developerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sysroot-clt-'))
fs.mkdirSync(path.join(developerDir, 'SDKs/MacOSX.sdk'), { recursive: true })
vi.mocked(execFileSync).mockReturnValue(`${developerDir}\n`)
expect(macosSysroot({})).toBe(path.join(developerDir, 'SDKs/MacOSX.sdk'))
fs.rmSync(developerDir, { recursive: true, force: true })
})
it('falls back to the Xcode platform SDK layout', () => {
const developerDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sysroot-xcode-'))
const sdk = 'Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'
fs.mkdirSync(path.join(developerDir, sdk), { recursive: true })
vi.mocked(execFileSync).mockReturnValue(`${developerDir}\n`)
expect(macosSysroot({})).toBe(path.join(developerDir, sdk))
fs.rmSync(developerDir, { recursive: true, force: true })
})
it('returns null when the developer dir ships no MacOSX.sdk', () => {
vi.mocked(execFileSync).mockReturnValue(`${os.tmpdir()}/sysroot-no-such-dir\n`)
expect(macosSysroot({})).toBeNull()
})
})
describe('xcrunClangArgv', () => {
it('pins the sysroot when one was resolved', () => {
expect(xcrunClangArgv('/sdk/MacOSX.sdk')).toEqual(['clang', '-isysroot', '/sdk/MacOSX.sdk'])
})
// xcrun options after `clang` would be handed to clang instead of resolving the SDK.
it('names the SDK before the tool when falling back', () => {
expect(macosSysroot(env)).toBeNull()
expect(xcrunClangArgv(null)).toEqual(['--sdk', 'macosx', 'clang'])
})
// When both layouts exist, the CLT default wins over the Xcode platform fallback.
for (const relative of ['Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk', 'SDKs/MacOSX.sdk']) {
const paired = path.join(developerDir, relative)
fs.mkdirSync(paired, { recursive: true })
expect(macosSysroot(env)).toBe(paired)
expect(execFileSync).toHaveBeenLastCalledWith('xcode-select', ['-p'], { encoding: 'utf8', env })
expect(xcrunClangArgv(paired)).toEqual(['clang', '-isysroot', paired])
}
} finally {
fs.rmSync(developerDir, { recursive: true, force: true })
}
})