Skip to content

Commit

Permalink
build: Add util.mkApplication
Browse files Browse the repository at this point in the history
This helper takes a virtual environment and a derivation to use as a structure template & outputs a derivation without venv cruft.
  • Loading branch information
adisbladis committed Dec 18, 2024
1 parent e239399 commit cc489ac
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 0 deletions.
1 change: 1 addition & 0 deletions build/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ lib.fix (self: {
};
lib = import ./lib { inherit lib pyproject-nix; };
hacks = import ./hacks;
util = import ./util;
})
43 changes: 43 additions & 0 deletions build/util/checks.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{ pkgs, pyproject-nix }:

let
inherit (pkgs) lib;

util = pkgs.callPackages pyproject-nix.build.util { };

python = pkgs.python3;

buildSystems = import ../checks/build-systems.nix {
inherit lib;
};

pythonSet =
(pkgs.callPackage pyproject-nix.build.packages {
inherit python;
}).overrideScope
buildSystems;

in
{
mkApplication =
let
drv = pythonSet.pip;

venv = pythonSet.mkVirtualEnv "mkApplication-check-venv" {
pip = [ ];
};

app = util.mkApplication {
inherit venv;
package = drv;
};

in
pkgs.runCommand "mkApplication-check" { } ''
# Test run binary
${app}/bin/pip --help > /dev/null
ln -s ${app} $out
'';

}
68 changes: 68 additions & 0 deletions build/util/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{ runCommand, python3 }:

{

/**
Build applications without venv cruft.
Virtual environments contains many files that are not relevant when
distributing applications.
This includes, but is not limited to
- Python interpreter
- Activation scripts
- `pyvenv.cfg`
This helper creates a new derivation, only symlinking venv files relevant for the application.
# Example
```nix
util.mkApplication {
venv = pythonSet.mkVirtualEnv "mkApplication-check-venv" {
pip = [ ];
}
package = pythonSet.pip;
}
=>
«derivation /nix/store/i60rydd6sagcgrsz9cx0la30djzpa8k9-pip-24.0.drv»
```
# Type
```
mkApplication :: AttrSet -> derivation
```
# Arguments
venv
: Virtualenv derivation created using `mkVirtualEnv`
package
: Python set package
*/
mkApplication =
{
venv,
package,
pname ? package.pname,
version ? package.version,
}:
runCommand "${pname}-${version}"
{
inherit (package)
name
pname
version
meta
passthru
;
nativeBuildInputs = [
python3
];
}
''
python3 ${./mk-application.py} --venv ${venv} --base ${package} --out "$out"
'';

}
131 changes: 131 additions & 0 deletions build/util/mk-application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#!/usr/bin/env python
import argparse
from pathlib import Path
from stat import S_ISDIR, S_ISLNK, S_ISREG
from typing import Union


class ArgsNS(argparse.Namespace):
venv: str
base: str
out: str

def __init__(self):
self.venv = ""
self.base = ""
self.out = ""
super().__init__()


arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("--venv", required=True)
arg_parser.add_argument("--base", help="Derivation output to use as structure template", required=True)
arg_parser.add_argument("--out", required=True)


DirectoryStructure = Union[None, dict[str, "DirectoryStructure"]]


SKIP_PATTERNS = (
("nix-support",),
(
"lib",
None,
"site-packages",
),
)


def do_skip(stack: tuple[str, ...]) -> bool:
"""Check if a stack matches a skip pattern"""
for pattern in SKIP_PATTERNS:
if len(stack) != len(pattern):
continue

for tpl, tok in zip(pattern, stack):
if tpl is None:
continue
if tpl != tok:
break
else:
return True

return False


def get_structure(root: Path) -> DirectoryStructure:
"""Get structure from package"""

def recurse(stack: tuple[str, ...], root: Path) -> DirectoryStructure:
st_mode = root.lstat().st_mode

if S_ISDIR(st_mode):
ret: DirectoryStructure = {}

for child in root.iterdir():
# Check if the current tree position is a skipped node
stack_child = (*stack, child.name)
if do_skip(stack):
continue

# Traverse into child
structure = recurse(stack_child, child)

# Append to output structure if result is file or non-empty directory.
# Note that the child may have been filtered by a skipped node,
# and may be returned as empty even if the directory is not.
if structure is None or structure:
ret[child.name] = structure

return ret

elif S_ISREG(st_mode):
return None

# An input file may be a symlink if created by hacks.nixpkgsPrebuilt or if created by an override
# We still want to traverse into this symlink to copy it's structure to the output.
elif S_ISLNK(st_mode):
return recurse(stack, root.resolve())

else:
raise ValueError(f"Unsupported file type for {root}")

return recurse((), root)


def write_structure(
structure: DirectoryStructure,
venv: Path,
out: Path,
):
"""Write out directory structure"""

def recurse(ds: DirectoryStructure, stack: tuple[str, ...]):
dst = out.joinpath(*stack)
if isinstance(ds, dict):
dst.mkdir()
for name, value in ds.items():
recurse(value, (*stack, name))
else:
target = venv.joinpath(*stack)

# Target may not exist because it's filtered by the venv creation script.
if target.exists():
dst.symlink_to(target)

recurse(structure, ())


def main():
args = arg_parser.parse_args(namespace=ArgsNS)

base = Path(args.base)
venv = Path(args.venv)
out = Path(args.out)

structure = get_structure(base)
write_structure(structure, venv, out)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions doc/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
- [resolvers](./build/lib/resolvers.md)
- [packages.hooks](./build/hooks.md)
- [hacks](./build/hacks.md)
- [util](./build/util.md)

# Contributing

Expand Down
1 change: 1 addition & 0 deletions doc/src/build/util.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<!-- cmdrun nixdoc --prefix build --category util --description build.util --file ../../../build/util/default.nix -->
5 changes: 5 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@
pyproject-nix = self;
}
))
// (lib.mapAttrs' (name: drv: lib.nameValuePair "build-util-${name}" drv) (
pkgs.callPackages ./build/util/checks.nix {
pyproject-nix = self;
}
))
// {
formatter =
pkgs.runCommand "fmt-check"
Expand Down

0 comments on commit cc489ac

Please sign in to comment.