diff --git a/tests/functional/meson.build b/tests/functional/meson.build index ddf2dbb6c..aec173d01 100644 --- a/tests/functional/meson.build +++ b/tests/functional/meson.build @@ -72,7 +72,6 @@ functional_tests_scripts = [ 'export-graph.sh', 'fetchGitRefs.sh', 'gc-runtime.sh', - 'tarball.sh', 'fetchers.sh', 'fetchGit.sh', 'fetchGitLocked.sh', diff --git a/tests/functional/tarball.sh b/tests/functional/tarball.sh deleted file mode 100644 index 72b6032da..000000000 --- a/tests/functional/tarball.sh +++ /dev/null @@ -1,72 +0,0 @@ -source common.sh - -clearStore - -rm -rf $TEST_HOME - -tarroot=$TEST_ROOT/tarball -rm -rf $tarroot -mkdir -p $tarroot -cp dependencies.nix $tarroot/default.nix -cp config.nix dependencies.builder*.sh $tarroot/ -touch -d '@1000000000' $tarroot $tarroot/* - -hash=$(nix hash path $tarroot) - -test_tarball() { - local ext="$1" - local compressor="$2" - - tarball=$TEST_ROOT/tarball.tar$ext - (cd $TEST_ROOT && GNUTAR_REPRODUCIBLE= tar --mtime=$tarroot/default.nix --owner=0 --group=0 --numeric-owner --sort=name -c -f - tarball) | $compressor > $tarball - - nix-env -f file://$tarball -qa --out-path | grepQuiet dependencies - - nix-build -o $TEST_ROOT/result file://$tarball - - nix-build -o $TEST_ROOT/result '' -I foo=file://$tarball - - nix-build -o $TEST_ROOT/result -E "import (fetchTarball \"file://$tarball\")" - # Do not re-fetch paths already present - nix-build -o $TEST_ROOT/result -E "import (fetchTarball { url = \"file:///does-not-exist/must-remain-unused/$tarball\"; sha256 = \"$hash\"; })" - - # Regression test: Ensure tarballs can be unpacked when the - # temporary directory is a symbolic link. - ( - export HOME=$(mktemp -d) - ln -sf "$TMPDIR" "$HOME/tmp" - nix-build -o "$TEST_ROOT/result" --temp-dir "$HOME/tmp" -E "import (fetchTarball \"file://$tarball\")" - ) - - nix-build -o $TEST_ROOT/result -E "import (fetchTree \"file://$tarball\")" - nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = \"file://$tarball\"; })" - nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = \"file://$tarball\"; narHash = \"$hash\"; })" - # Do not re-fetch paths already present - nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = \"file:///does-not-exist/must-remain-unused/$tarball\"; narHash = \"$hash\"; })" - expectStderr 102 nix-build -o $TEST_ROOT/result -E "import (fetchTree { type = \"tarball\"; url = \"file://$tarball\"; narHash = \"sha256-xdKv2pq/IiwLSnBBJXW8hNowI4MrdZfW+SYqDQs7Tzc=\"; })" | grep 'NAR hash mismatch in input' - - [[ $(nix eval --impure --expr "(fetchTree \"file://$tarball\").lastModified") = 1000000000 ]] - - nix-instantiate --strict --eval -E "!((import (fetchTree { type = \"tarball\"; url = \"file://$tarball\"; narHash = \"$hash\"; })) ? submodules)" >&2 - nix-instantiate --strict --eval -E "!((import (fetchTree { type = \"tarball\"; url = \"file://$tarball\"; narHash = \"$hash\"; })) ? submodules)" 2>&1 | grep 'true' - - nix-instantiate --eval -E '1 + 2' -I fnord=file://no-such-tarball.tar$ext - nix-instantiate --eval -E 'with ; 1 + 2' -I fnord=file://no-such-tarball$ext - (! nix-instantiate --eval -E ' 1' -I fnord=file://no-such-tarball$ext) - - nix-instantiate --eval -E '' -I fnord=file://no-such-tarball$ext -I fnord=. - - # Ensure that the `name` attribute isn’t accepted as that would mess - # with the content-addressing - (! nix-instantiate --eval -E "fetchTree { type = \"tarball\"; url = \"file://$tarball\"; narHash = \"$hash\"; name = \"foo\"; }") - -} - -test_tarball '' cat -test_tarball .xz xz -test_tarball .gz gzip - -rm -rf $TEST_ROOT/tmp -mkdir -p $TEST_ROOT/tmp -(! TMPDIR=$TEST_ROOT/tmp XDG_RUNTIME_DIR=$TEST_ROOT/tmp nix-env -f file://$(pwd)/bad.tar.xz -qa --out-path) -(! [ -e $TEST_ROOT/tmp/bad ]) diff --git a/tests/functional/bad.tar.xz b/tests/functional2/commands/test_build/assets/test_tarball/bad.tar.xz similarity index 100% rename from tests/functional/bad.tar.xz rename to tests/functional2/commands/test_build/assets/test_tarball/bad.tar.xz diff --git a/tests/functional2/commands/test_build/test_tarball.py b/tests/functional2/commands/test_build/test_tarball.py new file mode 100644 index 000000000..91b39819a --- /dev/null +++ b/tests/functional2/commands/test_build/test_tarball.py @@ -0,0 +1,224 @@ +import os +import shutil +from pathlib import Path + +import pytest +from _pytest.fixtures import FixtureRequest + +from functional2.testlib.fixtures.command import Command +from functional2.testlib.fixtures.file_helper import with_files, Symlink, CopyFile +from functional2.testlib.fixtures.nix import Nix +from functional2.testlib.utils import get_global_asset_pack, get_global_asset + + +TAR_FILES = { + "tarball": get_global_asset_pack("dependencies") + | {"default.nix": get_global_asset("dependencies/dependencies.nix")} +} + + +def _set_mtime_of_folder(path: Path, mtime: int = 1000000000): + for file in [path] + list(path.iterdir()) if path.is_dir() else []: + os.utime(file, (mtime, mtime)) + + +@pytest.fixture(params=[("", "cat"), (".xz", "xz"), (".gz", "gzip")]) +def tarball(request: FixtureRequest, nix: Nix, files: Path) -> Path: + ext, compressor = request.param + # setup + _set_mtime_of_folder(files / "tarball") + env = nix.env + env["GNUTAR_REPRODUCIBLE"] = "" + tar_exe = shutil.which("tar") + env.path.add_program("tar") + tarball_name = f"tarball.tar{ext}" + tarball_path = files / tarball_name + + # Create tarball + tarball_content = ( + Command( + [ + "tar", + f"--mtime={files / 'tarball' / 'default.nix'}", + "--owner=0", + "--group=0", + "--numeric-owner", + "--sort=name", + f"--to-command={compressor}", + "-c", + "-f", + "-", + "tarball", + ], + env, + exe=tar_exe, + ) + .run() + .ok() + .stdout + ) + tarball_path.write_bytes(tarball_content) + + res = nix.nix_env(["-f", f"file://{tarball_path}", "-qa", "--out-path"], build=True).run().ok() + assert "dependencies" in res.stdout_plain + + return tarball_path + + +@pytest.fixture +def tar_hash(files: Path, nix: Nix) -> str: + return nix.hash_path(files / "tarball") + + +@with_files(TAR_FILES) +@pytest.mark.parametrize( + "flags", + [ + ["file://{tarball}"], # noqa: RUF027 # yes, true, but we don't have the variables here + ["", "-I", "foo=file://{tarball}"], # noqa: RUF027 + ["-E", 'import (fetchTarball "file://{tarball}")'], # noqa: RUF027 + # Do not re-fetch paths already present + [ + "-E", + 'import (fetchTarball {{ url = "file:///does-not-exist/must-remain-unused/{tarball}"; sha256 = "{tar_hash}"; }})', # noqa: RUF027 + ], + ], +) +def test_fetch_tarball(nix: Nix, tarball: Path, tar_hash: str, flags: list[str]): + # HACK(Commentator2.0): if we don't create a copy, we'd try to format it twice, as it is the same list used in other test calls + flags = flags[:] + + flags[-1] = flags[-1].format(tarball=tarball, tar_hash=tar_hash) + nix.nix_build(["-o", "result", *flags]).run().ok() + + +@with_files(TAR_FILES | {"actual-tmp-dir": {}, "tmp-dir": Symlink("./actual-tmp-dir")}) +def test_tarball_symlink_extraction(nix: Nix, files: Path, tarball: Path): + nix.env.dirs.tmpdir = files / "tmp-dir" + + nix.nix_build( + ["-o", "result", "-E", f'import (fetchTarball "file://{files / tarball.name}")'] + ).run().ok() + + real_tmp_dir = nix.env.dirs.test_root / "tmp" + real_tmp_dir.mkdir(exist_ok=True) + nix.env.dirs.tmpdir = real_tmp_dir + + nix.nix_build( + [ + "-o", + "result", + "--temp-dir", + f"{files}/tmp-dir", + "-E", + f'import (fetchTarball "file://{files / tarball.name}")', + ] + ).run().ok() + + +@with_files(TAR_FILES) +@pytest.mark.parametrize( + "expr", + [ + 'import (fetchTree "file://{tarball}")', # noqa: RUF027 # yes, true, but we don't have the variables here + 'import (fetchTree {{ type = "tarball"; url = "file://{tarball}"; }})', # noqa: RUF027 + 'import (fetchTree {{ type = "tarball"; url = "file://{tarball}"; narHash = "{tar_hash}"; }})', # noqa: RUF027 + ], +) +def test_fetch_tree(nix: Nix, tarball: Path, tar_hash: str, expr: str): + expr = expr.format(tarball=tarball, tar_hash=tar_hash) + nix.nix_build(["-o", "result", "-E", expr], flake=True).run().ok() + + +@with_files(TAR_FILES) +def test_fetch_tree_hash_mismatch(nix: Nix, tarball: Path): + res = ( + nix.nix_build( + [ + "-o", + "result", + "-E", + f'import (fetchTree {{ type = "tarball"; url = "file://{tarball}"; narHash = "sha256-xdKv2pq/IiwLSnBBJXW8hNowI4MrdZfW+SYqDQs7Tzc="; }})', + ], + flake=True, + ) + .run() + .expect(102) + ) + assert "NAR hash mismatch in input" in res.stderr_plain + + +@with_files(TAR_FILES) +def test_last_modified(nix: Nix, tarball: Path): + res = ( + nix.nix( + ["eval", "--impure", "--expr", f'(fetchTree "file://{tarball}").lastModified'], + flake=True, + build=True, + ) + .run() + .ok() + ) + assert res.stdout_plain == "1000000000" + + +@with_files({"config.nix": get_global_asset("config.nix")}) +@pytest.mark.parametrize( + ("flags", "exit_code"), + [ + (["1 + 2"], 0), + (["with ; 1 + 2"], 0), + (["", "-I", "fnord=."], 0), + ([" 1"], 1), + ], +) +def test_no_accessing_tar(nix: Nix, flags: list[str], exit_code: int): + nix.nix_instantiate( + ["--eval", "-I", "fnord=file://no-such-tarball.tar.gz", "-E", *flags] + ).run().expect(exit_code) + + +@with_files(TAR_FILES) +def test_no_submodules(nix: Nix, tarball: Path, tar_hash: str): + res = ( + nix.nix_instantiate( + [ + "--strict", + "--eval", + "-E", + f'!((fetchTree {{ type = "tarball"; url = "file://{tarball}"; narHash = "{tar_hash}"; }})) ? submodules', + ], + flake=True, + build=True, + ) + .run() + .ok() + ) + assert res.stdout_plain == "true" + + +@with_files(TAR_FILES) +def test_no_accessing_name(nix: Nix, tarball: Path, tar_hash: str): + """ + Ensure that the `name` attribute isn't accepted as that would mess with the content-addressing + """ + res = ( + nix.nix_instantiate( + [ + "--eval", + "-E", + f'fetchTree {{ type = "tarball"; url = "file://{tarball}"; narHash = "{tar_hash}"; name = "foo"; }}', + ], + flake=True, + ) + .run() + .expect(1) + ) + assert "error: attribute 'name' isn’t supported in call" in res.stderr_plain # noqa: RUF001 # for some reason, this error message wants to feel special + + +@with_files({"bad.tar.xz": CopyFile("assets/test_tarball/bad.tar.xz")}) +def test_nix_env_bad_tarball(nix: Nix, files: Path): + res = nix.nix_env(["-f", f"file://{files / 'bad.tar.xz'}", "-qa", "--out-path"]).run().expect(1) + assert "error: failed to extract archive (Path contains '..')" in res.stderr_plain + assert not (nix.env.dirs.tmpdir / "bad").exists() diff --git a/tests/functional2/testlib/fixtures/env.py b/tests/functional2/testlib/fixtures/env.py index bfbe8cc61..55740e53c 100644 --- a/tests/functional2/testlib/fixtures/env.py +++ b/tests/functional2/testlib/fixtures/env.py @@ -139,6 +139,7 @@ class _Dirs: nix_store_dir: Path | None cache_dir: Path | None xdg_cache_home: Path | None + tmpdir: Path | None """used for nar caching""" def get_env_keys(self) -> set[str]: @@ -176,6 +177,7 @@ class ManagedEnv: nix_store_dir=self._get_dir("nix/store"), cache_dir=self._get_dir("binary-cache"), xdg_cache_home=self._get_dir("test-home/.cache"), + tmpdir=self._get_dir("tmp"), ) self.path.prepend(self.dirs.nix_bin_dir) self.init_defaults(global_path) diff --git a/tests/functional2/testlib/fixtures/test_env.py b/tests/functional2/testlib/fixtures/test_env.py index 77b863f55..0775bdabc 100644 --- a/tests/functional2/testlib/fixtures/test_env.py +++ b/tests/functional2/testlib/fixtures/test_env.py @@ -128,6 +128,7 @@ def test_env_to_env(tmp_path: Path): "XDG_CACHE_HOME", "PATH", "BUILD_TEST_SHELL", + "TMPDIR", } | ({"_NIX_TEST_NO_SANDBOX"} if sys.platform == "darwin" else set())