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:
@@ -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', () => {
|
||||
|
||||
39
apps/desktop/scripts/macos-sysroot-native.test.mjs
Normal file
39
apps/desktop/scripts/macos-sysroot-native.test.mjs
Normal 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)
|
||||
@@ -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']
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user