Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
});
```

### Fixes

- Fix race condition where iOS dSYM upload runs before debug symbols are fully generated ([#5653](https://github.com/getsentry/sentry-react-native/pull/5653))

## 8.0.0

### Upgrading from 7.x to 8.0
Expand Down
143 changes: 143 additions & 0 deletions packages/core/scripts/sentry-xcode-debug-files.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#!/bin/bash
# Upload Debug Symbols to Sentry Xcode Build Phase
# PWD=ios
#
# Configuration: See BUILD_CONFIGURATION.md for all available environment variables

# print commands before executing them
set -x
Expand Down Expand Up @@ -58,6 +60,141 @@ EXTRA_ARGS="$SENTRY_CLI_EXTRA_ARGS $SENTRY_CLI_DEBUG_FILES_UPLOAD_EXTRA_ARGS $IN

UPLOAD_DEBUG_FILES="\"$SENTRY_CLI_EXECUTABLE\" debug-files upload $EXTRA_ARGS \"$DWARF_DSYM_FOLDER_PATH\""

# Function to wait for dSYM files to be generated
# This addresses a race condition where the upload script runs before dSYM generation completes
wait_for_dsym_files() {
local max_attempts="${SENTRY_DSYM_WAIT_MAX_ATTEMPTS:-10}"
local wait_interval="${SENTRY_DSYM_WAIT_INTERVAL:-2}"
local attempt=1
local total_wait_time=0

# Check if we should wait for dSYM files
if [ "$SENTRY_DSYM_WAIT_ENABLED" == "false" ]; then
echo "SENTRY_DSYM_WAIT_ENABLED=false, skipping dSYM wait check"
return 0
fi

# Warn if DWARF_DSYM_FILE_NAME is not set - we can't verify the main app dSYM
if [ -z "$DWARF_DSYM_FILE_NAME" ]; then
echo "warning: DWARF_DSYM_FILE_NAME not set, cannot verify main app dSYM specifically"
echo "warning: Will proceed when any dSYM bundle is found"
fi

echo "Checking for dSYM files in: $DWARF_DSYM_FOLDER_PATH"

# Debug information to help diagnose issues
if [ -n "${SENTRY_DSYM_DEBUG}" ]; then
echo "DEBUG: DWARF_DSYM_FOLDER_PATH=$DWARF_DSYM_FOLDER_PATH"
echo "DEBUG: DWARF_DSYM_FILE_NAME=$DWARF_DSYM_FILE_NAME"
echo "DEBUG: PRODUCT_NAME=$PRODUCT_NAME"
if [ -d "$DWARF_DSYM_FOLDER_PATH" ]; then
echo "DEBUG: Contents of dSYM folder:"
ls -la "$DWARF_DSYM_FOLDER_PATH" 2>/dev/null || echo "Cannot list folder"
else
echo "DEBUG: dSYM folder does not exist yet"
fi
fi

while [ $attempt -le $max_attempts ]; do
# Check if the dSYM folder exists
if [ -d "$DWARF_DSYM_FOLDER_PATH" ]; then
# Check if there are any .dSYM bundles in the folder
local dsym_count=$(find "$DWARF_DSYM_FOLDER_PATH" -name "*.dSYM" -type d 2>/dev/null | wc -l | tr -d ' ')

if [ "$dsym_count" -gt 0 ]; then
echo "Found $dsym_count dSYM bundle(s) in $DWARF_DSYM_FOLDER_PATH"

# If DWARF_DSYM_FILE_NAME is set, verify the main app dSYM exists and is complete
if [ -n "$DWARF_DSYM_FILE_NAME" ]; then
local main_dsym="$DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME"

if [ -d "$main_dsym" ]; then
# Directory exists, now verify the actual DWARF binary exists inside
local dwarf_dir="$main_dsym/Contents/Resources/DWARF"

if [ -d "$dwarf_dir" ]; then
# Check if there are any files in the DWARF directory
local dwarf_files=$(find "$dwarf_dir" -type f 2>/dev/null | head -1)

if [ -n "$dwarf_files" ]; then
# Verify the DWARF file is not empty (still being written)
local dwarf_size=$(find "$dwarf_dir" -type f -size +0 2>/dev/null | head -1)

if [ -n "$dwarf_size" ]; then
echo "Verified main app dSYM is complete: $DWARF_DSYM_FILE_NAME"
return 0
else
echo "Main app dSYM DWARF binary is empty (still being written): $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)"
fi
else
echo "Main app dSYM DWARF directory is empty: $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)"
fi
else
echo "Main app dSYM structure incomplete (missing DWARF directory): $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)"
fi
else
echo "Main app dSYM not found yet: $DWARF_DSYM_FILE_NAME (attempt $attempt/$max_attempts)"
fi
else
# DWARF_DSYM_FILE_NAME not set, check if any dSYM has valid DWARF content
# This is less strict but better than nothing
local has_valid_dsym=false
for dsym in "$DWARF_DSYM_FOLDER_PATH"/*.dSYM; do
if [ -d "$dsym/Contents/Resources/DWARF" ]; then
local dwarf_files=$(find "$dsym/Contents/Resources/DWARF" -type f -size +0 2>/dev/null | head -1)
if [ -n "$dwarf_files" ]; then
has_valid_dsym=true
break
fi
fi
done

if [ "$has_valid_dsym" = true ]; then
echo "Found dSYM bundle(s) with valid DWARF content"
return 0
else
echo "Found dSYM bundle(s) but none have complete DWARF content yet (attempt $attempt/$max_attempts)"
fi
fi
else
echo "No dSYM bundles found yet in $DWARF_DSYM_FOLDER_PATH (attempt $attempt/$max_attempts)"
fi
else
echo "dSYM folder does not exist yet: $DWARF_DSYM_FOLDER_PATH (attempt $attempt/$max_attempts)"
fi

if [ $attempt -lt $max_attempts ]; then
# Progressive backoff: quick checks first, longer waits later
# Attempts 1-3: 0.5s (total 1.5s)
# Attempts 4-6: 1s (total 3s)
# Attempts 7+: 2s (remaining time)
local current_interval="$wait_interval"
if [ -z "${SENTRY_DSYM_WAIT_INTERVAL}" ]; then
# Only use progressive intervals if user hasn't set custom interval
if [ $attempt -le 3 ]; then
current_interval=0.5
elif [ $attempt -le 6 ]; then
current_interval=1
else
current_interval=2
fi
fi

echo "Waiting ${current_interval}s for dSYM generation to complete..."
sleep $current_interval
total_wait_time=$(awk "BEGIN {print $total_wait_time + $current_interval}")
fi

attempt=$((attempt + 1))
done

# Timeout reached
echo "warning: Timeout waiting for dSYM files after ${total_wait_time}s ($max_attempts attempts)"
echo "warning: This may result in incomplete debug symbol uploads"
echo "warning: To disable this check, set SENTRY_DSYM_WAIT_ENABLED=false"
return 1
}

XCODE_BUILD_CONFIGURATION="${CONFIGURATION}"

if [ "$SENTRY_DISABLE_AUTO_UPLOAD" == true ]; then
Expand All @@ -67,6 +204,12 @@ elif [ "$SENTRY_DISABLE_XCODE_DEBUG_UPLOAD" == true ]; then
elif echo "$XCODE_BUILD_CONFIGURATION" | grep -iq "debug"; then # case insensitive check for "debug"
echo "Skipping debug files upload for *Debug* configuration"
else
# Wait for dSYM files to be generated (addresses race condition in EAS builds)
# Don't fail the script if wait times out - we still want to attempt upload
set +e
wait_for_dsym_files
set -e

# 'warning:' triggers a warning in Xcode, 'error:' triggers an error
set +x +e # disable printing commands otherwise we might print `error:` by accident and allow continuing on error
SENTRY_UPLOAD_COMMAND_OUTPUT=$(/bin/sh -c "\"$LOCAL_NODE_BINARY\" $UPLOAD_DEBUG_FILES" 2>&1)
Expand Down
107 changes: 107 additions & 0 deletions packages/core/test/expo-plugin/modifyXcodeProject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,110 @@ describe('Configures iOS native project correctly', () => {
expect(warnOnce).toHaveBeenCalled();
});
});

describe('Upload Debug Symbols to Sentry build phase', () => {
let mockXcodeProject: any;
let addBuildPhaseSpy: jest.Mock;
const expectedShellScript =
"/bin/sh `${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`";

const getOptions = () => {
const callArgs = addBuildPhaseSpy.mock.calls[0];
return callArgs[4];
};

beforeEach(() => {
addBuildPhaseSpy = jest.fn();
mockXcodeProject = {
pbxItemByComment: jest.fn().mockReturnValue(null),
addBuildPhase: addBuildPhaseSpy,
};
});

afterEach(() => {
jest.clearAllMocks();
});

it('creates Upload Debug Symbols build phase with correct shell script', () => {
mockXcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, {
shellPath: '/bin/sh',
shellScript: expectedShellScript,
});

expect(addBuildPhaseSpy).toHaveBeenCalledWith(
[],
'PBXShellScriptBuildPhase',
'Upload Debug Symbols to Sentry',
null,
{
shellPath: '/bin/sh',
shellScript: expectedShellScript,
},
);
});

it('does not include inputPaths to avoid circular dependency', () => {
// We don't use inputPaths because they cause circular dependency errors in Xcode 15+
// (see issue #5641). Instead, the bash script waits for dSYM files to be generated.
mockXcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, {
shellPath: '/bin/sh',
shellScript: expectedShellScript,
});

const options = getOptions();

expect(options.inputPaths).toBeUndefined();
});

it('skips creating build phase if it already exists', () => {
mockXcodeProject.pbxItemByComment = jest.fn().mockReturnValue({
shellScript: 'existing',
});

expect(addBuildPhaseSpy).not.toHaveBeenCalled();
});

describe('Race condition handling', () => {
it('documents why we do not use inputPaths', () => {
// This test documents the decision NOT to use inputPaths.
//
// ISSUE #5288: Race condition where upload script runs before dSYM generation completes
// ISSUE #5641: inputPaths cause circular dependency errors in Xcode 15+
//
// We attempted to fix #5288 by adding inputPaths to declare dependency on dSYM files:
// inputPaths: [
// '"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Resources/DWARF/$(PRODUCT_NAME)"',
// '"$(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)"',
// ]
//
// However, this caused Xcode 15+ to fail with:
// "Cycle inside X; building could produce unreliable results"
//
// The cycle occurs because:
// 1. The target produces the dSYM as an output during linking
// 2. The "Upload Debug Symbols" build phase (part of the same target) declares the dSYM as an input
// 3. Xcode detects: target depends on its own output = CYCLE
//
// SOLUTION: Instead of using inputPaths, the bash script (sentry-xcode-debug-files.sh)
// now waits for dSYM files to exist before uploading. This avoids the circular dependency
// while still handling the race condition.
//
// See:
// - https://github.com/getsentry/sentry-react-native/issues/5288
// - https://github.com/getsentry/sentry-react-native/issues/5641
// - https://developer.apple.com/forums/thread/730974

mockXcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, {
shellPath: '/bin/sh',
shellScript: expectedShellScript,
});

const options = getOptions();

// Verify that inputPaths are NOT used
expect(options.inputPaths).toBeUndefined();
expect(options.shellPath).toBe('/bin/sh');
expect(options.shellScript).toBe(expectedShellScript);
});
});
});
Loading
Loading