Skip to content

Commit 42dd4c6

Browse files
committed
local evaluation: use nixpkgs ci.eval parallel implementation
1 parent 6da252a commit 42dd4c6

File tree

3 files changed

+154
-95
lines changed

3 files changed

+154
-95
lines changed

nixpkgs_review/eval_ci.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from pathlib import Path
2+
3+
from .utils import System, sh
4+
5+
6+
def _ci_command(
7+
worktree_dir: Path,
8+
command: str,
9+
output_dir: str,
10+
options: dict[str, str] | None = None,
11+
args: dict[str, str] | None = None,
12+
) -> None:
13+
cmd: list[str] = [
14+
"nix-build",
15+
str(worktree_dir.joinpath(Path("ci"))),
16+
"-A",
17+
f"eval.{command}",
18+
]
19+
if options is not None:
20+
for option, value in options.items():
21+
cmd.extend([option, value])
22+
23+
if args is not None:
24+
for arg, value in args.items():
25+
cmd.extend(["--arg", arg, value])
26+
27+
cmd.extend(["--out-link", output_dir])
28+
sh(cmd, capture_output=True)
29+
30+
31+
def local_eval(
32+
worktree_dir: Path,
33+
systems: set[System],
34+
max_jobs: int,
35+
n_cores: int,
36+
chunk_size: int,
37+
output_dir: str,
38+
) -> None:
39+
options: dict[str, str] = {
40+
"--max-jobs": str(max_jobs),
41+
"--cores": str(n_cores),
42+
}
43+
44+
eval_systems: str = " ".join(f'"{system}"' for system in systems)
45+
eval_systems = f"[{eval_systems}]"
46+
args: dict[str, str] = {
47+
"evalSystems": eval_systems,
48+
"chunkSize": str(chunk_size),
49+
}
50+
51+
_ci_command(
52+
worktree_dir=worktree_dir,
53+
command="full",
54+
options=options,
55+
args=args,
56+
output_dir=output_dir,
57+
)
58+
59+
60+
def compare(
61+
worktree_dir: Path,
62+
before_dir: str,
63+
after_dir: str,
64+
output_dir: str,
65+
) -> None:
66+
args: dict[str, str] = {
67+
"beforeResultDir": before_dir,
68+
"afterResultDir": after_dir,
69+
}
70+
_ci_command(
71+
worktree_dir=worktree_dir,
72+
command="compare",
73+
args=args,
74+
output_dir=output_dir,
75+
)

nixpkgs_review/review.py

Lines changed: 69 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import argparse
2-
import concurrent.futures
2+
import json
3+
import multiprocessing
34
import os
45
import subprocess
56
import sys
@@ -8,9 +9,10 @@
89
from enum import Enum
910
from pathlib import Path
1011
from re import Pattern
11-
from typing import IO
12+
from typing import IO, Any
1213
from xml.etree import ElementTree as ET
1314

15+
from . import eval_ci
1416
from .allow import AllowedFeatures
1517
from .builddir import Builddir
1618
from .errors import NixpkgsReviewError
@@ -207,7 +209,10 @@ def apply_unstaged(self, staged: bool = False) -> None:
207209
sys.exit(1)
208210

