Skip to content

WIP: Improve audit time #545

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 36 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
72120a6
add devcontainer for large wheel
oraluben Feb 18, 2025
f942693
Merge branch 'fast' of github.com:oraluben/auditwheel into fast
oraluben Feb 18, 2025
58868af
fix for ssh config
oraluben Feb 18, 2025
6c46685
reduce some verbose logs
oraluben Feb 19, 2025
f204219
add debug info to show time of [un]zip
oraluben Feb 19, 2025
f188d5e
add ad-hoc libs
oraluben Feb 19, 2025
62433c1
early return
oraluben Feb 19, 2025
43e56e8
add test script to devcontainer
oraluben Feb 19, 2025
92fc470
update
oraluben Feb 19, 2025
9716c54
upadte
oraluben Feb 19, 2025
eaef3cb
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 19, 2025
ae8f67c
set default level to 4
oraluben Feb 19, 2025
344df64
lint
oraluben Feb 19, 2025
3a5f385
concurrent grafting
oraluben Feb 19, 2025
63664d0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 19, 2025
d637359
fix
oraluben Feb 20, 2025
8f994bb
lint
oraluben Feb 20, 2025
69b154b
fix test
oraluben Feb 20, 2025
9924b97
fix test
oraluben Feb 20, 2025
1f1171a
test
oraluben Feb 20, 2025
ad24953
fix test
oraluben Feb 20, 2025
137185b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 20, 2025
5134001
update
oraluben Feb 20, 2025
c0908de
Merge branch 'fast' of github.com:oraluben/auditwheel into fast
oraluben Feb 20, 2025
3794fb3
reuse whl ctx
oraluben Feb 20, 2025
ae35d87
fix test
oraluben Feb 20, 2025
65d5112
fix
oraluben Feb 20, 2025
1c0d093
remove unused
oraluben Feb 20, 2025
3e8eea1
add log
oraluben Feb 21, 2025
bd76009
Merge branch 'fast' of github.com:oraluben/auditwheel into fast
oraluben Feb 21, 2025
aefee5e
Merge remote-tracking branch 'origin/main' into fast
oraluben Feb 22, 2025
bbf2514
update
oraluben Feb 23, 2025
32fb30c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 23, 2025
a2ec785
Merge branch 'pypa:main' into fast
oraluben Mar 7, 2025
be7f044
tmp
oraluben Mar 9, 2025
04195c4
Merge branch 'fast' of github.com:oraluben/auditwheel into fast
oraluben Mar 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM --platform=linux/amd64 mcr.microsoft.com/devcontainers/python:1-3.12-bullseye

