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 5, 2024
1 parent 6aadf21 commit de4425d
Show file tree
Hide file tree
Showing 7 changed files with 193 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;
from = 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 = [ ];
}
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"
'';

}
74 changes: 74 additions & 0 deletions build/util/mk-application.py
Original file line number Diff line number Diff line change
@@ -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)
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 de4425d

Please sign in to comment.