Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cc557b2
feat: Source Map v3 generation with runtime traceback remapping
tinovyatkin Aug 20, 2026
7992159
fix: address PR review feedback on source map runtime and tests
tinovyatkin Aug 20, 2026
73e7173
fix: address second review round on source map runtime
tinovyatkin Aug 20, 2026
d25ccde
fix: address third review round on source map runtime
tinovyatkin Aug 20, 2026
a9661a8
fix: harden runtime against shadowed builtins and read-only maps
tinovyatkin Aug 20, 2026
ccfcdde
fix: address fifth review round on source map runtime
tinovyatkin Aug 20, 2026
e615e70
fix: address sixth review round on source map runtime
tinovyatkin Aug 20, 2026
f0a598a
fix: address seventh review round on source map runtime
tinovyatkin Aug 20, 2026
d9ee813
fix: address eighth review round on source map runtime
tinovyatkin Aug 20, 2026
69c043b
fix: address ninth review round on source map publication and comments
tinovyatkin Aug 21, 2026
5b4750a
fix: address tenth review round on source map runtime
tinovyatkin Aug 21, 2026
e18a1ec
fix: address eleventh review round on source map runtime
tinovyatkin Aug 21, 2026
62dae93
fix: address twelfth review round on source map runtime
tinovyatkin Aug 21, 2026
0eb0081
fix: address thirteenth review round on source map runtime
tinovyatkin Aug 21, 2026
0d60b71
fix: address fourteenth review round on source map runtime
tinovyatkin Aug 22, 2026
16b23cf
fix: address fifteenth review round on source map runtime
tinovyatkin Aug 22, 2026
e99322a
fix: address sixteenth review round on source map runtime
tinovyatkin Aug 25, 2026
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
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 34 additions & 30 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,13 @@ ruff_python_semantic = { git = "https://github.com/astral-sh/ruff/", tag = "0.16
ruff_python_stdlib = { git = "https://github.com/astral-sh/ruff/", tag = "0.16.3" }
ruff_text_size = { git = "https://github.com/astral-sh/ruff/", tag = "0.16.3" }

# Source map generation (Source Map v3, pinned exact version)
oxc_sourcemap = "=8.1.2"

# Serialization and configuration
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.9"
toml = "0.9"
# Package name normalization (PEP 503)
pep508_rs = "0.9"

Expand All @@ -43,8 +46,9 @@ petgraph = "0.8"
rustc-hash = "2.1"

# Utilities
cow-utils = "0.1.3"
etcetera = "0.11"
base64-simd = "0.8"
cow-utils = "0.1.3"
etcetera = "0.11"

# Testing
insta = { version = "1.43.1", features = ["filters", "glob", "yaml"] }
Expand All @@ -62,17 +66,17 @@ unsafe_code = "warn"
# Add these for better AI-generated code catching:
exported_private_dependencies = { level = "allow", priority = 10 }
missing_debug_implementations = "warn"
rust_2018_compatibility = { level = "warn", priority = -2 }
rust_2018_idioms = { level = "warn", priority = -2 }
rust_2021_compatibility = { level = "warn", priority = -2 }
rust_2024_compatibility = { level = "warn", priority = -2 } # Warns about 2015 idioms in 2018 edition
missing_docs = "allow" # Or "allow" if too noisy
rust_2018_compatibility = { level = "warn", priority = -2 }
rust_2018_idioms = { level = "warn", priority = -2 }
rust_2021_compatibility = { level = "warn", priority = -2 }
rust_2024_compatibility = { level = "warn", priority = -2 } # Warns about 2015 idioms in 2018 edition
trivial_casts = "warn"
trivial_numeric_casts = "warn"
unused_extern_crates = "warn"
unused_import_braces = "warn"
unused_qualifications = "warn"
variant_size_differences = "warn"
missing_docs = "allow" # Or "allow" if too noisy

[workspace.lints.clippy]
# Categories
Expand Down Expand Up @@ -103,7 +107,7 @@ similar_names = "allow"
single_match_else = "allow"
too_many_lines = "deny"
unnecessary_debug_formatting = "allow"
unused_self = "allow"
unused_self = "allow"

# Additional allows for practical development
cast_possible_truncation = "allow"
Expand Down Expand Up @@ -149,29 +153,29 @@ path_buf_push_overwrite = "warn"
ptr_as_ptr = "warn"
# redundant_pub_crate disabled due to conflict with unreachable-pub lint
# When a module is pub but items inside are pub(crate), both lints fire incorrectly
redundant_pub_crate = "allow"
ref_binding_to_reference = "warn"
ref_option_ref = "warn"
semicolon_if_nothing_returned = "warn"
str_to_string = "warn"
string_add = "warn"
string_add_assign = "warn"
string_lit_as_bytes = "warn"
redundant_pub_crate = "allow"
ref_binding_to_reference = "warn"
ref_option_ref = "warn"
semicolon_if_nothing_returned = "warn"
str_to_string = "warn"
string_add = "warn"
string_add_assign = "warn"
string_lit_as_bytes = "warn"
# string_to_string has been removed - implicit_clone covers those cases
type_repetition_in_bounds = "warn"
unnecessary_self_imports = "warn"
unnecessary_wraps = "warn"
unneeded_field_pattern = "warn"
unnested_or_patterns = "warn"
unseparated_literal_suffix = "warn"
unused_async = "warn"
use_self = "warn"
useless_let_if_seq = "warn"
verbose_bit_mask = "warn"
wildcard_imports = "warn"
type_repetition_in_bounds = "warn"
unnecessary_self_imports = "warn"
unnecessary_wraps = "warn"
unneeded_field_pattern = "warn"
unnested_or_patterns = "warn"
unseparated_literal_suffix = "warn"
unused_async = "warn"
use_self = "warn"
useless_let_if_seq = "warn"
verbose_bit_mask = "warn"
wildcard_imports = "warn"

# Add these specific lints to catch .ok() misuse
match_result_ok = "warn" # Catches if let Some(x) = result.ok() (renamed from if_let_some_result)
match_result_ok = "warn" # Catches if let Some(x) = result.ok() (renamed from if_let_some_result)
option_map_unit_fn = "warn" # Catches .map(|_| ())
question_mark = "warn" # Encourages using ? operator
result_map_unit_fn = "warn" # Catches .map(|_| ())
Expand Down
60 changes: 60 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ cribo --entry src/main.py --output bundle.py -vvv # trace level

# Custom config file
cribo --entry src/main.py --output bundle.py --config my-cribo.toml

# Generate a Source Map v3 and remap runtime tracebacks to original sources
cribo --entry src/main.py --output bundle.py --sourcemap
```

### CLI Options
Expand All @@ -155,6 +158,8 @@ cribo --entry src/main.py --output bundle.py --config my-cribo.toml
- `--python <PATH>`: Python interpreter whose installed distribution metadata is used for requirements
- `--no-tree-shake`: Disable tree-shaking optimization (tree-shaking is enabled by default)
- `--target-version <VERSION>`: Target Python version (e.g., py38, py39, py310, py311, py312, py313)
- `--sourcemap[=<MODE>]`: Generate a Source Map v3 for the bundle and inject a traceback-remapping runtime. Modes: `linked` (default; writes `<output>.map` plus a `# sourceMappingURL=` comment), `inline` (embeds the map as a base64 comment; the default with `--stdout`), `external` (writes `<output>.map` with no comment). See [Source Maps](#source-maps)
- `--sources-content=<BOOL>`: Force embedding original sources in the map (default: omitted for `inline`, included for `linked`/`external`)
- `-h, --help`: Print help information
- `-V, --version`: Print version information

Expand Down Expand Up @@ -212,6 +217,61 @@ cribo --entry main.py --output bundle.py --no-tree-shake
- When you need to preserve all code for dynamic imports or reflection
- For debugging purposes to see the complete bundled output

### Source Maps

Cribo can emit a [Source Map v3](https://tc39.es/ecma426/) (the language-agnostic
format used across the JavaScript ecosystem) mapping every statement in the bundle
back to its original file and line, plus an injected runtime that remaps uncaught
exception tracebacks to the original sources — analogous to
`node --enable-source-maps`:

```bash
# linked (default): writes bundle.py.map + a trailing sourceMappingURL comment
cribo --entry src/main.py --output bundle.py --sourcemap

# inline: the map travels inside the bundle as a base64 comment
cribo --entry src/main.py --output bundle.py --sourcemap=inline

# external: writes bundle.py.map, no comment in the bundle
cribo --entry src/main.py --output bundle.py --sourcemap=external
```

With source maps enabled, a crash inside the bundle prints the original locations:

```text
Traceback (most recent call last):
File "/app/src/main.py", line 3, in <module>
boom()
File "/app/src/helper.py", line 5, in inner
raise ValueError("kaboom")
ValueError: kaboom
```

Runtime activation follows the delivery mode:

- `inline`: active by default; `CRIBO_SOURCE_MAPS=0` disables it
- `linked`: active exactly when `<bundle>.map` exists next to the bundle at run
time — delete the map to ship without remapping, drop it back to re-enable
- `external`: dormant unless `CRIBO_SOURCE_MAPS=1` is set (or the variable holds a
path to the map file)
- In every mode, `CRIBO_SOURCE_MAPS=<path>` points the runtime at an explicit map
file — the only way to remap a bundle executed via `python -` (stdin), whose
inline map cannot be re-read at run time

The runtime is lazy (zero file access, parsing, or decoding until the first
uncaught exception), streams the map in constant memory so it works under resource
pressure, covers `sys.excepthook`, `threading.excepthook`, and
`sys.unraisablehook`, chains to any pre-installed custom hooks, preserves the
default silent handling of `SystemExit` in worker threads, and falls back to the
standard traceback on any failure. Known limitations: user code that formats
tracebacks itself (e.g. `traceback.format_exc()`) is not remapped;
`ExceptionGroup` chains defer to the standard (unremapped but complete) rendering;
and under a hard out-of-memory condition no pure-Python hook can run.
Configuration-file equivalents: `sourcemap = "linked" | "inline" | "external"` and
`sources-content = true|false` in `cribo.toml`; environment equivalents:
`CRIBO_SOURCEMAP` and `CRIBO_SOURCES_CONTENT`. Full design:
[docs/source-maps.md](docs/source-maps.md).

### Dependency Detection (`cribo deps`)

The `deps` subcommand analyzes a Python file or directory and reports the third-party
Expand Down
2 changes: 2 additions & 0 deletions crates/cribo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ required-features = ["bench"]

[dependencies]
anyhow = { workspace = true }
base64-simd = { workspace = true }
clap = { workspace = true }
cow-utils = { workspace = true }
env_logger = { workspace = true }
etcetera = { workspace = true }
indexmap = { workspace = true }
log = { workspace = true }
oxc_sourcemap = { workspace = true }
pep508_rs = { workspace = true }
petgraph = { workspace = true }
ruff_python_ast = { workspace = true }
Expand Down
16 changes: 12 additions & 4 deletions crates/cribo/src/code_generator/inliner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,12 @@ impl Bundler<'_> {
module_renames.insert(func_name.clone(), renamed_name.clone());
ctx.global_symbols.insert(renamed_name.clone());

// Clone and rename the function
// Clone and rename the function, preserving the original
// identifier's range for source-map provenance (see the
// class rename below for the rationale).
let original_name_range = func_def.name.range;
let mut func_def_clone = func_def.clone();
func_def_clone.name = Identifier::new(renamed_name, TextRange::default());
func_def_clone.name = Identifier::new(renamed_name, original_name_range);

// Apply renames to function annotations (parameters and return type)
if let Some(ref mut returns) = func_def_clone.returns {
Expand Down Expand Up @@ -626,9 +629,14 @@ impl Bundler<'_> {
module_renames.insert(class_name.clone(), renamed_name.clone());
ctx.global_symbols.insert(renamed_name.clone());

// Clone and rename the class
// Clone and rename the class. The original identifier's range is
// preserved on the renamed name: these ranges point into the original
// module source and feed source-map provenance (the code generator
// never reads them), and the name is the only header token that
// exists on every class form (`class C:` has no argument list).
let original_name_range = class_def.name.range;
let mut class_def_clone = class_def.clone();
class_def_clone.name = Identifier::new(renamed_name.clone(), TextRange::default());
class_def_clone.name = Identifier::new(renamed_name.clone(), original_name_range);

// Apply renames to base classes and keyword arguments
// CRITICAL: For cross-module inheritance, we need to apply renames from the
Expand Down
Loading
Loading