DioxusLabs / DioxusLabs/dioxus

Hot-patching produces an empty patch when the tip package has both a lib and a bin (regression from #5479)

Open
#5,778 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
39.1k
Forks
1.9k
Avg merge
4d 10h
Merged PRs (30d)
4

Description

If a package has both a `[lib]` and a `[[bin]]` (the usual `src/lib.rs` + thin `src/main.rs` layout), `dx serve --hot-patch` stops applying edits to any code in the lib, which in that layout is all of the code. Nothing reports a failure: the CLI logs `Hot-patching: ... took NNNms`, a patch dylib is written and loaded, and subsecond fires the patch event. The patch just contains none of the edited code, so every function keeps running its old body forever.

The one honest tell is a DEBUG line from `dx::build::link`:

```
[DEBUG] dx::build::builder: Patch rebuild: changed_crates=["myapp"], modified_crates={"myapp"}
[DEBUG] dx::build::link: Changed crates dag using {"myapp"}
[DEBUG] dx::build::link: replaying crates: []
...
[TRACE] dx::build::link: Workspace hotpatch rlibs: []
```

`replaying crates: []` on an edit that definitely touched lib code is the signature. I confirmed the patch was actually empty by dumping its symbols: the `.so` had about 1,900 defined symbols, all std/core plumbing, zero from my crate.

## Mechanism

`workspace_hotpatch_replay_order` (packages/cli/src/build/link.rs) unconditionally excludes the tip package from the replay set, on the theory that `cargo_build` compiles it separately:

```rust
let tip = self.tip_package_name();
let crates: HashSet<&String> = modified_crates
.iter()
.filter(|name| **name != tip)
.collect();
```

That's true of the tip's bin target, but the objects `cargo_build` contributes to the patch are the bin unit's only; the lib is consumed as an rlib. So for a lib+bin tip, the file watcher correctly maps the edit to the tip package, the replay filter drops it, `workspace_hotpatch_link_rlibs` gets an empty list, and the patch is thin-linked from the bin's unchanged objects. Empty patch, success logs.

Everything needed to do the right thing is already there. The fat build captures the lib target's rustc args (`.captured-args/.../{tip}.lib.json` sits right next to `{tip}.bin.json`), and `workspace_hotpatch_replay_args` already prefers the `{crate}.lib` key over `{crate}.bin`. Only the filter stands in the way.

## This is a regression

- #4160 asked for exactly this layout to work, and #5291 implemented it: `compile_workspace_deps` compiled the tip's lib before its bin ("so the bin links against the fresh lib rlib") and `write_patch` included the tip lib's cached objects. #4160 was closed as completed.
- #5479 removed both halves in the cache refactor, and the replacement `link.rs` kept the tip exclusion with no lib case. Diffing `request.rs` across fb855b708: the parent has both blocks, the commit has neither. `compile_workspace_deps` now exists only as two doc-comment references in builder.rs to a function that's gone.
- So this worked after #5291 and broke in 0.7.6. I checked the filter on v0.7.10, the v0.7 branch head, main, and v0.8.0-alpha.1, and it's byte-identical in all four.

I think this also explains the unanswered comment on #5314 ("Hotpatching the binary works, but not the library"), though the watcher is fine there. The code is lost at the replay filter, not at file detection. The 0.7 hot-reload docs still describe the pre-#5291 "tip crate only" limitation, which makes this easy to misread as working as documented.

## Fix

Make the tip eligible for replay exactly when the captured args contain a `{tip}.lib` entry. Gating on the captured args rather than the manifest means eligibility and the replay/rlib lookups can't disagree about the key:

```rust
let tip_has_lib = workspace_rustc_args
.rustc_args
.contains_key(&format!("{tip}.lib"));

let crates: HashSet<&String> = modified_crates
.iter()
.filter(|name| tip_has_lib || **name != tip)
.collect();
```

(plus threading `workspace_rustc_args` into the function). The topo sort handles the tip node fine, since nothing in the workspace depends on it, so it just orders last.

I've been running this patch locally: `replaying crates: ["myapp"]`, the patch went from ~1,900 symbols to ~127,000 including the edited functions by name, and edits to lib code apply in ~2s again, repeatedly.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start in packages/cli/src/build/link.rs at workspace_hotpatch_replay_order and inspect the captured rustc arguments under .captured-args, including the tip's lib and bin entries. Verify that a lib+bin tip package replays the lib crate, produces nonempty patch symbols, and applies edits to lib code through dx serve --hot-patch.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
build-system, cli
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.