Skip to content

Commit f1062d7

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

File tree

3 files changed

+161
-95
lines changed

3 files changed

+161
-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: 76 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import argparse
2-
import concurrent.futures
2+
import json
33
import os
44
import subprocess
55
import sys
66
import tempfile
77
from dataclasses import dataclass, field
88
from enum import Enum
99
from pathlib import Path
10+
from pprint import pprint
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,87 @@ 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+
# TODO
225+
max_jobs: int = 2
226+
n_cores: int = 2
227+
chunk_size: int = 10_000
228+
229+
with tempfile.TemporaryDirectory() as temp_dir:
230+
before_dir: str = str(temp_dir / Path("before_eval_results"))
231+
after_dir: str = str(temp_dir / Path("after_eval_results"))
232+
# base_packages: dict[System, list[Package]] = list_packages(
233+
# self.builddir.nix_path,
234+
# self.systems,
235+
# self.allow,
236+
# n_threads=self.num_parallel_evals,
237+
# )
238+
eval_ci.local_eval(
239+
worktree_dir=self.builddir.worktree_dir,
240+
systems=self.systems,
241+
max_jobs=max_jobs,
242+
n_cores=n_cores,
243+
chunk_size=chunk_size,
244+
output_dir=before_dir,
245+
)
226246

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)
247+
if reviewed_commit is None:
248+
self.apply_unstaged(staged)
249+
elif self.checkout == CheckoutOption.MERGE:
250+
self.git_checkout(reviewed_commit)
251+
else:
252+
self.git_merge(reviewed_commit)
253+
254+
eval_ci.local_eval(
255+
worktree_dir=self.builddir.worktree_dir,
256+
systems=self.systems,
257+
max_jobs=max_jobs,
258+
n_cores=n_cores,
259+
chunk_size=chunk_size,
260+
output_dir=after_dir,
261+
)
233262

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-
)
263+
# merged_packages: dict[System, list[Package]] = list_packages(
264+
# self.builddir.nix_path,
265+
# self.systems,
266+
# self.allow,
267+
# n_threads=self.num_parallel_evals,
268+
# check_meta=True,
269+
# )
270+
271+
output_dir: Path = temp_dir / Path("comparison")
272+
eval_ci.compare(
273+
worktree_dir=self.builddir.worktree_dir,
274+
before_dir=before_dir,
275+
after_dir=after_dir,
276+
output_dir=str(output_dir),
277+
)
278+
279+
with (output_dir / Path("changed-paths.json")).open() as compare_result:
280+
outpaths_dict: dict[str, Any] = json.load(compare_result)
281+
282+
# TODO remove
283+
pprint(outpaths_dict)
242284

243285
# Systems ordered correctly (x86_64-linux, aarch64-linux, x86_64-darwin, aarch64-darwin)
244286
sorted_systems: list[System] = sorted(
245287
self.systems,
246288
key=system_order_key,
247289
reverse=True,
248290
)
291+
249292
changed_attrs: dict[System, set[str]] = {}
250293
for system in sorted_systems:
251-
changed_pkgs, removed_pkgs = differences(
252-
base_packages[system], merged_packages[system]
294+
print(f"--------- Rebuilds on '{system}' ---------")
295+
296+
rebuilds: set[str] = set(
297+
outpaths_dict["rebuildsByPlatform"].get(system, [])
298+
)
299+
print_packages(
300+
names=list(rebuilds),
301+
msg="to rebuild",
253302
)
254-
print(f"--------- Impacted packages on '{system}' ---------")
255-
print_updates(changed_pkgs, removed_pkgs)
256303

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

259306
return self.build(changed_attrs, self.build_args)
260307

@@ -451,70 +498,6 @@ def parse_packages_xml(stdout: IO[str]) -> list[Package]:
451498
return packages
452499

453500

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-
518501
def package_attrs(
519502
package_set: set[str],
520503
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)