Repository: pypa/auditwheel

Issue:
auditwheel 6.3.0 crashes when TMPDIR contains symlink path components
When TMPDIR is set to a path with a component that is a symlink, repairing a wheel crashes as seen in https://github.com/passagemath/passagemath/pull/676#issuecomment-2765241528

This is a regression from 6.2.0

Discussion:

Solution patch:
diff --git a/src/auditwheel/tmpdirs.py b/src/auditwheel/tmpdirs.py
index b214b5c5..b5fe2c2d 100644
--- a/src/auditwheel/tmpdirs.py
+++ b/src/auditwheel/tmpdirs.py
@@ -27,15 +27,17 @@ class InTemporaryDirectory:
 
     def __init__(self) -> None:
         self._tmpdir = TemporaryDirectory()
+        self._name = Path(self._tmpdir.name).resolve(strict=True)
 
     @property
     def name(self) -> Path:
-        return Path(self._tmpdir.name)
+        return self._name
 
     def __enter__(self) -> Path:
         self._pwd = Path.cwd()
-        os.chdir(self._tmpdir.name)
-        return Path(self._tmpdir.__enter__())
+        os.chdir(self._name)
+        self._tmpdir.__enter__()
+        return self._name
 
     def __exit__(
         self,
@@ -44,7 +46,7 @@ def __exit__(
         tb: TracebackType | None,
     ) -> None:
         os.chdir(self._pwd)
-        return self._tmpdir.__exit__(exc, value, tb)
+        self._tmpdir.__exit__(exc, value, tb)
 
 
 class InGivenDirectory:
diff --git a/src/auditwheel/tools.py b/src/auditwheel/tools.py
index 423cfa89..a84532e4 100644
--- a/src/auditwheel/tools.py
+++ b/src/auditwheel/tools.py
@@ -142,6 +142,9 @@ def dir2zip(
         st = in_dir.stat()
         date_time = datetime.fromtimestamp(st.st_mtime, tz=timezone.utc)
     date_time_args = date_time.timetuple()[:6]
+    if date_time_args < (1980, 1, 1, 0, 0, 0):
+        logger.warning("dir2zip, clipping timestamp to 1980-01-01")
+        date_time_args = (1980, 1, 1, 0, 0, 0)
     compression = zipfile.ZIP_DEFLATED
     with zipfile.ZipFile(zip_fname, "w", compression=compression) as z:
         for dname, _, files in walk(in_dir):


Test patch:
diff --git a/tests/integration/test_bundled_wheels.py b/tests/integration/test_bundled_wheels.py
index aa11ed1a..28065a00 100644
--- a/tests/integration/test_bundled_wheels.py
+++ b/tests/integration/test_bundled_wheels.py
@@ -3,7 +3,6 @@
 import importlib
 import os
 import platform
-import subprocess
 import sys
 import zipfile
 from argparse import Namespace
@@ -138,29 +137,25 @@ def test_analyze_wheel_abi_static_exe(caplog):
         assert result.overall_policy.name == "manylinux_2_5_x86_64"
 
 
-@pytest.mark.skipif(platform.machine() != "x86_64", reason="only checked on x86_64")
-def test_wheel_source_date_epoch(tmp_path, monkeypatch):
-    wheel_build_path = tmp_path / "wheel"
-    subprocess.run(
-        [
-            sys.executable,
-            "-m",
-            "pip",
-            "wheel",
-            "--no-deps",
-            "-w",
-            wheel_build_path,
-            HERE / "sample_extension",
-        ],
-        check=True,
+@pytest.mark.parametrize(
+    "timestamp",
+    [
+        (0, 315532800),  # zip timestamp starts 1980-01-01, not 1970-01-01
+        (315532799, 315532800),  # zip timestamp starts 1980-01-01, not 1970-01-01
+        (315532801, 315532800),  # zip timestamp round odd seconds down to even seconds
+        (315532802, 315532802),
+        (650203201, 650203200),  # zip timestamp round odd seconds down to even seconds
+    ],
+)
+def test_wheel_source_date_epoch(timestamp, tmp_path, monkeypatch):
+    wheel_path = (
+        HERE / "arch-wheels/musllinux_1_2/testsimple-0.0.1-cp312-cp312-linux_x86_64.whl"
     )
-
-    wheel_path, *_ = list(wheel_build_path.glob("*.whl"))
     wheel_output_path = tmp_path / "out"
     args = Namespace(
         LIB_SDIR=".libs",
         ONLY_PLAT=False,
-        PLAT="manylinux_2_5_x86_64",
+        PLAT="auto",
         STRIP=False,
         UPDATE_TAGS=True,
         WHEEL_DIR=wheel_output_path,
@@ -173,7 +168,8 @@ def test_wheel_source_date_epoch(tmp_path, monkeypatch):
         prog="auditwheel",
         verbose=1,
     )
-    monkeypatch.setenv("SOURCE_DATE_EPOCH", "650203200")
+
+    monkeypatch.setenv("SOURCE_DATE_EPOCH", str(timestamp[0]))
     # patchelf might not be available as we aren't running in a manylinux container
     # here. We don't need need it in this test, so just patch it.
     with mock.patch("auditwheel.patcher._verify_patchelf"):
@@ -182,6 +178,5 @@ def test_wheel_source_date_epoch(tmp_path, monkeypatch):
     output_wheel, *_ = list(wheel_output_path.glob("*.whl"))
     with zipfile.ZipFile(output_wheel) as wheel_file:
         for file in wheel_file.infolist():
-            assert (
-                datetime(*file.date_time, tzinfo=timezone.utc).timestamp() == 650203200
-            )
+            file_date_time = datetime(*file.date_time, tzinfo=timezone.utc)
+            assert file_date_time.timestamp() == timestamp[1]
diff --git a/tests/unit/test_wheeltools.py b/tests/unit/test_wheeltools.py
index 7187b2ba..14915d02 100644
--- a/tests/unit/test_wheeltools.py
+++ b/tests/unit/test_wheeltools.py
@@ -1,6 +1,8 @@
 from __future__ import annotations
 
 import re
+import tempfile
+from pathlib import Path
 
 import pytest
 
@@ -8,11 +10,14 @@
 from auditwheel.error import NonPlatformWheel
 from auditwheel.libc import Libc
 from auditwheel.wheeltools import (
+    InWheelCtx,
     WheelToolsError,
     get_wheel_architecture,
     get_wheel_libc,
 )
 
+HERE = Path(__file__).parent.resolve()
+
 
 @pytest.mark.parametrize(
     ("filename", "expected"),
@@ -76,3 +81,19 @@ def test_get_wheel_libc_multiple(filename: str) -> None:
     match = re.escape("multiple libc are not supported")
     with pytest.raises(WheelToolsError, match=match):
         get_wheel_libc(filename)
+
+
+def test_inwheel_tmpdir(tmp_path, monkeypatch):
+    wheel_path = (
+        HERE
+        / "../integration/arch-wheels/glibc/testsimple-0.0.1-cp313-cp313-linux_x86_64.whl"
+    )
+    tmp_path = tmp_path.resolve(strict=True)
+    tmpdir = tmp_path / "tmpdir"
+    tmpdir.mkdir()
+    tmpdir_symlink = tmp_path / "symlink"
+    tmpdir_symlink.symlink_to(str(tmpdir), target_is_directory=True)
+    monkeypatch.setattr(tempfile, "gettempdir", lambda: str(tmpdir_symlink))
+    with InWheelCtx(wheel_path, tmp_path / wheel_path.name) as context:
+        Path(context._tmpdir.name).relative_to(tmpdir_symlink)
+        context.name.relative_to(tmpdir)