diff --git a/CHANGELOG.md b/CHANGELOG.md index 12b79ca1c8..32a77b1a16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Fixes + +- Load `.xcode.env` and `.xcode.env.local` in the Expo plugin's "Upload Debug Symbols to Sentry" build phase before it looks for Node, so builds started from Xcode opened in the Dock or Finder find the upload script ([#6860](https://github.com/getsentry/sentry-react-native/pull/6860)) + ## 8.30.0 ### Features diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index a9947041fe..9277869135 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -15,6 +15,13 @@ 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'; +// Same as the "Bundle React Native code and images" phase of the Expo template. +const LOAD_XCODE_ENV = `if [[ -f "$PODS_ROOT/../.xcode.env" ]]; then + source "$PODS_ROOT/../.xcode.env" +fi +if [[ -f "$PODS_ROOT/../.xcode.env.local" ]]; then + source "$PODS_ROOT/../.xcode.env.local" +fi`; export const withSentryIOS: ConfigPlugin<{ sentryProperties: string; @@ -114,7 +121,9 @@ 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}"`; + // NODE_BINARY is needed to find the script, before the script itself can load `.xcode.env`. + // Xcode launched from the Dock or Finder does not have `node` on its PATH. See issue #6859. + return `${disableAutoUploadExport}${LOAD_XCODE_ENV}\n/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..88baaa01d9 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('\n/bin/sh "`'); }); it('drops a previously injected export when disableAutoUpload is toggled back to false', () => { @@ -488,6 +494,51 @@ describe('overwriteDebugFilesUploadScript: existing phase re-quoted on upgrade ( }); }); +describe('Upload Debug Symbols phase finds Node from .xcode.env (issue #6859)', () => { + let projectRoot: string; + + // Stands in for Node: prints the path of a stub debug files script, like `node --print` does. + const writeFakeNode = (name: string): string => { + const stubScript = path.join(projectRoot, 'sentry-xcode-debug-files.sh'); + fs.writeFileSync(stubScript, `echo "ran with ${name}"\n`); + const fakeNode = path.join(projectRoot, name); + fs.writeFileSync(fakeNode, `#!/bin/sh\necho "${stubScript}"\n`, { mode: 0o755 }); + return fakeNode; + }; + + // Xcode launched from the Dock or Finder only has launchd's PATH, which has no `node`. + const runPhase = (script: string): string => + execFileSync('/bin/sh', ['-c', script], { + cwd: path.join(projectRoot, 'ios'), + env: { PATH: '/usr/bin:/bin:/usr/sbin:/sbin', PODS_ROOT: path.join(projectRoot, 'ios', 'Pods') }, + encoding: 'utf8', + }); + + beforeEach(() => { + projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'sentry-xcode-env-')); + fs.mkdirSync(path.join(projectRoot, 'ios', 'Pods'), { recursive: true }); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + it('uses NODE_BINARY from .xcode.env when node is not on the PATH', () => { + const fakeNode = writeFakeNode('node-from-xcode-env'); + fs.writeFileSync(path.join(projectRoot, 'ios', '.xcode.env'), `export NODE_BINARY="${fakeNode}"\n`); + + expect(runPhase(getDebugFilesUploadScript())).toBe('ran with node-from-xcode-env\n'); + }); + + it('prefers NODE_BINARY from .xcode.env.local over .xcode.env', () => { + fs.writeFileSync(path.join(projectRoot, 'ios', '.xcode.env'), 'export NODE_BINARY=$(command -v node)\n'); + const fakeNode = writeFakeNode('node-from-xcode-env-local'); + fs.writeFileSync(path.join(projectRoot, 'ios', '.xcode.env.local'), `export NODE_BINARY="${fakeNode}"\n`); + + expect(runPhase(getDebugFilesUploadScript(true))).toBe('ran with node-from-xcode-env-local\n'); + }); +}); + describe('Project paths with spaces are quoted (issue #6583)', () => { let consoleWarnMock: jest.SpyInstance; @@ -564,7 +615,8 @@ 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('\n/bin/sh "`'); expect(result).toContain('sentry-xcode-debug-files.sh'); expect(result).not.toMatch(/\/bin\/sh `/); });