Skip to content
Merged
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
14 changes: 12 additions & 2 deletions docs/src/fragments/commands/debug-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ sentry debug-files upload ./build --no-zips
sentry debug-files upload ./dsyms --type dsym --wait
sentry debug-files upload ./build --id <debug-id> --require-all

# Unity: upload IL2CPP line mappings (optionally with referenced C# sources)
sentry debug-files upload ./build --il2cpp-mapping
sentry debug-files upload ./build --il2cpp-mapping --include-sources

# Preview what would be uploaded without uploading (no credentials needed)
sentry debug-files upload ./build --no-upload
```
Expand Down Expand Up @@ -72,8 +76,14 @@ sentry debug-files upload ./build --no-upload
server-side processing and exit non-zero if any file fails. `--require-all`
fails if a requested `--id` was not found. The server-advertised maximum file
size and maximum processing wait are honored automatically (oversized files
are skipped with a warning). `--symbol-maps` (BCSymbolMap resolution) and
`--il2cpp-mapping` line mappings are not yet supported.
are skipped with a warning). `--symbol-maps` (BCSymbolMap resolution) is not
yet supported.
- Managed .NET PE assemblies that embed a Portable PDB have it extracted and
uploaded automatically as a separate `<name>.pdb` debug file (no flag needed).
- `--il2cpp-mapping` computes Unity IL2CPP C++→C# line mappings from each file's
referenced generated C++ sources and uploads them as separate `il2cpp` debug
files. Combine with `--include-sources` to also bundle the referenced C#
source files.
- Upload a JVM bundle separately via `sentry debug-files upload --type jvm`.
- Supported JVM source file extensions: `.java`, `.kt`, `.scala`, `.sc`,
`.groovy`, `.gvy`, `.gy`, `.gsh`, `.clj`, `.cljc`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Upload debug information files to Sentry
- `--no-unwind - Do not upload files whose only feature is unwind info`
- `--no-sources - Do not upload files whose only feature is source info`
- `--include-sources - Build and upload a source bundle for each file with debug info`
- `--il2cpp-mapping - Compute and upload Unity IL2CPP line mappings for each scanned file`
- `--derived-data - Also scan Xcode's DerivedData folder (macOS only)`
- `--no-zips - Do not scan inside .zip archives`
- `--no-upload - Scan and print what would be uploaded without uploading`
Expand Down Expand Up @@ -90,6 +91,10 @@ sentry debug-files upload ./build --no-zips
sentry debug-files upload ./dsyms --type dsym --wait
sentry debug-files upload ./build --id <debug-id> --require-all

# Unity: upload IL2CPP line mappings (optionally with referenced C# sources)
sentry debug-files upload ./build --il2cpp-mapping
sentry debug-files upload ./build --il2cpp-mapping --include-sources

