diff --git a/apps/desktop/scripts/build-command-screenshot-monitor.test.mjs b/apps/desktop/scripts/build-command-screenshot-monitor.test.mjs index a72e12fbf0..b362dff575 100644 --- a/apps/desktop/scripts/build-command-screenshot-monitor.test.mjs +++ b/apps/desktop/scripts/build-command-screenshot-monitor.test.mjs @@ -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', () => { diff --git a/apps/desktop/scripts/macos-sysroot-native.test.mjs b/apps/desktop/scripts/macos-sysroot-native.test.mjs new file mode 100644 index 0000000000..6df8dbc628 --- /dev/null +++ b/apps/desktop/scripts/macos-sysroot-native.test.mjs @@ -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) diff --git a/apps/desktop/scripts/macos-sysroot.mjs b/apps/desktop/scripts/macos-sysroot.mjs index 0c509b20e0..dd706e0f63 100644 --- a/apps/desktop/scripts/macos-sysroot.mjs +++ b/apps/desktop/scripts/macos-sysroot.mjs @@ -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'] } diff --git a/apps/desktop/scripts/macos-sysroot.test.mjs b/apps/desktop/scripts/macos-sysroot.test.mjs index 55ece0ab27..6cede58474 100644 --- a/apps/desktop/scripts/macos-sysroot.test.mjs +++ b/apps/desktop/scripts/macos-sysroot.test.mjs @@ -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 }) + } })