Skip to content

Default features pull C zlib (libz-sys) — block pure-Rust downstream binaries #3

@FelixKrueger

Description

@FelixKrueger

Hi @ewels — small observation surfaced while working through a bioconda packaging issue for TrimGalore (bioconda-recipes#65446#65450). Posting here so it's tracked; no urgency.

Observation

fastqc-rust v1.0.1's Cargo.toml opts out of flate2's default features for the direct dependency:

flate2 = { version = "1", default-features = false, features = ["rust_backend"] }   # line 38

But then re-enables the C zlib backend through the crate's own default feature:

[features]
default = ["native-zlib"]                  # line 54
native-zlib = ["flate2/zlib"]              # line 55

So downstream users who set default-features = false on fastqc-rust and on flate2 directly still end up with libz-sys linked into their binary, because Cargo's feature unification across the dependency graph adds flate2/zlib back through this path. There's also a secondary contributor — zip = "2" (line 41) is pulled with default features, and zip's default _deflate-any chain enables flate2's deflate-flate2 feature, which itself activates the C zlib backend.

Concrete example — TrimGalore v2.2.0

TrimGalore's Cargo.toml:

flate2 = { version = "1.1", default-features = false, features = ["zlib-rs"] }

Explicitly asks for the pure-Rust zlib-rs backend. But cargo tree -i libz-sys shows:

libz-sys v1.1.28
└── flate2 v1.1.9
    ├── fastqc-rust v1.0.1
    ├── gzp v2.0.2
    ├── hdf5-pure v0.1.1
    └── noodles-bgzf v0.35.0

And cargo tree --edges features traces libz-sys activation via zip → deflate-flate2 → flate2/zlib (from fastqc-rust's native-zlib feature). The resulting Linux release binary dynamically links libz.so.1, which surfaced in bioconda as a runtime dependency that needed to be declared in the recipe's run: block.

Why this matters

Two downstream stories suffer:

  1. Static-binary / single-static-binary goals. TrimGalore's v2.x value prop is "no external runtime dependencies — single Rust binary". The libz.so.1 dynamic link contradicts that.
  2. Packaging on minimal sysroots. Distros / containers that don't ship system zlib (or version-skew on libz) hit runtime errors.

There's no functional bug here — the C zlib backend works fine. It's a purity / packaging-ergonomics request.

Suggested directions (not prescriptive)

I'd lean toward making the pure-Rust path the default and the C-zlib path an explicit opt-in:

Option A — flip the default:

[features]
default = []                            # was: default = ["native-zlib"]
native-zlib = ["flate2/zlib"]           # unchanged — explicit opt-in for users who want C zlib speed

Option B — also fix zip's transitive default:

zip = { version = "2", default-features = false, features = ["deflate-zlib-rs"] }   # or "deflate-miniz" for fully Rust path

Option A alone moves the explicit default but leaves zip's transitive pull. Option A + B together would deliver a fully pure-Rust path when downstream users disable native-zlib.

The behavior change for existing users is non-zero: anyone relying on the implicit C-zlib default would see a perf delta in deflate paths. Whether that's worth flipping depends on how much you weight static-binary stories vs. raw deflate speed for the typical FastQC use case (single file pass, deflate is rarely the bottleneck).

Coordination note

If you do ship a v1.0.2 with this change, TrimGalore would deliberately bump and re-verify output byte-identity against Java FastQC 0.12.1 (per our exact-pin convention). Happy to help drive a TrimGalore-side patch the moment a v1.0.2 lands. No urgency — happy to wait on your roadmap.

Thanks for the great crate either way.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions