Skip to content

feat(xcode): Dynamically resolve SDK and CLI package paths #3431

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@
"test:tools": "jest --config jest.config.tools.js",
"fix": "yarn fix:eslint && yarn fix:prettier",
"fix:eslint": "eslint --config .eslintrc.js --fix .",
"fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"",
"fix:prettier": "prettier --write \"{src,test,scripts,plugin/src}/**/**.ts\"",
"lint": "yarn lint:eslint && yarn lint:prettier",
"lint:eslint": "eslint --config .eslintrc.js .",
"lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"",
"lint:prettier": "prettier --check \"{src,test,scripts,plugin/src}/**/**.ts\"",
"test:watch": "jest --watch",
"run-ios": "cd sample-new-architecture && yarn react-native run-ios",
"run-android": "cd sample-new-architecture && yarn react-native run-android",
Expand Down
4 changes: 1 addition & 3 deletions plugin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { withSentry } from './withSentry';

export {
withSentry,
};
export { withSentry };

export default withSentry;
20 changes: 20 additions & 0 deletions plugin/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import * as fs from 'fs';
import * as path from 'path';

export function writeSentryPropertiesTo(filepath: string, sentryProperties: string): void {
if (!fs.existsSync(filepath)) {
throw new Error(`Directory '${filepath}' does not exist.`);
}

fs.writeFileSync(path.resolve(filepath, 'sentry.properties'), sentryProperties);
}

const sdkPackage: {
name: string;
version: string;
// eslint-disable-next-line @typescript-eslint/no-var-requires
} = require('../../package.json');

const SDK_PACKAGE_NAME = sdkPackage.name;

export { sdkPackage, SDK_PACKAGE_NAME };
22 changes: 9 additions & 13 deletions plugin/src/withSentry.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import type { ConfigPlugin } from 'expo/config-plugins';
import { createRunOncePlugin, WarningAggregator } from 'expo/config-plugins';

import { SDK_PACKAGE_NAME, sdkPackage } from './utils';
import { withSentryAndroid } from './withSentryAndroid';
import { withSentryIOS } from './withSentryIOS';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const pkg = require('../../package.json');

interface PluginProps {
organization?: string;
project?: string;
Expand All @@ -22,15 +20,15 @@ const withSentryPlugin: ConfigPlugin<PluginProps | void> = (config, props) => {
cfg = withSentryAndroid(cfg, sentryProperties);
} catch (e) {
WarningAggregator.addWarningAndroid(
'sentry-expo',
SDK_PACKAGE_NAME,
`There was a problem configuring sentry-expo in your native Android project: ${e}`,
);
}
try {
cfg = withSentryIOS(cfg, sentryProperties);
} catch (e) {
WarningAggregator.addWarningIOS(
'sentry-expo',
SDK_PACKAGE_NAME,
`There was a problem configuring sentry-expo in your native iOS project: ${e}`,
);
}
Expand All @@ -50,25 +48,23 @@ export function getSentryProperties(props: PluginProps | void): string | null {
if (missingProperties.length) {
const warningMessage = `Missing Sentry configuration properties: ${missingProperties.join(
', ',
)} in config plugin. Builds will fall back to environment variables. Refer to @sentry/react-native docs for how to configure this.`;
WarningAggregator.addWarningAndroid('sentry-expo', warningMessage);
WarningAggregator.addWarningIOS('sentry-expo', warningMessage);
)} in config plugin. Builds will fall back to environment variables. See: https://docs.sentry.io/platforms/react-native/manual-setup/.`;
WarningAggregator.addWarningAndroid(SDK_PACKAGE_NAME, warningMessage);
WarningAggregator.addWarningIOS(SDK_PACKAGE_NAME, warningMessage);
}