RUN --mount=type=cache,target=/home/vscode/.cache/pip \
set -eux; \
apt-get update; \
apt-get install -y moreutils; \
pip wheel --no-deps torch; \
pip install patchelf pre-commit nox ipdb torch-*.whl; \
mkdir -p /usr/local/lib/nv; \
ln -s /usr/local/lib/python3.12/site-packages/nvidia/*/lib/*.so* /usr/local/lib/nv/; \
echo "/usr/local/lib/nv" > /etc/ld.so.conf.d/nv.conf; \
ldconfig -p

COPY demo.sh /
12 changes: 12 additions & 0 deletions .devcontainer/demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#! /bin/bash

rm -rf /tmp/wheelhouse

auditwheel -v repair \
--exclude libcuda.so.1 \
--exclude libcusolver.so.11 \
--exclude libcusparseLt.so.0 \
--plat=manylinux_2_35_x86_64 \
-w /tmp/wheelhouse \
/torch-2.6.0-cp312-cp312-manylinux1_x86_64.whl \
2>&1 | ts '[%Y-%m-%d %H:%M:%S]'
9 changes: 9 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"build": { "dockerfile": "Dockerfile" },
"mounts": [
"type=bind,source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh/id_rsa,target=/home/vscode/.ssh/id_rsa,readonly",
"type=bind,source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh/known_hosts,target=/home/vscode/.ssh/known_hosts",
"type=tmpfs,target=/tmp"
],
"postStartCommand": "pip install -e /workspaces/auditwheel"
}
3 changes: 2 additions & 1 deletion src/auditwheel/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import auditwheel

from . import main_lddtree, main_repair, main_show
from . import main_lddtree, main_repair, main_show, tools


def main() -> int | None:
Expand Down Expand Up @@ -46,6 +46,7 @@ def main() -> int | None:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
tools._COMPRESS_LEVEL = args.zip

if not hasattr(args, "func"):
p.print_help()
Expand Down
15 changes: 15 additions & 0 deletions src/auditwheel/main_repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import argparse
import logging
import zlib
from pathlib import Path

from auditwheel.patcher import Patchelf
Expand Down Expand Up @@ -40,6 +41,18 @@ def configure_parser(sub_parsers) -> None: # type: ignore[no-untyped-def]
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument("WHEEL_FILE", type=Path, help="Path to wheel file.", nargs="+")
parser.add_argument(
"-z",
"--zip-level",
action=EnvironmentDefault,
metavar="zip",
env="AUDITWHEEL_ZIP_LEVEL",
dest="zip",
type=int,
help="Compress level to be used to create zip file.",
choices=list(range(zlib.Z_NO_COMPRESSION, zlib.Z_BEST_COMPRESSION + 1)),
default=zlib.Z_DEFAULT_COMPRESSION,
)
parser.add_argument(
"--plat",
action=EnvironmentDefault,
Expand Down Expand Up @@ -112,6 +125,8 @@ def configure_parser(sub_parsers) -> None: # type: ignore[no-untyped-def]


def execute(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
print(args)
exit()
from .repair import repair_wheel
from .wheel_abi import NonPlatformWheel, analyze_wheel_abi

Expand Down
22 changes: 11 additions & 11 deletions src/auditwheel/policy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,19 +145,19 @@ def versioned_symbols_policy(
def policy_is_satisfied(
policy_name: str, policy_sym_vers: dict[str, set[str]]
) -> bool:
policy_satisfied = True
for name in set(required_vers) & set(policy_sym_vers):
if not required_vers[name].issubset(policy_sym_vers[name]):
for symbol in required_vers[name] - policy_sym_vers[name]:
logger.debug(
"Package requires %s, incompatible with "
"policy %s which requires %s",
symbol,
policy_name,
policy_sym_vers[name],
)
policy_satisfied = False
return policy_satisfied
symbols = required_vers[name] - policy_sym_vers[name]
logger.debug(
"%s requires any of %s, incompatible with "
"policy %s which requires %s",
name,
symbols,
policy_name,
policy_sym_vers[name],
)
return False
return True

required_vers: dict[str, set[str]] = {}
for symbols in versioned_symbols.values():
Expand Down
Empty file added src/auditwheel/pool.py
Empty file.
81 changes: 57 additions & 24 deletions src/auditwheel/repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
import re
import shutil
import stat
from collections.abc import Iterable
import typing as t
from concurrent.futures import Future, ThreadPoolExecutor, as_completed
from fnmatch import fnmatch
from os.path import isabs
from pathlib import Path
Expand Down Expand Up @@ -65,6 +66,10 @@ def repair_wheel(

dest_dir = Path(match.group("name") + lib_sdir)

pool = ThreadPoolExecutor()
copy_works: dict[Path, Future[t.Any]] = {}
replace_works: dict[Path, Future[t.Any]] = {}

# here, fn is a path to an ELF file (lib or executable) in
# the wheel, and v['libs'] contains its required libs
for fn, v in external_refs_by_fn.items():
Expand All @@ -82,52 +87,78 @@ def repair_wheel(

if not dest_dir.exists():
dest_dir.mkdir()
new_soname, new_path = copylib(src_path, dest_dir, patcher)
new_soname, new_path = copylib(src_path, dest_dir, patcher, dry=True)
if new_path not in copy_works:
copy_works[new_path] = pool.submit(
copylib, src_path, dest_dir, patcher
)
soname_map[soname] = (new_soname, new_path)
replacements.append((soname, new_soname))
if replacements:
patcher.replace_needed(fn, *replacements)

if len(ext_libs) > 0:
new_fn = fn
if _path_is_script(fn):
new_fn = _replace_elf_script_with_shim(match.group("name"), fn)
# Replace rpath do not need copy to be done
def _inner_replace(
fn: Path, replacements: list[tuple[str, str]], append_rpath: bool
) -> None:
logger.info("Start replace for %s", fn)
if replacements:
patcher.replace_needed(fn, *replacements)

if append_rpath:
new_fn = fn
if _path_is_script(fn):
new_fn = _replace_elf_script_with_shim(match.group("name"), fn)

new_rpath = os.path.relpath(dest_dir, new_fn.parent)
new_rpath = os.path.join("$ORIGIN", new_rpath)
append_rpath_within_wheel(new_fn, new_rpath, ctx.name, patcher)

logger.info("Done replace for %s", fn)

new_rpath = os.path.relpath(dest_dir, new_fn.parent)
new_rpath = os.path.join("$ORIGIN", new_rpath)
append_rpath_within_wheel(new_fn, new_rpath, ctx.name, patcher)
replace_works[fn] = pool.submit(
_inner_replace, fn, replacements, len(ext_libs) > 0
)

# we grafted in a bunch of libraries and modified their sonames, but
# they may have internal dependencies (DT_NEEDED) on one another, so
# we need to update those records so each now knows about the new
# name of the other.
assert all(
f.exception() is None
for f in as_completed(
itertools.chain(copy_works.values(), replace_works.values())
)
)
replace_works.clear()
for _, path in soname_map.values():
needed = elf_read_dt_needed(path)
replacements = []
for n in needed:
if n in soname_map:
replacements.append((n, soname_map[n][0]))
if replacements:
patcher.replace_needed(path, *replacements)
replace_works[path] = pool.submit(
patcher.replace_needed, path, *replacements
)

assert all(f.exception() is None for f in as_completed(replace_works.values()))
if update_tags:
ctx.out_wheel = add_platforms(ctx, abis, get_replace_platforms(abis[0]))

if strip:
libs_to_strip = [path for (_, path) in soname_map.values()]
extensions = external_refs_by_fn.keys()
strip_symbols(itertools.chain(libs_to_strip, extensions))

return ctx.out_wheel
for lib in itertools.chain(
[path for (_, path) in soname_map.values()], external_refs_by_fn.keys()
):
logger.info("Stripping symbols from %s", lib)
pool.submit(check_call, ["strip", "-s", lib])

pool.shutdown()

def strip_symbols(libraries: Iterable[Path]) -> None:
for lib in libraries:
logger.info("Stripping symbols from %s", lib)
check_call(["strip", "-s", lib])
return ctx.out_wheel


def copylib(src_path: Path, dest_dir: Path, patcher: ElfPatcher) -> tuple[str, Path]:
def copylib(
src_path: Path, dest_dir: Path, patcher: ElfPatcher, dry: bool = False
) -> tuple[str, Path]:
"""Graft a shared library from the system into the wheel and update the
relevant links.

Expand All @@ -151,10 +182,10 @@ def copylib(src_path: Path, dest_dir: Path, patcher: ElfPatcher) -> tuple[str, P
new_soname = src_name

dest_path = dest_dir / new_soname
if dest_path.exists():
if dry or dest_path.exists():
return new_soname, dest_path

logger.debug("Grafting: %s -> %s", src_path, dest_path)
logger.debug("Start grafting: %s -> %s", src_path, dest_path)
rpaths = elf_read_rpaths(src_path)
shutil.copy2(src_path, dest_path)
statinfo = dest_path.stat()
Expand All @@ -166,6 +197,8 @@ def copylib(src_path: Path, dest_dir: Path, patcher: ElfPatcher) -> tuple[str, P
if any(itertools.chain(rpaths["rpaths"], rpaths["runpaths"])):
patcher.set_rpath(dest_path, "$ORIGIN")

logger.debug("Done grafting to: %s", src_path)

return new_soname, dest_path


Expand Down
26 changes: 23 additions & 3 deletions src/auditwheel/tools.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
from __future__ import annotations

import argparse
import logging
import os
import subprocess
import zipfile
import zlib
from collections.abc import Generator, Iterable
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, TypeVar

_T = TypeVar("_T")

logger = logging.getLogger(__name__)

# Default: zlib.Z_DEFAULT_COMPRESSION (-1 aka. level 6) balances speed and size.
# Maintained for typical builds where iteration speed outweighs distribution savings.
# Override via AUDITWHEEL_ZIP_LEVEL/--zip-level for:
# - some test builds that needs no compression at all (0)
# - bandwidth-constrained or large amount of downloads (9)
_COMPRESS_LEVEL = zlib.Z_DEFAULT_COMPRESSION


def unique_by_index(sequence: Iterable[_T]) -> list[_T]:
"""unique elements in `sequence` in the order in which they occur
Expand Down Expand Up @@ -90,6 +101,7 @@ def zip2dir(zip_fname: Path, out_dir: Path) -> None:
out_dir : str
Directory path containing files to go in the zip archive
"""
start = datetime.now()
with zipfile.ZipFile(zip_fname, "r") as z:
for name in z.namelist():
member = z.getinfo(name)
Expand All @@ -102,6 +114,9 @@ def zip2dir(zip_fname: Path, out_dir: Path) -> None:
attr &= 511 # only keep permission bits
attr |= 6 << 6 # at least read/write for current user
os.chmod(extracted_path, attr)
logger.info(
"zip2dir from %s to %s takes %s", zip_fname, out_dir, datetime.now() - start
)


def dir2zip(in_dir: Path, zip_fname: Path, date_time: datetime | None = None) -> None:
Expand All @@ -120,6 +135,7 @@ def dir2zip(in_dir: Path, zip_fname: Path, date_time: datetime | None = None) ->
date_time : Optional[datetime]
Time stamp to set on each file in the archive
"""
start = datetime.now()
in_dir = in_dir.resolve(strict=True)
if date_time is None:
st = in_dir.stat()
Expand All @@ -140,7 +156,10 @@ def dir2zip(in_dir: Path, zip_fname: Path, date_time: datetime | None = None) ->
zinfo.date_time = date_time_args
zinfo.compress_type = compression
with open(fname, "rb") as fp:
z.writestr(zinfo, fp.read())
z.writestr(zinfo, fp.read(), compresslevel=_COMPRESS_LEVEL)
logger.info(
"dir2zip from %s to %s takes %s", in_dir, zip_fname, datetime.now() - start
)


def tarbz2todir(tarbz2_fname: Path, out_dir: Path) -> None:
Expand All @@ -157,15 +176,16 @@ def __init__(
required: bool = True,
default: str | None = None,
choices: Iterable[str] | None = None,
type: type | None = None,
**kwargs: Any,
) -> None:
self.env_default = os.environ.get(env)
self.env = env
if self.env_default:
default = self.env_default
default = self.env_default if type is None else type(self.env_default)
if default:
required = False
if self.env_default and choices is not None and self.env_default not in choices:
if default and choices is not None and default not in choices:
self.option_strings = kwargs["option_strings"]
args = {
"value": self.env_default,
Expand Down
Loading