{"record":{"id":"d789b60041c21be4","repo":"headroomlabs-ai/headroom","slug":"archive-did-not-contain-expected-member-member-r","errorCode":null,"errorMessage":"archive did not contain expected member {member!r}","messagePattern":"archive did not contain expected member (.+?)","errorType":"exception","errorClass":"BinaryFetchError","httpStatus":null,"severity":"error","filePath":"headroom/binaries.py","lineNumber":366,"sourceCode":"            shutil.copy2(archive, dest)\n    except (tarfile.TarError, zipfile.BadZipFile, OSError) as e:\n        raise BinaryFetchError(f\"failed to extract {archive.name}: {e}\") from e\n\n\ndef _extract_member_from_tar(tf: tarfile.TarFile, member: str, dest: Path) -> None:\n    # Match by basename so that registries can specify \"difft\" even though the\n    # upstream tar may include a leading directory like \"difft-0.64.0/difft\".\n    wanted = member.lower()\n    for m in tf.getmembers():\n        base = m.name.rsplit(\"/\", 1)[-1].lower()\n        if base == wanted and m.isfile():\n            extracted = tf.extractfile(m)\n            if extracted is None:\n                continue\n            with dest.open(\"wb\") as out:\n                shutil.copyfileobj(extracted, out)\n            return\n    raise BinaryFetchError(f\"archive did not contain expected member {member!r}\")\n\n\ndef _extract_member_from_zip(zf: zipfile.ZipFile, member: str, dest: Path) -> None:\n    wanted = member.lower()\n    for info in zf.infolist():\n        base = info.filename.rsplit(\"/\", 1)[-1].lower()\n        if base == wanted and not info.is_dir():\n            with zf.open(info) as src, dest.open(\"wb\") as out:\n                shutil.copyfileobj(src, out)\n            return\n    raise BinaryFetchError(f\"archive did not contain expected member {member!r}\")\n\n\n# ---------- Public API ---------------------------------------------------- #\n\n\ndef _binary_name(tool: str, plat: PlatformKey) -> str:\n    entry = _tool_entry(tool)","sourceCodeStart":348,"sourceCodeEnd":384,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/headroom/binaries.py#L348-L384","documentation":"After scanning every member of a tar (_extract_member_from_tar) or zip (_extract_member_from_zip), the expected binary was not found, so BinaryFetchError is raised. Matching is by basename, case-insensitive (e.g. registry name 'difft' matches 'difft-0.64.0/difft' inside the tar), and only regular files count. The error names the exact member string that failed to match.","triggerScenarios":"The registry's member name for a tool disagrees with what upstream actually shipped: upstream renamed the binary inside the archive (e.g. added a version or .exe suffix pattern), restructured leading directories, or shipped a top-level directory where the registry expected a file.","commonSituations":"A tool upstream changed its release archive layout after the headroom-ai registry entry was written; Windows assets where the member is 'tool.exe' but the registry says 'tool'; a new tool added to the registry with a guessed member name.","solutions":["Inspect the actual archive contents: tar -tzf <archive> (or unzip -l) and compare basenames against the member name in the error.","Update the registry entry's member field to the real basename (case-insensitive matching means only the basename must be right, e.g. 'difft' or 'difft.exe').","Upgrade headroom-ai — registry fixes for layout changes usually land quickly after upstream releases.","If you control the tool's release process, keep a stable top-level binary name inside archives."],"exampleFix":"# before (registry.json)\n{\"tools\": {\"difft\": {\"member\": \"difftastic\"}}}\n# error: archive did not contain expected member 'difftastic'\n\n# after\ntar -tzf difft-*.tar.gz  # shows difft-0.64.0/difft\n{\"tools\": {\"difft\": {\"member\": \"difft\"}}}  # basename match works","handlingStrategy":"validation","validationCode":"import tarfile, zipfile\n\ndef archive_contains_member(archive: str, member: str) -> bool:\n    wanted = member.lower()\n    name = archive.lower()\n    if name.endswith((\".tar.gz\", \".tgz\")):\n        with tarfile.open(archive) as tf:\n            return any(m.name.rsplit(\"/\", 1)[-1].lower() == wanted and m.isfile() for m in tf.getmembers())\n    if name.endswith(\".zip\"):\n        with zipfile.ZipFile(archive) as zf:\n            return any(i.filename.rsplit(\"/\", 1)[-1].lower() == wanted and not i.is_dir() for i in zf.infolist())\n    return True  # plain/gz single-binary archives copy the file directly\n\n# check before deploying a registry change\nassert archive_contains_member(downloaded, \"difft\")","typeGuard":null,"tryCatchPattern":"from headroom.binaries import BinaryFetchError\n\ntry:\n    ensure_binary(tool)\nexcept BinaryFetchError as e:\n    if \"did not contain expected member\" in str(e):\n        raise SystemExit(f\"registry member name for {tool} is stale; update it after checking the archive listing\") from e\n    raise","preventionTips":["When bumping tool versions in the registry, always verify the archive listing matches the member field (case-insensitive basename).","Remember matching is by basename and case-insensitive — leading directories are fine.","Add a CI job that downloads each registry tool on each supported platform and runs the extraction end-to-end."],"tags":["python","binaries","extraction","registry","archive","packaging"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}