#!/usr/bin/env python3 """make_msvc_import_libs.py -- MSVC import libraries for the mingw-built FFmpeg DLLs. For every /lib/-.def whose DLL is /bin/-.dll, writes /lib/.lib with MSVC's own lib.exe: lib.exe /nologo /machine:x64 /def:-.def /name:-.dll /out:.lib That is the layout libs/ffmpeg/lib has today (avcodec.lib, avformat.lib, ...), which CMakeLists.txt links. Then checks with dumpbin /exports that every .def entry is in the .lib and every .lib entry is really exported by the DLL (a .lib naming a symbol the DLL lacks links fine and fails at load). lib.exe/dumpbin.exe are found with vswhere (newest MSVC toolset), or pass --msvc-bin . Exit 0 ok, 1 mismatch, 2 could not run. """ from __future__ import annotations import argparse import hashlib import re import subprocess import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from check_ffmpeg_build import PE # noqa: E402 def find_msvc_bin() -> Path: vswhere = Path(r"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe") if not vswhere.is_file(): raise FileNotFoundError("vswhere.exe not found; pass --msvc-bin") roots = subprocess.run([str(vswhere), "-all", "-products", "*", "-property", "installationPath"], capture_output=True, text=True).stdout.splitlines() cands = [] for r in roots: for t in (Path(r) / "VC" / "Tools" / "MSVC").glob("*"): b = t / "bin" / "Hostx64" / "x64" if (b / "lib.exe").is_file(): cands.append((tuple(int(x) for x in re.findall(r"\d+", t.name)), b)) if not cands: raise FileNotFoundError("no MSVC x64 toolset with lib.exe found") return max(cands)[1] def def_exports(path: Path) -> set[str]: names, on = set(), False for line in path.read_text(encoding="ascii", errors="replace").splitlines(): s = line.strip() if not s or s.startswith(";"): continue if s.upper() == "EXPORTS": on = True continue if s.split()[0].upper() in ("LIBRARY", "NAME"): continue if on: names.add(re.split(r"[\s=@]", s)[0]) return names def lib_exports(dumpbin: Path, lib: Path) -> set[str]: out = subprocess.run([str(dumpbin), "/nologo", "/exports", str(lib)], capture_output=True, text=True).stdout names, on = set(), False for line in out.splitlines(): s = line.strip() if s == "Exports": on = True continue if on and s and not s.startswith(("ordinal", "Summary")) and re.fullmatch(r"[A-Za-z_][\w@?$]*", s): names.add(s) if s.startswith("Summary"): break return names def main() -> int: ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) ap.add_argument("root", type=Path) ap.add_argument("--msvc-bin", type=Path, help="directory with lib.exe and dumpbin.exe") a = ap.parse_args() root = a.root.resolve() try: mb = a.msvc_bin or find_msvc_bin() except FileNotFoundError as e: print(e, file=sys.stderr) return 2 lib_exe, dumpbin = mb / "lib.exe", mb / "dumpbin.exe" print(f"lib.exe: {lib_exe}") defs = sorted((root / "lib").glob("*-*.def")) if not defs: print(f"no lib/*-*.def under {root}", file=sys.stderr) return 2 bad = 0 for d in defs: stem = d.stem # avcodec-62 name = stem.rsplit("-", 1)[0] # avcodec dll = root / "bin" / f"{stem}.dll" out = root / "lib" / f"{name}.lib" r = subprocess.run([str(lib_exe), "/nologo", "/machine:x64", f"/def:{d}", f"/name:{dll.name}", f"/out:{out}"], capture_output=True, text=True) if r.returncode != 0: print(f"FAIL {out.name}: lib.exe exit {r.returncode}\n{r.stdout}{r.stderr}") bad += 1 continue out.with_suffix(".exp").unlink(missing_ok=True) # lib.exe also writes an export file want, got = def_exports(d), lib_exports(dumpbin, out) real = PE(dll).exports() if dll.is_file() else set() problems = [] if want - got: problems.append(f"{len(want - got)} .def names not in .lib: {sorted(want - got)[:5]}") if got - real: problems.append(f"{len(got - real)} .lib names not exported by {dll.name}: {sorted(got - real)[:5]}") print(f"{'FAIL' if problems else 'ok '} {out.name:<16} {len(got):5d} symbols -> {dll.name}" + (f" {'; '.join(problems)}" if problems else "")) bad += bool(problems) sums = root / "SHA256SUMS" if not bad and sums.is_file(): # the Linux export wrote it before the .lib files existed files = sorted(p for d in ("bin", "lib", "include") for p in (root / d).rglob("*") if p.is_file()) sums.write_text("".join(f"{hashlib.sha256(p.read_bytes()).hexdigest()} {p.relative_to(root).as_posix()}\n" for p in files), encoding="ascii", newline="\n") print(f"rewrote {sums} ({len(files)} files)") return 1 if bad else 0 if __name__ == "__main__": sys.exit(main())