Repository navigation
feat(elf): upgrade a stored symbol table to DWARF without --force - #79
Conversation
A library first uploaded as an AGP SYMBOL_TABLE (.so.sym) and later available
with debug info shares one GNU build-id, so the server skipped the richer file
and the only remedy was --force, which re-sends every library in the run.
Each native library now declares what it can symbolicate (`format_variant`:
"dwarf" or "symtab", read from the file itself, never its name) and asks the
server to replace a poorer stored copy (`replace_if_richer: true`). The server
(bugsee-appserver#65-67) then decides:
- stored symtab, local dwarf: replaced, bytes transferred once, logged as
"upgraded SYMBOL_TABLE -> FULL" and counted in the run summary
- same file again, or a symtab when the server holds dwarf: 16004, no PUT
- --force is unchanged ("always replace") and still declares the variant
The 16004 reply may now carry the stored variant. A dwarf library skipped
without the server saying what it holds means a server that predates this, and
only then is the "re-run with --force" advice still printed.
Both new Metadata fields are omitted from every other flow's wire body, and
an older server ignores them. `Outcome` and `presigned::upload` are unchanged;
the ELF flow uses the new `upload_detailed`.
Ship order: the appserver change is deployed first, then this release, then the
Gradle plugin's version floor.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep review —
feat(elf): upgrade a stored symbol table to DWARF without --forceCloses #78. I reviewed the ELF identify/upload path, the shared presigned client (every format uses
register),--type rustELF (sameformat: elfdocuments), the Gradle directory-upload caller (#13 / #78), and the merged appserver ranking notes from #65/#67. CI is green. Locally:cargo test --test elf_upload(16/16) plus the newformat_variant/upload_detailedunit tests — all pass.The feature shape is right: content-based
dwarf/symtab, other flows omit the new fields,Outcome/uploadstay stable, and the old-server--forcesummary is still pinned byelf_force_overwrites_a_library_already_on_the_server.There is one merge-blocking issue: this CLI asks the server to replace-if-richer for every native library, including files it labels
symtab. At ship time every stored ELF record is unlabeled. That combination can downgrade a historical FULL upload.Findings
- P1 High —
replace_if_richer: trueon non-DWARF uploads vs unlabeled production records (src/cli/debug_files.rs~1405,src/symbols/elf.rsformat_variant)- P2 Medium — skip log always says “same or better level”, including when the server did not say what it holds (
src/cli/debug_files.rs~1428)Overall risk: High
Merge recommendation: Do not merge until the P1 is fixed
Gate
replace_if_richeron a localdwarffile (the only upgrade #78 asks for). If create-timeoutranksis numeric with missing <symtab<dwarf— which matches appserver #67 treatingnullas outranked — the first--type elfrun after this ships can replace unlabeled FULL documents with a symbol table / stripped.so. That is silent loss offile:line. If the server already refuses non-dwarf incoming, the CLI is still requesting a replacement it does not need; omit the flag anyway and add a wire test that a.so.symPOST does not carryreplace_if_richer.Most important to fix
- Only send
replace_if_richer: truewhenlib.format_variant() == Some("dwarf").- Do not treat
.dynsymas AGPSYMBOL_TABLE(symbolic’shas_symbols()issyms || dynsyms).Residual (not blocking)
--type ruststill omits both fields, so Linux/Android Rust ELF keeps creating unlabeledformat: elfdocuments and still needs--forcefor stripped → unstripped. Same server identity as--type elf.Positive
Content vs name is tested (FULL packed as
.so.symstill sendsdwarf). Other formats leave the new keys off the wire.--forcestill sendsoverwrite. Upgrade logging /upgraded=N/ zero-PUT skip paths are pinned against a protocol-accurate mock.Sent by Cursor Automation: Bugsee code review
🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Deep review (re-review of 9c97b31)
Closes #78. I re-reviewed the ELF identify/upload path, the shared presigned client, --type rust ELF (same format: elf documents), the Gradle directory-upload caller, and the merged appserver ranking from #65/#67. CI is green (fmt, clippy, test, e2e on all platforms).
Both findings from d9b6645 are addressed.
Prior findings
-
P1 High —
replace_if_richer: trueon non-DWARF uploads vs unlabeled production records — fully addressed (rebuttal).outranks()only letsdwarfreplace an unlabeled stored copy;symtabvs unlabeled is 16004, pinned by the appserver test “lets only dwarf replace a stored record that predates format_variant”. The unlabeled-FULL-then-.so.symscenario is therefore the same skip as today. Gating the flag ondwarfwould also be a regression: on a plain Android app the flag is what leaves the legacy find-or-update (no duplicate check), so an unchanged.so.symwould re-transfer on every build. The call-site comment inupload_one_sodocuments that. -
P2 Medium — skip log always said “same or better” — fully addressed.
AlreadyExistsuses that line only whendetail.storedisSome; otherwise it logs “already on server, skipped” and the run summary still carries the--forceadvice. Pinned byelf_upload_does_not_claim_same_or_better_when_the_server_does_not_say.
Findings
No remaining P0–P3.
Overall risk: Low
Merge recommendation: Approve
Most important issues to fix
None in this PR.
Residual, not blocking: --type rust ELF still omits both new fields, so Linux/Android Rust ELF keeps creating unlabeled format: elf documents and still needs --force for stripped → unstripped. .dynsym-only libraries are labelled symtab (symbolic’s has_symbols() is syms || dynsyms); that can miss a later true SYMBOL_TABLE upgrade, never a downgrade. Agreed as a follow-up.
Positive
Content vs name is tested (FULL packed as .so.sym still sends dwarf). Other formats leave the new keys off the wire. --force still sends overwrite. Upgrade logging / upgraded=N / zero-PUT skip paths are pinned against a protocol-accurate mock, including the old-server --force summary.
Sent by Cursor Automation: Bugsee code review


Closes #78. Server half: bugsee/bugsee-appserver#65, #66, #67 (all merged).
Problem
A library first uploaded as an AGP
SYMBOL_TABLE(.so.sym) and later available with debug info shares one GNU build-id, so the server skipped the richer file (16004, no PUT). The only remedy was--force, which re-sends every library in the run — a poor fit for callers that upload a whole directory on every build (bugsee-android-gradle-plugin#10/#13).Change
format_variant=dwarf|symtab, read from the file (debug info vs symbol table), never from its name. A library with neither declares nothing.--type elfmetadata POST also sendsreplace_if_richer: true; the server compares with what it holds.uploaded (upgraded SYMBOL_TABLE -> FULL)and counted asupgraded=Nin the run summary--forceunchanged ("always replace"); it still declares the varianterror.params.format_variant). A dwarf library skipped without the server saying what it holds means a server that predates this, and only then is the "re-run with --force" advice still printed (reworded). Against a current server it no longer appears, since it would be wrong.Metadatafields are omitted from every other flow's body; an older server ignores them (strict schema), so nothing changes there.Outcomeandpresigned::uploadare unchanged — the ELF flow uses a newupload_detailedreturning{outcome, upgraded, stored}.--helpfor--forceand the README--type elfsection updated.Rollout
Per CLAUDE.md this is a two-step rollout: the appserver change is already merged (staging deploys on merge; production is a manual deploy). Release this CLI, then bump the Gradle plugin's version floor. Until the server is on production, the new fields are ignored and behaviour is today's.
Test plan
cargo fmt -- --check,cargo clippy --all-targets -- -D warnings,cargo test(657 unit + all integration)scripts/e2e_flows.pyagainst a release build: ALL PASS (including the ELF-directory peak-RSS budget)New tests: variant classification on real fixtures (unstripped =
dwarf, same library stripped of debug sections =symtab, same build-id), wire body per case incl. name/content disagreement,upgraded/ stored-variant parsing (known, unknown, absent), upgrade → one PUT, richer-kept → zero PUTs and no--forceadvice,--forcestill sendsoverwriteReal uploads against the staging appserver (
apidev.bugsee.com, appservermainincl. docs: validate READMEs against the code and published 0.7.12 #65-chore: license under MIT #67), release build of this branch, no--forceanywhere. Fixturelibsymbol1(build-idbca64abf…4472), symbol-table and unstripped variants:uploaded=1; recordready(427 KB)already on server … stored="symtab",uploaded=0— no transferuploaded (upgraded SYMBOL_TABLE -> FULL),upgraded=1; afterwards exactly ONEreadyrecord, the 1.8 MB DWARF one — the symbol-table record is gonestored="dwarf", no transferstored="dwarf", no transfer (never downgrades)Left on that staging app: one
readyELF record, versioncli-78-verify.New fixture
tests/fixtures/elf/libsymbol1.symtab.so(427 KB):libsymbol1.sowithrust-objcopy --strip-debug.🤖 Generated with Claude Code