From de4425d67779a5ed6073da5083fff45f6a1837da Mon Sep 17 00:00:00 2001 From: adisbladis Date: Thu, 5 Dec 2024 03:04:19 +0000 Subject: [PATCH] build: Add util.mkApplication This helper takes a virtual environment and a derivation to use as a structure template & outputs a derivation without venv cruft. --- build/default.nix | 1 + build/util/checks.nix | 43 +++++++++++++++++++++ build/util/default.nix | 68 +++++++++++++++++++++++++++++++++ build/util/mk-application.py | 74 ++++++++++++++++++++++++++++++++++++ doc/src/SUMMARY.md | 1 + doc/src/build/util.md | 1 + flake.nix | 5 +++ 7 files changed, 193 insertions(+) create mode 100644 build/util/checks.nix create mode 100644 build/util/default.nix create mode 100644 build/util/mk-application.py create mode 100644 doc/src/build/util.md diff --git a/build/default.nix b/build/default.nix index 618823b..757b3df 100644 --- a/build/default.nix +++ b/build/default.nix @@ -7,4 +7,5 @@ lib.fix (self: { }; lib = import ./lib { inherit lib pyproject-nix; }; hacks = import ./hacks; + util = import ./util; }) diff --git a/build/util/checks.nix b/build/util/checks.nix new file mode 100644 index 0000000..e9f87dd --- /dev/null +++ b/build/util/checks.nix @@ -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; + from = drv; + }; + + in + pkgs.runCommand "mkApplication-check" { } '' + # Test run binary + ${app}/bin/pip --help > /dev/null + + ln -s ${app} $out + ''; + +} diff --git a/build/util/default.nix b/build/util/default.nix new file mode 100644 index 0000000..527372d --- /dev/null +++ b/build/util/default.nix @@ -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 = [ ]; + } + from = pythonSet.pip; + } + => + «derivation /nix/store/i60rydd6sagcgrsz9cx0la30djzpa8k9-mkApplication-check.drv» + ``` + + # Type + + ``` + mkApplication :: AttrSet -> derivation + ``` + + # Arguments + + venv + : Virtualenv derivation created using `mkVirtualEnv` + + from + : Python set package + */ + mkApplication = + { + venv, + from, + pname ? from.pname, + version ? from.version, + }: + runCommand "${pname}-${version}" + { + inherit (from) + name + pname + version + meta + passthru + ; + nativeBuildInputs = [ + python3 + ]; + } + '' + python3 ${./mk-application.py} --venv ${venv} --base ${from} --out "$out" + ''; + +} diff --git a/build/util/mk-application.py b/build/util/mk-application.py new file mode 100644 index 0000000..dd68fbd --- /dev/null +++ b/build/util/mk-application.py @@ -0,0 +1,74 @@ +#!/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[Path, dict[str, "DirectoryStructure"]] + + +def get_structure(root: Path) -> DirectoryStructure: + """Get structure from package""" + st_mode = root.lstat().st_mode + + if S_ISDIR(st_mode): + return {child.name: get_structure(child) for child in root.iterdir()} + + elif S_ISLNK(st_mode): + return get_structure(root.resolve()) + + elif S_ISREG(st_mode): + return root + + else: + raise ValueError(f"Unsupported file type for {root}") + + +def relink_structure(base: Path, venv: Path, ds: DirectoryStructure) -> DirectoryStructure: + """Relink structure from base to venv""" + if isinstance(ds, dict): + return {name: relink_structure(base, venv, value) for name, value in ds.items()} + else: + return Path(str(ds).replace(str(base), str(venv))) + + +def write_structure(root: Path, ds: DirectoryStructure): + """Write out directory structure""" + if isinstance(ds, dict): + root.mkdir() + for name, value in ds.items(): + write_structure(root.joinpath(name), value) + else: + root.symlink_to(ds) + + +if __name__ == "__main__": + args = arg_parser.parse_args(namespace=ArgsNS) + + base = Path(args.base) + venv = Path(args.venv) + out = Path(args.out) + + base_struct = get_structure(base) + venv_struct = relink_structure(base, venv, base_struct) + + write_structure(out, venv_struct) diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index a551fcb..5d6b605 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -48,6 +48,7 @@ - [resolvers](./build/lib/resolvers.md) - [packages.hooks](./build/hooks.md) - [hacks](./build/hacks.md) + - [util](./build/util.md) # Contributing diff --git a/doc/src/build/util.md b/doc/src/build/util.md new file mode 100644 index 0000000..b7f14f4 --- /dev/null +++ b/doc/src/build/util.md @@ -0,0 +1 @@ + diff --git a/flake.nix b/flake.nix index 0779e40..558e4d2 100644 --- a/flake.nix +++ b/flake.nix @@ -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"