return `defaults.url=${url}
${organization ? `defaults.org=${organization}` : missingOrgMessage}
${project ? `defaults.project=${project}` : missingProjectMessage}
${
authToken
? `# Configure this value through \`SENTRY_AUTH_TOKEN\` environment variable instead. See:https://docs.expo.dev/guides/using-sentry/#app-configuration\nauth.token=${authToken}`
? `# Configure this value through \`SENTRY_AUTH_TOKEN\` environment variable instead. See: https://docs.sentry.io/platforms/react-native/manual-setup/\nauth.token=${authToken}`
: missingAuthTokenMessage
}
`;
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const withSentry = createRunOncePlugin(withSentryPlugin, pkg.name, pkg.version);
const withSentry = createRunOncePlugin(withSentryPlugin, sdkPackage.name, sdkPackage.version);

export {
withSentry,
};
export { withSentry };
18 changes: 8 additions & 10 deletions plugin/src/withSentryAndroid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { ConfigPlugin } from 'expo/config-plugins';
import { WarningAggregator, withAppBuildGradle, withDangerousMod } from 'expo/config-plugins';
import * as path from 'path';

import { writeSentryPropertiesTo } from './withSentryIOS';
import { SDK_PACKAGE_NAME, writeSentryPropertiesTo } from './utils';

export const withSentryAndroid: ConfigPlugin<string> = (config, sentryProperties: string) => {
const cfg = withAppBuildGradle(config, config => {
Expand All @@ -23,14 +23,14 @@ export const withSentryAndroid: ConfigPlugin<string> = (config, sentryProperties
};

const resolveSentryReactNativePackageJsonPath =
'["node", "--print", "require.resolve(\'@sentry/react-native/package.json\')"].execute().text.trim()';
'["node", "--print", "require(\'path\').dirname(require.resolve(\'@sentry/react-native/package.json\'))"].execute().text.trim()';

/**
* Writes to projectDirectory/android/app/build.gradle,
* adding the relevant @sentry/react-native script.
*/
export function modifyAppBuildGradle(buildGradle: string): string {
if (buildGradle.includes('/sentry.gradle"')) {
if (buildGradle.includes('sentry.gradle')) {
return buildGradle;
}

Expand All @@ -40,15 +40,13 @@ export function modifyAppBuildGradle(buildGradle: string): string {

if (!buildGradle.match(pattern)) {
WarningAggregator.addWarningAndroid(
'sentry-expo',
'Could not find react.gradle script in android/app/build.gradle. Please open a bug report at https://github.com/expo/sentry-expo.',
SDK_PACKAGE_NAME,
'Could not find `^android {` in `android/app/build.gradle`. Please open a bug report at https://github.com/getsentry/sentry-react-native.',
);
return buildGradle;
}

const sentryOptions = !buildGradle.includes('project.ext.sentryCli')
? `project.ext.sentryCli=[collectModulesScript: new File(${resolveSentryReactNativePackageJsonPath}, "../dist/js/tools/collectModules.js")]`
: '';
const applyFrom = `apply from: new File(${resolveSentryReactNativePackageJsonPath}, "../sentry.gradle")`;
const applyFrom = `apply from: new File(${resolveSentryReactNativePackageJsonPath}, "sentry.gradle")`;

return buildGradle.replace(pattern, match => `${sentryOptions}\n\n${applyFrom}\n\n${match}`);
return buildGradle.replace(pattern, match => `${applyFrom}\n\n${match}`);
}
48 changes: 21 additions & 27 deletions plugin/src/withSentryIOS.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import type { ConfigPlugin, XcodeProject } from 'expo/config-plugins';
import { WarningAggregator, withDangerousMod, withXcodeProject } from 'expo/config-plugins';
import * as fs from 'fs';
import * as path from 'path';

const SENTRY_CLI = "`node --print \"require.resolve('@sentry/cli/package.json').slice(0, -13) + '/bin/sentry-cli'\"`";
import { SDK_PACKAGE_NAME, writeSentryPropertiesTo } from './utils';

type BuildPhase = { shellScript: string };

const SENTRY_REACT_NATIVE_XCODE_PATH =
"`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"`";
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'\"`";

export const withSentryIOS: ConfigPlugin<string> = (config, sentryProperties: string) => {
const cfg = withXcodeProject(config, config => {
Expand All @@ -17,11 +23,7 @@ export const withSentryIOS: ConfigPlugin<string> = (config, sentryProperties: st
if (!sentryBuildPhase) {
xcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, {
shellPath: '/bin/sh',
shellScript: `
export SENTRY_PROPERTIES=sentry.properties
[[ $SENTRY_INCLUDE_NATIVE_SOURCES == "true" ]] && INCLUDE_SOURCES_FLAG="--include-sources" || INCLUDE_SOURCES_FLAG=""
${SENTRY_CLI} debug-files upload --force-foreground "$INCLUDE_SOURCES_FLAG" "$DWARF_DSYM_FOLDER_PATH"
`,
shellScript: `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH}`,
});
}

Expand All @@ -43,35 +45,27 @@ ${SENTRY_CLI} debug-files upload --force-foreground "$INCLUDE_SOURCES_FLAG" "$DW
]);
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function modifyExistingXcodeBuildScript(script: any): void {
export function modifyExistingXcodeBuildScript(script: BuildPhase): void {
if (
!script.shellScript.match(/(packager|scripts)\/react-native-xcode\.sh\b/) ||
script.shellScript.match(/bin\/sentry-cli.*react-native[\s-]xcode/)
script.shellScript.includes('sentry-xcode.sh') ||
script.shellScript.includes('@sentry')
) {
WarningAggregator.addWarningIOS(
'sentry-expo',
SDK_PACKAGE_NAME,
"Unable to modify build script 'Bundle React Native code and images'. Please open a bug report at https://github.com/expo/sentry-expo.",
);
return;
}
let code = JSON.parse(script.shellScript);
code = `${
'export SENTRY_PROPERTIES=sentry.properties\n' +
'export EXTRA_PACKAGER_ARGS="--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map"\n'
}${code.replace(
/^.*?(packager|scripts)\/react-native-xcode\.sh\s*(\\'\\\\")?/m,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(match: any) => `${SENTRY_CLI} react-native xcode --force-foreground ${match}`,
)}\n\n\`node --print "require.resolve('@sentry/react-native/package.json').slice(0, -13) + '/scripts/collect-modules.sh'"\``;

script.shellScript = JSON.stringify(code);
const code = JSON.parse(script.shellScript);
script.shellScript = JSON.stringify(addSentryWithBundledScriptsToBundleShellScript(code));
}

export function writeSentryPropertiesTo(filepath: string, sentryProperties: string): void {
if (!fs.existsSync(filepath)) {
throw new Error(`Directory '${filepath}' does not exist.`);
}

fs.writeFileSync(path.resolve(filepath, 'sentry.properties'), sentryProperties);
export function addSentryWithBundledScriptsToBundleShellScript(script: string): string {
return script.replace(
/^.*?(packager|scripts)\/react-native-xcode\.sh\s*(\\'\\\\")?/m,
// eslint-disable-next-line no-useless-escape
(match: string) => `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_PATH} ${match}`,
);
}
1 change: 1 addition & 0 deletions sample-new-architecture/ios/.xcode.env
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export SENTRY_CLI_EXTRA_ARGS="--force-foreground"
export SENTRY_CLI_DEBUG_FILES_UPLOAD_EXTRA_ARGS=""
export SENTRY_CLI_RN_XCODE_EXTRA_ARGS=""
export MODULES_PATHS="$PWD/../node_modules,$PWD/../../.."
export SENTRY_COLLECT_MODULES="../../scripts/collect-modules.sh"
6 changes: 5 additions & 1 deletion scripts/sentry-xcode-debug-files.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
# print commands before executing them and stop on first error
set -x -e

LOCAL_NODE_BINARY=${NODE_BINARY:-node}

# load envs if loader file exists (since rn 0.68)
WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh"
if [ -f "$WITH_ENVIRONMENT" ]; then
. "$WITH_ENVIRONMENT"
fi

[ -z "$SENTRY_PROPERTIES" ] && export SENTRY_PROPERTIES=sentry.properties
[ -z "$SENTRY_CLI_EXECUTABLE" ] && SENTRY_CLI_EXECUTABLE="../node_modules/@sentry/cli/bin/sentry-cli"

[ -z "$SENTRY_CLI_EXECUTABLE" ] && SENTRY_CLI_PACKAGE_PATH=$("$LOCAL_NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/cli/package.json'))")
[ -z "$SENTRY_CLI_EXECUTABLE" ] && SENTRY_CLI_EXECUTABLE="${SENTRY_CLI_PACKAGE_PATH}/bin/sentry-cli"

[[ $SENTRY_INCLUDE_NATIVE_SOURCES == "true" ]] && INCLUDE_SOURCES_FLAG="--include-sources" || INCLUDE_SOURCES_FLAG=""

Expand Down
9 changes: 7 additions & 2 deletions scripts/sentry-xcode.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ set -x -e

# WITH_ENVIRONMENT is executed by React Native

LOCAL_NODE_BINARY=${NODE_BINARY:-node}

[ -z "$SENTRY_PROPERTIES" ] && export SENTRY_PROPERTIES=sentry.properties
[ -z "$SOURCEMAP_FILE" ] && export SOURCEMAP_FILE="$DERIVED_FILE_DIR/main.jsbundle.map"
[ -z "$SENTRY_CLI_EXECUTABLE" ] && SENTRY_CLI_EXECUTABLE="../node_modules/@sentry/cli/bin/sentry-cli"

[ -z "$SENTRY_CLI_EXECUTABLE" ] && SENTRY_CLI_PACKAGE_PATH=$("$LOCAL_NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/cli/package.json'))")
[ -z "$SENTRY_CLI_EXECUTABLE" ] && SENTRY_CLI_EXECUTABLE="${SENTRY_CLI_PACKAGE_PATH}/bin/sentry-cli"

REACT_NATIVE_XCODE=$1

Expand All @@ -25,7 +29,8 @@ else
/bin/sh -c "$REACT_NATIVE_XCODE"
fi

[ -z "$SENTRY_COLLECT_MODULES" ] && SENTRY_COLLECT_MODULES="../../scripts/collect-modules.sh"
[ -z "$SENTRY_COLLECT_MODULES" ] && SENTRY_RN_PACKAGE_PATH=$("$LOCAL_NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/react-native/package.json'))")
[ -z "$SENTRY_COLLECT_MODULES" ] && SENTRY_COLLECT_MODULES="${SENTRY_RN_PACKAGE_PATH}/scripts/collect-modules.sh"

if [ -f "$SENTRY_COLLECT_MODULES" ]; then
/bin/sh "$SENTRY_COLLECT_MODULES"
Expand Down
12 changes: 6 additions & 6 deletions test/expo-plugin/modifyAppBuildGradle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jest.mock('@expo/config-plugins', () => {
});

const buildGradleWithSentry = `
apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle")
apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle")

android {
}
Expand All @@ -23,7 +23,7 @@ android {
`;

const monoRepoBuildGradleWithSentry = `
apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle")
apply from: new File(["node", "--print", "require('path').dirname(require.resolve('@sentry/react-native/package.json'))"].execute().text.trim(), "sentry.gradle")

android {
}
Expand All @@ -39,19 +39,19 @@ const buildGradleWithOutReactGradleScript = `

describe('Configures Android native project correctly', () => {
it("Non monorepo: Doesn't modify app/build.gradle if Sentry's already configured", () => {
expect(modifyAppBuildGradle(buildGradleWithSentry)).toMatch(buildGradleWithSentry);
expect(modifyAppBuildGradle(buildGradleWithSentry)).toStrictEqual(buildGradleWithSentry);
});

it('Non monorepo: Adds sentry.gradle script if not present already', () => {
expect(modifyAppBuildGradle(buildGradleWithOutSentry)).toMatch(buildGradleWithSentry);
expect(modifyAppBuildGradle(buildGradleWithOutSentry)).toStrictEqual(buildGradleWithSentry);
});

it("Monorepo: Doesn't modify app/build.gradle if Sentry's already configured", () => {
expect(modifyAppBuildGradle(monoRepoBuildGradleWithSentry)).toMatch(monoRepoBuildGradleWithSentry);
expect(modifyAppBuildGradle(monoRepoBuildGradleWithSentry)).toStrictEqual(monoRepoBuildGradleWithSentry);
});

it('Monorepo: Adds sentry.gradle script if not present already', () => {
expect(modifyAppBuildGradle(monoRepoBuildGradleWithOutSentry)).toMatch(monoRepoBuildGradleWithSentry);
expect(modifyAppBuildGradle(monoRepoBuildGradleWithOutSentry)).toStrictEqual(monoRepoBuildGradleWithSentry);
});

it('Warns to file a bug report if no react.gradle is found', () => {
Expand Down
31 changes: 20 additions & 11 deletions test/expo-plugin/modifyXcodeProject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,31 @@ jest.mock('@expo/config-plugins', () => {
});

const buildScriptWithoutSentry = {
shellScript: '"export NODE_BINARY=node\\n../node_modules/react-native/scripts/react-native-xcode.sh"',
shellScript: JSON.stringify(`"
export NODE_BINARY=node
../node_modules/react-native/scripts/react-native-xcode.sh
"`),
};

const buildScriptWithSentry = {
shellScript:
'"export SENTRY_PROPERTIES=sentry.properties\\nexport EXTRA_PACKAGER_ARGS=\\"--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map\\"\\nexport NODE_BINARY=node\\n`node --print \\"require.resolve(\'@sentry/cli/package.json\').slice(0, -13) + \'/bin/sentry-cli\'\\"` react-native xcode --force-foreground ../node_modules/react-native/scripts/react-native-xcode.sh\\n\\n`node --print \\"require.resolve(\'@sentry/react-native/package.json\').slice(0, -13) + \'/scripts/collect-modules.sh\'\\"`"',
shellScript: JSON.stringify(`"
export NODE_BINARY=node
/bin/sh \`"$NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'"\` ../node_modules/react-native/scripts/react-native-xcode.sh
"`),
};

const monorepoBuildScriptWithoutSentry = {
shellScript:
'"export NODE_BINARY=node\\n`node --print \\"require.resolve(\'react-native/package.json\').slice(0, -13) + \'/scripts/react-native-xcode.sh\'\\"`"',
shellScript: JSON.stringify(`"
export NODE_BINARY=node
\`node --print "require.resolve('react-native/package.json').slice(0, -13) + '/scripts/react-native-xcode.sh'"\`
"`),
};

const monorepoBuildScriptWithSentry = {
shellScript:
"\"export SENTRY_PROPERTIES=sentry.properties\\nexport EXTRA_PACKAGER_ARGS=\\\"--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map\\\"\\nexport NODE_BINARY=node\\n`node --print \\\"require.resolve('@sentry/cli/package.json').slice(0, -13) + '/bin/sentry-cli'\\\"` react-native xcode --force-foreground `node --print \\\"require.resolve('react-native/package.json').slice(0, -13) + '/scripts/react-native-xcode.sh'\\\"`\\n\\n`node --print \\\"require.resolve('@sentry/react-native/package.json').slice(0, -13) + '/scripts/collect-modules.sh'\\\"`\"",
shellScript: JSON.stringify(`"
export NODE_BINARY=node
/bin/sh \`"$NODE_BINARY" --print "require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'"\` \`node --print "require.resolve('react-native/package.json').slice(0, -13) + '/scripts/react-native-xcode.sh'"\`
"`),
};

const buildScriptWeDontExpect = {
Expand All @@ -49,25 +58,25 @@ describe('Configures iOS native project correctly', () => {
it("Doesn't modify build script if Sentry's already configured", () => {
const script = Object.assign({}, buildScriptWithSentry);
modifyExistingXcodeBuildScript(script);
expect(script).toStrictEqual(buildScriptWithSentry);
expect(JSON.parse(script.shellScript)).toStrictEqual(JSON.parse(buildScriptWithSentry.shellScript));
});

it("Add Sentry configuration to 'Bundle React Native Code' build script", () => {
const script = Object.assign({}, buildScriptWithoutSentry);
modifyExistingXcodeBuildScript(script);
expect(script).toStrictEqual(buildScriptWithSentry);
expect(JSON.parse(script.shellScript)).toStrictEqual(JSON.parse(buildScriptWithSentry.shellScript));
});

it("Monorepo: doesn't modify build script if Sentry's already configured", () => {
const script = Object.assign({}, monorepoBuildScriptWithSentry);
modifyExistingXcodeBuildScript(script);
expect(script).toStrictEqual(monorepoBuildScriptWithSentry);
expect(JSON.parse(script.shellScript)).toStrictEqual(JSON.parse(monorepoBuildScriptWithSentry.shellScript));
});

it("Monorepo: add Sentry configuration to 'Bundle React Native Code' build script", () => {
const script = Object.assign({}, monorepoBuildScriptWithoutSentry);
modifyExistingXcodeBuildScript(script);
expect(script).toStrictEqual(monorepoBuildScriptWithSentry);
expect(JSON.parse(script.shellScript)).toStrictEqual(JSON.parse(monorepoBuildScriptWithSentry.shellScript));
});

it("Warns to file a bug report if build script isn't what we expect to find", () => {
Expand Down