209211
def build_commit(
210-
self, base_commit: str, reviewed_commit: str | None, staged: bool = False
212+
self,
213+
base_commit: str,
214+
reviewed_commit: str | None,
215+
staged: bool = False,
211216
) -> dict[System, list[Attr]]:
212217
"""
213218
Review a local git commit
@@ -216,45 +221,80 @@ def build_commit(
216221

217222
print("Local evaluation for computing rebuilds")
218223

219-
# TODO: nix-eval-jobs ?
220-
base_packages: dict[System, list[Package]] = list_packages(
221-
self.builddir.nix_path,
222-
self.systems,
223-
self.allow,
224-
n_threads=self.num_parallel_evals,
225-
)
224+
# Source: https://github.com/NixOS/nixpkgs/blob/master/ci/eval/README.md
225+
# TODO: make those overridable
226+
max_jobs: int = len(self.systems)
227+
n_cores: int = multiprocessing.cpu_count() // max_jobs
228+
chunk_size: int = 10_000
229+
230+
with tempfile.TemporaryDirectory() as temp_dir:
231+
before_dir: str = str(temp_dir / Path("before_eval_results"))
232+
after_dir: str = str(temp_dir / Path("after_eval_results"))
233+
# TODO: handle `self.allow` settings
234+
eval_ci.local_eval(
235+
worktree_dir=self.builddir.worktree_dir,
236+
systems=self.systems,
237+
max_jobs=max_jobs,
238+
n_cores=n_cores,
239+
chunk_size=chunk_size,
240+
output_dir=before_dir,
241+
)
226242

227-
if reviewed_commit is None:
228-
self.apply_unstaged(staged)
229-
elif self.checkout == CheckoutOption.MERGE:
230-
self.git_checkout(reviewed_commit)
231-
else:
232-
self.git_merge(reviewed_commit)
243+
if reviewed_commit is None:
244+
self.apply_unstaged(staged)
245+
elif self.checkout == CheckoutOption.MERGE:
246+
self.git_checkout(reviewed_commit)
247+
else:
248+
self.git_merge(reviewed_commit)
249+
250+
eval_ci.local_eval(
251+
worktree_dir=self.builddir.worktree_dir,
252+
systems=self.systems,
253+
max_jobs=max_jobs,
254+
n_cores=n_cores,
255+
chunk_size=chunk_size,
256+
output_dir=after_dir,
257+
)
233258

234-
# TODO: nix-eval-jobs ?
235-
merged_packages: dict[System, list[Package]] = list_packages(
236-
self.builddir.nix_path,
237-
self.systems,
238-
self.allow,
239-
n_threads=self.num_parallel_evals,
240-
check_meta=True,
241-
)
259+
# merged_packages: dict[System, list[Package]] = list_packages(
260+
# self.builddir.nix_path,
261+
# self.systems,
262+
# self.allow,
263+
# n_threads=self.num_parallel_evals,
264+
# check_meta=True,
265+
# )
266+
267+
output_dir: Path = temp_dir / Path("comparison")
268+
eval_ci.compare(
269+
worktree_dir=self.builddir.worktree_dir,
270+
before_dir=before_dir,
271+
after_dir=after_dir,
272+
output_dir=str(output_dir),
273+
)
274+
275+
with (output_dir / Path("changed-paths.json")).open() as compare_result:
276+
outpaths_dict: dict[str, Any] = json.load(compare_result)
242277

243278
# Systems ordered correctly (x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin)
244279
sorted_systems: list[System] = sorted(
245280
self.systems,
246281
key=system_order_key,
247282
reverse=True,
248283
)
284+
249285
changed_attrs: dict[System, set[str]] = {}
250286
for system in sorted_systems:
251-
changed_pkgs, removed_pkgs = differences(
252-
base_packages[system], merged_packages[system]
287+
print(f"--------- Rebuilds on '{system}' ---------")
288+
289+
rebuilds: set[str] = set(
290+
outpaths_dict["rebuildsByPlatform"].get(system, [])
291+
)
292+
print_packages(
293+
names=list(rebuilds),
294+
msg="to rebuild",
253295
)
254-
print(f"--------- Impacted packages on '{system}' ---------")
255-
print_updates(changed_pkgs, removed_pkgs)
256296

257-
changed_attrs[system] = {p.attr_path for p in changed_pkgs}
297+
changed_attrs[system] = rebuilds
258298

259299
return self.build(changed_attrs, self.build_args)
260300

@@ -451,70 +491,6 @@ def parse_packages_xml(stdout: IO[str]) -> list[Package]:
451491
return packages
452492

453493

454-
def _list_packages_system(
455-
system: System,
456-
nix_path: str,
457-
allow: AllowedFeatures,
458-
check_meta: bool = False,
459-
) -> list[Package]:
460-
cmd = [
461-
"nix-env",
462-
"--extra-experimental-features",
463-
"" if allow.url_literals else "no-url-literals",
464-
"--option",
465-
"system",
466-
system,
467-
"-f",
468-
"<nixpkgs>",
469-
"--nix-path",
470-
nix_path,
471-
"-qaP",
472-
"--xml",
473-
"--out-path",
474-
"--show-trace",
475-
"--allow-import-from-derivation"
476-
if allow.ifd
477-
else "--no-allow-import-from-derivation",
478-
]
479-
if check_meta:
480-
cmd.append("--meta")
481-
info("$ " + " ".join(cmd))
482-
with tempfile.NamedTemporaryFile(mode="w") as tmp:
483-
res = subprocess.run(cmd, stdout=tmp, check=False)
484-
if res.returncode != 0:
485-
msg = f"Failed to list packages: nix-env failed with exit code {res.returncode}"
486-
raise NixpkgsReviewError(msg)
487-
tmp.flush()
488-
with Path(tmp.name).open() as f:
489-
return parse_packages_xml(f)
490-
491-
492-
def list_packages(
493-
nix_path: str,
494-
systems: set[System],
495-
allow: AllowedFeatures,
496-
n_threads: int,
497-
check_meta: bool = False,
498-
) -> dict[System, list[Package]]:
499-
results: dict[System, list[Package]] = {}
500-
with concurrent.futures.ThreadPoolExecutor(max_workers=n_threads) as executor:
501-
future_to_system = {
502-
executor.submit(
503-
_list_packages_system,
504-
system=system,
505-
nix_path=nix_path,
506-
allow=allow,
507-
check_meta=check_meta,
508-
): system
509-
for system in systems
510-
}
511-
for future in concurrent.futures.as_completed(future_to_system):
512-
system = future_to_system[future]
513-
results[system] = future.result()
514-
515-
return results
516-
517-
518494
def package_attrs(
519495
package_set: set[str],
520496
system: str,

nixpkgs_review/utils.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,18 @@ def wrapper(text: str) -> None:
3030

3131

3232
def sh(
33-
command: list[str], cwd: Path | str | None = None
33+
command: list[str],
34+
cwd: Path | str | None = None,
35+
capture_output: bool = False,
3436
) -> "subprocess.CompletedProcess[str]":
3537
info("$ " + shlex.join(command))
36-
return subprocess.run(command, cwd=cwd, text=True, check=False)
38+
return subprocess.run(
39+
command,
40+
cwd=cwd,
41+
text=True,
42+
check=False,
43+
capture_output=capture_output,
44+
)
3745

3846

3947
def verify_commit_hash(commit: str) -> str:

0 commit comments

Comments
 (0)