Skip to content

Commit

Permalink
build.hooks.make-venv: Add support for ignoring file collisions
Browse files Browse the repository at this point in the history
  • Loading branch information
adisbladis committed Dec 30, 2024
1 parent f88c319 commit 22a63d2
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 7 deletions.
23 changes: 20 additions & 3 deletions build/hooks/make-venv/make_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ class ArgsNS(argparse.Namespace):
deps: list[str]
env: list[str]
skip: list[str]
ignore_collisions: list[str]

def __init__(self):
self.out = ""
self.python = ""
self.deps = []
self.env = []
self.skip = []
self.ignore_collisions = []
super().__init__()


Expand All @@ -41,6 +43,9 @@ def __init__(self):
arg_parser.add_argument("--env", action="append", help="Source dependencies from environment variable")
arg_parser.add_argument("--deps", action="append", help="Source dependencies from colon separated list")
arg_parser.add_argument("--skip", action="append", help="Skip linking path into venv")
arg_parser.add_argument(
"--ignore-collisions", action="append", help="Ignore collisions for path, link the first path encountered"
)


class FileCollisionError(Exception):
Expand Down Expand Up @@ -87,12 +92,14 @@ def lstat(path: Path):
def merge_inputs(
inputs: list[Path],
skip_paths: Optional[list[str]] = None,
ignore_collisions: Optional[list[str]] = None,
) -> MergedInputs:
"""
Merge multiple store paths
"""

skip_paths = skip_paths or []
ignore_collisions = ignore_collisions or []

def recurse(inputs: list[Path], stack: tuple[str, ...]) -> MergedInputs:
path_rel = "/".join(stack)
Expand All @@ -117,7 +124,11 @@ def recurse(inputs: list[Path], stack: tuple[str, ...]) -> MergedInputs:
return {k: recurse(v, stack=(*stack, k)) for k, v in entries.items()}

elif any(S_ISREG(lstat(input).st_mode) for input in inputs): # Regular files
if not is_bytecode(inputs[0]) and not compare_paths(inputs):
if (
not is_bytecode(inputs[0])
and not compare_paths(inputs)
and not any(fnmatch.fnmatch(path_rel, pat) for pat in ignore_collisions)
):
raise FileCollisionError(inputs)

# Return the first regular file from input list.
Expand All @@ -143,7 +154,11 @@ def recurse(inputs: list[Path], stack: tuple[str, ...]) -> MergedInputs:

# If any file is a regular file treat the rest as such
elif resolved.is_file():
if not is_bytecode(input) and not compare_paths(inputs):
if (
not is_bytecode(input)
and not compare_paths(inputs)
and not any(fnmatch.fnmatch(path_rel, pat) for pat in ignore_collisions)
):
raise FileCollisionError(inputs)
return input

Expand Down Expand Up @@ -291,8 +306,10 @@ def main():
*(args.skip or []),
]

ignore_collisions = args.ignore_collisions or []

# Merge created venv with inputs
merged = merge_inputs([out_root, *dependencies], skip_paths)
merged = merge_inputs([out_root, *dependencies], skip_paths, ignore_collisions)

# Write merged dependencies to venv
write_venv_deps(python_bin, out_root, merged)
Expand Down
41 changes: 39 additions & 2 deletions build/hooks/make-venv/test_make_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@ def test_ambiguous_sym(self):
with self.assertRaises(FileMergeError):
merge_inputs([Path(a.name), Path(b.name)])


def test_skip(self):
"""Test skipping files"""
tree: FileTree = {"hello.py": File("hello")}
Expand Down Expand Up @@ -232,12 +231,50 @@ def test_skip_wildcard(self):
stack.enter_context(b)

self.assertEqual(
merge_inputs([Path(a.name), Path(b.name)], skip_paths=["*.py"]),
merge_inputs([Path(a.name), Path(b.name)], skip_paths=["hello.py"]),
{
"hello.py": None,
},
)

def test_ignore_collisions(self):
"""Test ignoring collisions"""
tree_a: FileTree = {"hello.py": File("hello")}
tree_b: FileTree = {"hello.py": File("goodbye")}

with contextlib.ExitStack() as stack:
a = TemporaryTree(tree_a)
stack.enter_context(a)

b = TemporaryTree(tree_b)
stack.enter_context(b)

self.assertEqual(
merge_inputs([Path(a.name), Path(b.name)], ignore_collisions=["hello.py"]),
{
"hello.py": Path(pjoin(a.name, "hello.py")),
},
)

def test_ignore_collisions_wildcard(self):
"""Test ignoring collisions using glob pattern"""
tree_a: FileTree = {"hello.py": File("hello")}
tree_b: FileTree = {"hello.py": File("goodbye")}

with contextlib.ExitStack() as stack:
a = TemporaryTree(tree_a)
stack.enter_context(a)

b = TemporaryTree(tree_b)
stack.enter_context(b)

self.assertEqual(
merge_inputs([Path(a.name), Path(b.name)], ignore_collisions=["*.py"]),
{
"hello.py": Path(pjoin(a.name, "hello.py")),
},
)


if __name__ == "__main__":
unittest.main()
10 changes: 8 additions & 2 deletions build/packages.nix
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
}:
let
inherit (resolvers) resolveCyclic resolveNonCyclic;
inherit (lib) makeScope concatStringsSep;
inherit (lib) makeScope concatStringsSep escapeShellArg;

mkPkgs' = import ./pkgs { inherit pyproject-nix lib; };

Expand Down Expand Up @@ -70,14 +70,20 @@ let
# Skip linking files into venv
venvSkip = [ ];

# Ignore collisions for paths
venvIgnoreCollisions = [ ];

nativeBuildInputs = [
pkgsFinal.pyprojectMakeVenvHook
];

env = {
NIX_PYPROJECT_DEPS = concatStringsSep ":" (pkgsFinal.resolveVirtualEnv spec);
dontMoveLib64 = true;
mkVirtualenvFlags = concatStringsSep " " (map (path: "--skip '${path}'") finalAttrs.venvSkip);
mkVirtualenvFlags = concatStringsSep " " (
map (path: "--skip ${escapeShellArg path}") finalAttrs.venvSkip
++ map (pat: "--ignore-collisions ${escapeShellArg pat}") finalAttrs.venvIgnoreCollisions
);
};

buildInputs = pkgsFinal.resolveVirtualEnv spec;
Expand Down

0 comments on commit 22a63d2

Please sign in to comment.