]> git.scottworley.com Git - nix-pin-deps/blame - nix_pin_deps.py
Start keeping a changelog
[nix-pin-deps] / nix_pin_deps.py
CommitLineData
cf8e9f60
SW
1# nix-pin-deps: gc-pin dependencies of a partially-built derivation
2#
3# This program is free software: you can redistribute it and/or modify it
4# under the terms of the GNU General Public License as published by the
5# Free Software Foundation, version 3.
6
7
1e40791b
SW
8from contextlib import contextmanager
9import json
10import os
11import subprocess
12import sys
13import xml.sax
14import xml.sax.handler
15
58265043
SW
16from typing import Any, Dict, Iterator, List, Set
17
1e40791b
SW
18
19@contextmanager
58265043 20def log(msg: str) -> Iterator[None]:
1e40791b
SW
21 print(msg, file=sys.stderr, end='', flush=True)
22 try:
23 yield
24 finally:
25 print('\r', file=sys.stderr, end='', flush=True)
26
27
28class ParseNixStoreQueryGraphML(xml.sax.handler.ContentHandler):
58265043 29 def __init__(self) -> None:
07fdf185 30 super().__init__()
58265043
SW
31 self.roots: Set[str] = set()
32 self.non_roots: Set[str] = set()
33 self.deps: Dict[str, List[str]] = {}
1e40791b 34
58265043 35 def startElement(self, name: str, attrs: Any) -> None:
1e40791b
SW
36 if name == "edge":
37 source = attrs.getValue("source")
38 target = attrs.getValue("target")
39 self.non_roots.add(target)
40 self.deps.setdefault(source, []).append(target)
41 if target in self.roots:
42 self.roots.remove(target)
43 if source not in self.non_roots:
44 self.roots.add(source)
45
46
58265043 47def getDeps(drv: str) -> ParseNixStoreQueryGraphML:
1e40791b 48 with log("Loading dependency tree..."):
07fdf185
SW
49 with subprocess.Popen(
50 ["nix-store", "--query", "--graphml", drv], stdout=subprocess.PIPE) as process:
51 parser = ParseNixStoreQueryGraphML()
52 assert process.stdout
53 xml.sax.parse(process.stdout, parser)
54 assert process.wait() == 0
1e40791b
SW
55 return parser
56
57
58265043 58def getDrvInfo(drv: str) -> Any:
07fdf185
SW
59 with log(f"Loading {drv}..."):
60 with subprocess.Popen(
61 ["nix", "--experimental-features", "nix-command",
62 "show-derivation", "/nix/store/" + drv],
63 stdout=subprocess.PIPE) as process:
64 assert process.stdout
65 info = json.load(process.stdout)
66 assert process.wait() == 0
1e40791b
SW
67 return info
68
69
58265043 70def allBuilt(drv: str) -> bool:
1e40791b
SW
71 # TODO: How to pin outputs one at a time?
72 # Currently, we only pin when all outputs are available.
73 # It would be better to pin the outputs we've got.
74 return all(os.path.exists(o['path']) for d in getDrvInfo(
75 drv).values() for o in d['outputs'].values())
76
77
58265043 78def isDrv(thing: str) -> bool:
1e40791b
SW
79 return thing.endswith(".drv")
80
81
58265043 82def removesuffix(s: str, suf: str) -> str:
1e40791b
SW
83 return s[:-len(suf)] if s.endswith(suf) else s
84
85
58265043 86def pin(drv: str) -> None:
1e40791b
SW
87 outPath = os.path.join(sys.argv[2], removesuffix(drv, ".drv"))
88 if not os.path.exists(outPath):
07fdf185
SW
89 subprocess.run(["nix-store", "--realise", "--add-root",
90 outPath, "/nix/store/" + drv], check=True)
1e40791b
SW
91
92
58265043
SW
93def pinBuiltThings(thing: str,
94 done: Set[str],
95 deps: Dict[str, List[str]]) -> None:
1e40791b
SW
96 if thing in done:
97 return
98 done.add(thing)
99 if not isDrv(thing) or allBuilt(thing):
100 pin(thing)
101 return
102 for dep in deps.get(thing, []):
103 pinBuiltThings(dep, done, deps)
104
105
107c5850
SW
106def main() -> None:
107 dep_graph = getDeps(sys.argv[1])
108 for root in dep_graph.roots:
109 pinBuiltThings(root, set(), dep_graph.deps)
110
111
112if __name__ == '__main__':
113 main()