dbt-labs/dbt-core · error · anyhow

{filename} has no *.dist-info/METADATA

Error message

{filename} has no *.dist-info/METADATA

What it means

check_wheel_metadata_agrees expects every candidate wheel to contain a *.dist-info/METADATA file inside its zip. If wheel_metadata returns None (no METADATA entry found), the function bails because Requires-Python and Requires-Dist cross-checks are impossible without it.

Source

Thrown at crates/dbt-ci/src/sdist.rs:145

    }

    build_sdist(spec, &version_pep440, &wheels, base_url, out_dir)
}

/// Fails the release when the sdist's static metadata disagrees with a wheel it
/// points at, in either direction. pip survives such a mismatch (it builds the
/// wheel and reads the real metadata), but uv trusts the sdist's PKG-INFO and
/// never re-checks — so a wrong `Requires-Python` installs on an unsupported
/// interpreter and dies at import on an ABI symbol, and a missing `Requires-Dist`
/// (in either direction) installs the wrong dependency set and dies on first run.
fn check_wheel_metadata_agrees(spec: &Spec, filename: &str, wheel: &[u8]) -> Result<()> {
    let Some(metadata) = wheel_metadata(wheel)
        .with_context(|| format!("read METADATA from {filename}"))?
        .map(|raw| Metadata::parse(&raw))
        .transpose()
        .with_context(|| format!("parse METADATA from {filename}"))?
    else {
        bail!("{filename} has no *.dist-info/METADATA");
    };

    let expected_python = spec.requires_python.as_deref();
    let actual_python = metadata.requires_python.as_deref();
    if actual_python != expected_python {
        bail!(
            "{filename} declares Requires-Python {actual:?} but the sdist says \
             {expected:?}; point `--pyproject-dir` at the pyproject that built \
             these wheels",
            actual = actual_python.unwrap_or("(absent)"),
            expected = expected_python.unwrap_or("(absent)"),
        );
    }

    let expected_deps: std::collections::BTreeSet<&str> =
        spec.dependencies.iter().map(|d| d.trim()).collect();
    let actual_deps: std::collections::BTreeSet<&str> =
        metadata.requires_dist.iter().map(|d| d.trim()).collect();

View on GitHub (pinned to 0267ce9170)

Solutions

  1. Verify the file is a proper wheel (unzip -l FILE | grep 'dist-info/METADATA') and rebuild it with a real wheel builder if not.
  2. Ensure you are passing wheel files (.whl), not sdists or plain zips, as targets.
  3. Re-run the wheel build (maturin/cibuildwheel) if the wheel was truncated during upload/download (also check the recorded sha256).

Example fix

# before
unzip -l dbt_core-1.0.0.zip   # no dist-info/METADATA -> error
# after
maturin build --release       # produce a real .whl and pass that path
Defensive patterns

Strategy: validation

Validate before calling

# verify each wheel contains METADATA before handing it to the pipeline
for whl in wheels/*.whl; do
  unzip -l "$whl" | grep -q 'dist-info/METADATA' || { echo "$whl has no METADATA" >&2; exit 1; }
done

Type guard

fn looks_like_wheel(path: &Path) -> bool {
    path.extension().map_or(false, |e| e == "whl")
}

Try / catch

match check_wheel_metadata_agrees(...) {
    Err(e) if e.to_string().contains("no *.dist-info/METADATA") => {
        eprintln!("not a valid wheel; rebuild it: {e:#}");
        std::process::exit(1);
    }
    r => r,
}

Prevention

When it happens

Trigger: Passing a file to build_release_sdist / check_wheel_metadata_agrees that is not a valid wheel (no .dist-info/METADATA in the zip): a raw .tar.gz, a corrupted/truncated wheel, or a zip built without dist-info.

Common situations: Pointing --target/asset globs at artifacts that aren't wheels, a build step producing broken wheels, or manually renamed/zipped files being fed to the cross-check.

Related errors


AI-assisted analysis of dbt-labs/dbt-core@0267ce9170 (2026-09-07). Data as JSON: /api/errors/13a23d32ff8b835e. Report an issue: GitHub.