# Preview what would be uploaded without uploading (no credentials needed)
sentry debug-files upload ./build --no-upload
```
Expand Down
103 changes: 82 additions & 21 deletions src/commands/debug-files/upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
* and `max_wait` (clamps the processing wait). `.zip` archives are scanned in
* place (disable with `--no-zips`); `--derived-data` additionally scans Xcode's
* DerivedData folder on macOS. Managed PE assemblies that embed a Portable PDB
* have it extracted and uploaded automatically as a separate DIF. `--symbol-maps`
* (BCSymbolMap resolution) and `--il2cpp-mapping` line mappings are deferred to
* follow-up PRs (see the command's full description).
* have it extracted and uploaded automatically as a separate DIF, and
* `--il2cpp-mapping` uploads Unity IL2CPP line mappings. `--symbol-maps`
* (BCSymbolMap resolution) is deferred to a follow-up PR (see the command's
* full description).
*/

import { createHash } from "node:crypto";
Expand All @@ -34,7 +35,10 @@ import {
uploadDebugFiles,
} from "../../lib/api/debug-files.js";
import { buildCommand } from "../../lib/command.js";
import { createSourceBundle } from "../../lib/dif/index.js";
import {
createIl2cppLineMapping,
createSourceBundle,
} from "../../lib/dif/index.js";
import {
buildDifFilters,
debugIdMatches,
Expand Down Expand Up @@ -127,6 +131,7 @@ type UploadFlags = {
"no-unwind"?: boolean;
"no-sources"?: boolean;
"include-sources"?: boolean;
"il2cpp-mapping"?: boolean;
"derived-data"?: boolean;
"no-zips"?: boolean;
"no-upload"?: boolean;
Expand Down Expand Up @@ -171,8 +176,54 @@ function difKey(dif: DebugFileUpload): string {
}

/**
* Convert prepared files into the DIF upload list, optionally appending a
* source bundle per file when `--include-sources` is set.
* Read a source file from disk for source-bundle/IL2CPP resolution, returning
* `null` (and logging at debug level) when it is not available locally.
*/
function readSourceFile(sourcePath: string): Uint8Array | null {
try {
return readFileSync(sourcePath);
} catch (err) {
log.debug(`Source file not available, skipping: ${sourcePath}`, err);
return null;
}
}

/**
* Compute a Unity IL2CPP line mapping for a prepared file and, when non-empty,
* append it as a separate `il2cpp` DIF carrying the file's debug id.
*
* The generated C++ source files the object references are read from disk;
* files not present locally are skipped. Failures (or a file with no IL2CPP
* data) are swallowed (logged at debug level) so they never abort the upload.
*/
function appendIl2cppMapping(difs: DebugFileUpload[], file: PreparedDif): void {
let result: ReturnType<typeof createIl2cppLineMapping>;
try {
result = createIl2cppLineMapping(
new Uint8Array(file.content),
readSourceFile
);
} catch (err) {
log.debug(`Could not compute IL2CPP line mapping for ${file.path}`, err);
return;
}
if (!result) {
return;
}
difs.push({
name: `${basename(file.path)}.il2cpp`,
debugId: file.debugId ?? result.debugId,
content: Buffer.from(result.mapping),
});
}

/**
* Convert prepared files into the DIF upload list.
*
* For each prepared file this queues the main DIF, then optionally a Unity
* IL2CPP line-mapping DIF (`--il2cpp-mapping`) and a per-file source bundle
* (`--include-sources`). Combining both also bundles the C# sources referenced
* by IL2CPP `source_info` markers (`collectIl2cppSources`).
*
* Embedded Portable PDBs (extracted from managed PE assemblies during scanning)
* arrive here as ordinary prepared DIFs, so no special handling is needed.
Expand All @@ -183,8 +234,9 @@ function difKey(dif: DebugFileUpload): string {
*/
function buildDifList(
prepared: PreparedDif[],
includeSources: boolean
options: { includeSources: boolean; il2cppMapping: boolean }
): DebugFileUpload[] {
const { includeSources, il2cppMapping } = options;
const difs: DebugFileUpload[] = [];
for (const file of prepared) {
difs.push({
Expand All @@ -193,6 +245,10 @@ function buildDifList(
content: file.content,
});

if (il2cppMapping) {
appendIl2cppMapping(difs, file);
}

if (!includeSources) {
continue;
}
Expand All @@ -202,17 +258,8 @@ function buildDifList(
result = createSourceBundle(
new Uint8Array(file.content),
basename(file.path),
(sourcePath) => {
try {
return readFileSync(sourcePath);
} catch (err) {
log.debug(
`Source file not available, skipping: ${sourcePath}`,
err
);
return null;
}
}
readSourceFile,
{ collectIl2cppSources: il2cppMapping }
);
} catch (err) {
log.debug(`Could not build source bundle for ${file.path}`, err);
Expand Down Expand Up @@ -460,15 +507,19 @@ export const uploadCommand = buildCommand({
"recursed.\n\n" +
"Managed PE assemblies (.NET) that embed a Portable PDB have it extracted " +
"and uploaded automatically as a separate <name>.pdb debug file.\n\n" +
"With --il2cpp-mapping, Unity IL2CPP C++->C# line mappings are computed " +
"from each file's referenced generated C++ sources and uploaded as " +
"separate il2cpp debug files; combine with --include-sources to also " +
"bundle the referenced C# source files.\n\n" +
"Usage:\n" +
" sentry debug-files upload ./build\n" +
" sentry debug-files upload ./symbols.zip\n" +
" sentry debug-files upload ./libexample.so --include-sources\n" +
" sentry debug-files upload ./dsyms --type dsym --wait\n" +
" sentry debug-files upload ./build --il2cpp-mapping --include-sources\n" +
" sentry debug-files upload --derived-data --no-upload\n" +
" sentry debug-files upload ./build --no-zips --no-upload\n\n" +
"Not yet supported (planned): --symbol-maps (BCSymbolMap resolution) " +
"and --il2cpp-mapping line mappings.",
"Not yet supported (planned): --symbol-maps (BCSymbolMap resolution).",
},
output: {
human: formatUploadResult,
Expand Down Expand Up @@ -529,6 +580,13 @@ export const uploadCommand = buildCommand({
optional: true,
default: false,
},
"il2cpp-mapping": {
kind: "boolean",
brief:
"Compute and upload Unity IL2CPP line mappings for each scanned file",
optional: true,
default: false,
},
"derived-data": {
kind: "boolean",
brief: "Also scan Xcode's DerivedData folder (macOS only)",
Expand Down Expand Up @@ -610,7 +668,10 @@ export const uploadCommand = buildCommand({
scanZips: !flags["no-zips"],
});
const difs = dedupeDifs(
buildDifList(prepared, Boolean(flags["include-sources"]))
buildDifList(prepared, {
includeSources: Boolean(flags["include-sources"]),
il2cppMapping: Boolean(flags["il2cpp-mapping"]),
})
);
const missingIds = missingRequestedIds(flags.id, prepared);

Expand Down
68 changes: 66 additions & 2 deletions src/lib/dif/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@

import { existsSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { Archive, initSync, SourceBundleWriter } from "@sentry/symbolic";
import {
Archive,
il2cppLineMapping,
initSync,
SourceBundleWriter,
} from "@sentry/symbolic";
import { logger } from "../logger.js";

const log = logger.withTag("dif");
Expand Down Expand Up @@ -259,14 +264,19 @@ export type SourceBundleResult = {
* @param objectName - Name stamped on the bundle (typically the input file name).
* @param readSource - Supplies source content for a referenced path, or `null` to skip.
* Invoked synchronously, so it must read synchronously (e.g. `readFileSync`).
* @param options - Optional behavior flags.
* @param options.collectIl2cppSources - When `true`, also include the C# source
* files referenced by Unity IL2CPP `source_info` markers in the object's
* generated C++ (used together with `--il2cpp-mapping`).
* @returns The bundle bytes (or `null` if empty), the object's debug id, and the
* number of files included.
* @throws If the buffer cannot be parsed, or if `readSource` throws.
*/
export function createSourceBundle(
data: Uint8Array,
objectName: string,
readSource: (path: string) => Uint8Array | null
readSource: (path: string) => Uint8Array | null,
options?: { collectIl2cppSources?: boolean }
): SourceBundleResult {
ensureInitialized();
const archive = new Archive(data);
Expand All @@ -279,6 +289,10 @@ export function createSourceBundle(

let fileCount = 0;
const writer = new SourceBundleWriter();
// Must be set before the one-shot writeObject() consumes the writer.
if (options?.collectIl2cppSources) {
writer.collectIl2cppSources = true;
}
// The filter runs before each file; we include everything the object
// references and let the provider decide availability (returning null skips).
const filter = (_path: string): boolean => true;
Expand All @@ -295,6 +309,56 @@ export function createSourceBundle(
return { bundle, debugId: object.debugId, fileCount, objectCount };
}

/** Result of computing a Unity IL2CPP line mapping for a debug information file. */
export type Il2cppMappingResult = {
/** The serialized IL2CPP C++→C# line-mapping JSON document bytes. */
mapping: Uint8Array;
/** Debug id of the object the mapping was computed for. */
debugId: string;
};

/**
* Compute a Unity IL2CPP C++→C# line mapping for a debug information file.
*
* Unity's IL2CPP transpiles C# to C++, embedding `//<source_info:File.cs:line>`
* markers in the generated C++. This enumerates the C++ source files referenced
* by the object's debug info; for each, `readSource` supplies that file's
* contents (return `null` to skip a path not available locally). The markers are
* parsed into a C++→C# mapping serialized as a JSON document — the format Sentry
* consumes for IL2CPP symbolication — which can be uploaded as a separate DIF.
*
* The mapping is computed for the single object chosen by
* {@link selectBundledObject}, matching {@link createSourceBundle}. Nothing is
* read from disk by this function itself; `readSource` performs all I/O.
*
* @param data - The full contents of the debug information file.
* @param readSource - Supplies C++ source content for a referenced path, or
* `null` to skip. Invoked synchronously (e.g. `readFileSync`).
* @returns The serialized mapping bytes plus the object's debug id, or `null`
* when the archive has no objects or the object references no IL2CPP
* `source_info` markers (an empty mapping).
* @throws If the buffer cannot be parsed, or if `readSource` throws.
*/
export function createIl2cppLineMapping(
data: Uint8Array,
readSource: (path: string) => Uint8Array | null
): Il2cppMappingResult | null {
ensureInitialized();
const archive = new Archive(data);
const object = selectBundledObject(archive.objects());
if (!object) {
return null;
}
const mapping = il2cppLineMapping(
object,
(path: string) => readSource(path) ?? undefined
);
if (!mapping) {
return null;
}
return { mapping, debugId: object.debugId };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IL2CPP mapping wrong object

Medium Severity

For archives with multiple debug objects, createIl2cppLineMapping always maps the first debug-info object in the file, while PreparedDif.debugId comes from the filter-matched primary. With --id targeting another slice, the uploaded .il2cpp DIF can advertise that debug id but contain line mappings for a different object.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9e2377d. Configure here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — this is a real mismatch for the fat-archive + --id case. Fixed in #1166: createIl2cppLineMapping now maps the object identified by the target debug id, and the DIF is stamped with the mapped object's own id, so the id always matches the contents.

}

/** A source file referenced by an object, with any resolved descriptor metadata. */
export type DifSourceFile = {
/** Absolute path recorded in the debug info. */
Expand Down
57 changes: 57 additions & 0 deletions test/commands/debug-files/upload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,23 @@ async function writeDifFixture(fixture: string, name: string): Promise<string> {
return path;
}

/**
* Write a Breakpad file referencing an on-disk generated C++ source that carries
* an IL2CPP `source_info` marker, so `--il2cpp-mapping` can resolve a mapping.
*/
async function writeIl2cppFixture(): Promise<void> {
const cppPath = join(tempDir, "Game.cpp");
await writeFile(cppPath, "//<source_info:Game.cs:42>\nint generated = 0;\n");
const sym = [
"MODULE Linux x86_64 0F13A5DA412AFBF7C8662048F3294F3D0 example",
"INFO CODE_ID DAA5130F2A41F7FBC8662048F3294F3D439CA7FF",
`FILE 0 ${cppPath}`,
"FUNC 1000 10 0 main",
"1000 10 42 0",
].join("\n");
await writeFile(join(tempDir, "example.sym"), sym);
}

/** Run `debug-files upload` and capture stdout + exit code. */
async function runUpload(
args: string[]
Expand Down Expand Up @@ -585,4 +602,44 @@ describe("sentry debug-files upload", () => {
expect(difs[0]?.debugId).toBe(EMBEDDED_PE_DEBUG_ID);
expect(difs[0]?.content).toBeInstanceOf(Buffer);
});

// ── IL2CPP line mappings ─────────────────────────────────────────

test("--il2cpp-mapping produces a separate il2cpp DIF (dry-run)", async () => {
await writeIl2cppFixture();
const { output, exitCode } = await runUpload([
tempDir,
"--il2cpp-mapping",
"--no-upload",
"--json",
]);
expect(exitCode).toBe(0);
const files = JSON.parse(output).files as { name: string }[];
expect(files.some((f) => f.name === "example.sym")).toBe(true);
expect(files.some((f) => f.name === "example.sym.il2cpp")).toBe(true);
});

test("no il2cpp DIF is produced without --il2cpp-mapping", async () => {
await writeIl2cppFixture();
const { output } = await runUpload([tempDir, "--no-upload", "--json"]);
const files = JSON.parse(output).files as { name: string }[];
expect(files.some((f) => f.name.endsWith(".il2cpp"))).toBe(false);
});

test("--il2cpp-mapping threads the mapping DIF through to uploadDebugFiles", async () => {
process.env.SENTRY_ORG = "test-org";
process.env.SENTRY_PROJECT = "test-project";
await writeIl2cppFixture();
const spy = vi
.spyOn(debugFilesApi, "uploadDebugFiles")
.mockResolvedValue([]);

await runUpload([tempDir, "--il2cpp-mapping"]);
expect(spy).toHaveBeenCalledTimes(1);
const difs = spy.mock.calls[0]?.[0]?.difs ?? [];
const il2cpp = difs.find((d) => d.name === "example.sym.il2cpp");
expect(il2cpp).toBeDefined();
expect(il2cpp?.debugId).toBe(KNOWN_DEBUG_ID);
expect(il2cpp?.content).toBeInstanceOf(Buffer);
});
});
Loading
Loading