diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b79ca1c8..0950576e28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Fixes +- Load `ios/.xcode.env` and `ios/.xcode.env.local` in the Expo plugin "Upload Debug Symbols to Sentry" phase before it runs Node, so the phase finds Node when Xcode starts from the Dock ([#6861](https://github.com/getsentry/sentry-react-native/pull/6861)) - Preserve Hermes internal bytecode frames as non-application frames when rewriting Expo stack traces ([#6848](https://github.com/getsentry/sentry-react-native/pull/6848)) - Fix stale `turbo_module.*` tags on Android native crashes ([#6823](https://github.com/getsentry/sentry-react-native/pull/6823)) - Forward image attachments to Spotlight instead of stripping them from the envelope ([#6818](https://github.com/getsentry/sentry-react-native/pull/6818)) diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index a9947041fe..6301fad754 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -15,6 +15,11 @@ const SENTRY_REACT_NATIVE_XCODE_PATH = const SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH = "`${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; const SENTRY_DISABLE_AUTO_UPLOAD_EXPORT = 'export SENTRY_DISABLE_AUTO_UPLOAD=true'; +// Xcode started from the Dock has no node on PATH, so load NODE_BINARY the same way as with-environment.sh. +const LOAD_XCODE_ENV = `XCODE_ENV_PATH="\${PODFILE_DIR:-$PODS_ROOT/..}/.xcode.env" +[ -f "$XCODE_ENV_PATH" ] && . "$XCODE_ENV_PATH" +[ -f "$XCODE_ENV_PATH.local" ] && . "$XCODE_ENV_PATH.local" +`; export const withSentryIOS: ConfigPlugin<{ sentryProperties: string; @@ -114,7 +119,7 @@ export function getDebugFilesUploadScript(disableAutoUpload: boolean = false): s // The resolved script path is wrapped in double quotes so a project path containing spaces is // passed to `/bin/sh` as a single argument instead of being word-split. See issue #6583. const disableAutoUploadExport = disableAutoUpload ? `${SENTRY_DISABLE_AUTO_UPLOAD_EXPORT}\n` : ''; - return `${disableAutoUploadExport}/bin/sh "${SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH}"`; + return `${disableAutoUploadExport}${LOAD_XCODE_ENV}/bin/sh "${SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH}"`; } export function requoteExistingSentryBundleScript(script: BuildPhase): void { diff --git a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts index ce5bc4e97c..96d8c978b5 100644 --- a/packages/core/test/expo-plugin/modifyXcodeProject.test.ts +++ b/packages/core/test/expo-plugin/modifyXcodeProject.test.ts @@ -1,3 +1,8 @@ +import { execFileSync } from 'child_process'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + import { warnOnce } from '../../plugin/src/logger'; import { addDisableAutoUploadToExistingScript, @@ -470,7 +475,8 @@ describe('overwriteDebugFilesUploadScript: existing phase re-quoted on upgrade ( overwriteDebugFilesUploadScript(script, true); const parsed = JSON.parse(script.shellScript); expect(parsed).toBe(getDebugFilesUploadScript(true)); - expect(parsed).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n\/bin\/sh "`/); + expect(parsed).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/); + expect(parsed).toContain('/bin/sh "`'); }); it('drops a previously injected export when disableAutoUpload is toggled back to false', () => { @@ -564,8 +570,62 @@ export NODE_BINARY=node it('quotes the debug files upload script path with disableAutoUpload', () => { const result = getDebugFilesUploadScript(true); - expect(result).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n\/bin\/sh "`/); + expect(result).toMatch(/^export SENTRY_DISABLE_AUTO_UPLOAD=true\n/); + expect(result).toContain('/bin/sh "`'); expect(result).toContain('sentry-xcode-debug-files.sh'); expect(result).not.toMatch(/\/bin\/sh `/); }); }); + +describe('Upload Debug Symbols phase loads .xcode.env before it resolves the script (issue #6859)', () => { + let projectDir: string; + let iosDir: string; + + beforeEach(() => { + projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-expo-phase-')); + iosDir = path.join(projectDir, 'ios'); + const sentryDir = path.join(projectDir, 'node_modules', '@sentry', 'react-native'); + fs.mkdirSync(path.join(sentryDir, 'scripts'), { recursive: true }); + fs.mkdirSync(path.join(iosDir, 'Pods'), { recursive: true }); + fs.writeFileSync(path.join(sentryDir, 'package.json'), '{"name":"@sentry/react-native"}'); + fs.writeFileSync( + path.join(sentryDir, 'scripts', 'sentry-xcode-debug-files.sh'), + 'echo "debug files script ran with SENTRY_DISABLE_AUTO_UPLOAD=$SENTRY_DISABLE_AUTO_UPLOAD"\n', + ); + fs.writeFileSync(path.join(iosDir, '.xcode.env'), 'export NODE_BINARY=$(command -v node)\n'); + }); + + afterEach(() => { + fs.rmSync(projectDir, { recursive: true, force: true }); + }); + + const runPhase = (script: string, env: Record): string => + execFileSync('/bin/sh', ['-c', script], { + cwd: iosDir, + env: { HOME: os.homedir(), PATH: '/usr/bin:/bin:/usr/sbin:/sbin', ...env }, + encoding: 'utf8', + stdio: 'pipe', + }); + + it('includes the .xcode.env loading before the script path is resolved', () => { + const result = getDebugFilesUploadScript(); + expect(result.indexOf('.xcode.env')).toBeLessThan(result.indexOf('--print')); + expect(result).toContain('. "$XCODE_ENV_PATH.local"'); + }); + + it('uses NODE_BINARY from .xcode.env.local when node is not on PATH', () => { + fs.writeFileSync(path.join(iosDir, '.xcode.env.local'), `export NODE_BINARY="${process.execPath}"\n`); + + const stdout = runPhase(getDebugFilesUploadScript(true), { PODS_ROOT: path.join(iosDir, 'Pods') }); + + expect(stdout).toContain('debug files script ran with SENTRY_DISABLE_AUTO_UPLOAD=true'); + }); + + it('uses PODFILE_DIR before PODS_ROOT to find .xcode.env', () => { + fs.writeFileSync(path.join(iosDir, '.xcode.env.local'), `export NODE_BINARY="${process.execPath}"\n`); + + const stdout = runPhase(getDebugFilesUploadScript(), { PODFILE_DIR: iosDir, PODS_ROOT: '/nonexistent' }); + + expect(stdout).toContain('debug files script ran'); + }); +});