speed up ci using nix-eval-jobs

This commit is contained in:
Jörg Thalheim 2023-12-25 22:45:38 +01:00 committed by mergify[bot]
parent 05fc10e093
commit 65753f5d11
2 changed files with 95 additions and 78 deletions

View file

@ -1,7 +1,9 @@
{ profile }: { profile, pkgs }:
let (pkgs.nixos [
shim = { config, lib, pkgs, ... }: { profile
({ config, lib, ... }: {
nixpkgs.pkgs = pkgs;
boot.loader.systemd-boot.enable = !config.boot.loader.generic-extlinux-compatible.enable && !config.boot.loader.raspberryPi.enable; boot.loader.systemd-boot.enable = !config.boot.loader.generic-extlinux-compatible.enable && !config.boot.loader.raspberryPi.enable;
# we forcefully disable grub here just for testing purposes, even though some profiles might still use grub in the end. # we forcefully disable grub here just for testing purposes, even though some profiles might still use grub in the end.
boot.loader.grub.enable = false; boot.loader.grub.enable = false;
@ -14,13 +16,6 @@ let
device = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000"; device = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000";
fsType = "btrfs"; fsType = "btrfs";
}; };
system.stateVersion = lib.version;
nixpkgs.config = { })
allowBroken = true; ]).config.system.build.toplevel
allowUnfree = true;
nvidia.acceptLicense = true;
};
};
in (import <nixpkgs/nixos> {
configuration.imports = [ profile shim ];
}).system

View file

@ -1,13 +1,16 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#!nix-shell --quiet -p nix -p python3 -i python #!nix-shell --quiet -p nix-eval-jobs -p nix -p python3 -i python
import argparse import argparse
import json
import multiprocessing import multiprocessing
import re import re
import subprocess import subprocess
import sys import sys
from functools import partial import textwrap
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory
from typing import IO
TEST_ROOT = Path(__file__).resolve().parent TEST_ROOT = Path(__file__).resolve().parent
ROOT = TEST_ROOT.parent ROOT = TEST_ROOT.parent
@ -16,58 +19,18 @@ GREEN = "\033[92m"
RED = "\033[91m" RED = "\033[91m"
RESET = "\033[0m" RESET = "\033[0m"
re_nixos_hardware = re.compile(r"<nixos-hardware/([^>]+)>")
def parse_readme() -> list[str]: def parse_readme() -> list[str]:
profiles = set() profiles = set()
with ROOT.joinpath("README.md").open() as f: with ROOT.joinpath("README.md").open() as f:
for line in f: for line in f:
results = re.findall(r"<nixos-hardware/[^>]+>", line) if (m := re_nixos_hardware.search(line)) is not None:
profiles.update(results) profiles.add(m.group(1).strip())
return list(profiles) return list(profiles)
def build_profile(
profile: str, verbose: bool
) -> tuple[str, subprocess.CompletedProcess]:
# Hard-code this for now until we have enough other architectures to care about this.
system = "x86_64-linux"
if "raspberry-pi/2" in profile:
system = "armv7l-linux"
if "raspberry-pi/4" in profile:
system = "aarch64-linux"
cmd = [
"nix",
"build",
"--extra-experimental-features",
"nix-command",
"-f",
"build-profile.nix",
"-I",
f"nixos-hardware={ROOT}",
"--show-trace",
"--system",
system,
"--arg",
"profile",
profile,
]
# uses import from derivation
if profile != "<nixos-hardware/toshiba/swanky>":
cmd += ["--dry-run"]
if verbose:
print(f"$ {' '.join(cmd)}")
res = subprocess.run(
cmd,
cwd=TEST_ROOT,
capture_output=True,
text=True,
check=False,
)
return (profile, res)
def parse_args() -> argparse.Namespace: def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Run hardware tests") parser = argparse.ArgumentParser(description="Run hardware tests")
parser.add_argument( parser.add_argument(
@ -86,31 +49,90 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args() return parser.parse_args()
def write_eval_test(f: IO[str], profiles: list[str]) -> None:
build_profile = TEST_ROOT.joinpath("build-profile.nix")
f.write(
textwrap.dedent(
f"""
let
purePkgs = system: import <nixpkgs> {{
config = {{
allowBroken = true;
allowUnfree = true;
nvidia.acceptLicense = true;
}};
overlays = [];
inherit system;
}};
pkgs.x86_64-linux = purePkgs "x86_64-linux";
pkgs.aarch64-linux = purePkgs "aarch64-linux";
buildProfile = import {build_profile};
in
"""
)
)
f.write("{\n")
for profile in profiles:
# does import-from-derivation
if profile == "toshiba/swanky":
continue
# uses custom nixpkgs config
if profile == "raspberry-pi/2":
continue
system = "x86_64-linux"
if "raspberry-pi/4" == profile:
system = "aarch64-linux"
f.write(
f' "{profile}" = buildProfile {{ profile = import {ROOT}/{profile}; pkgs = pkgs.{system}; }};\n'
)
f.write("}\n")
def run_eval_test(eval_test: Path, gcroot_dir: Path, jobs: int) -> list[str]:
failed_profiles = []
cmd = [
"nix-eval-jobs",
"--gc-roots-dir",
gcroot_dir,
"--max-memory-size",
"2048",
"--workers",
str(jobs),
str(eval_test),
]
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
text=True,
)
with proc as p:
assert p.stdout is not None
for line in p.stdout:
data = json.loads(line)
attr = data.get("attr")
if "error" in data:
failed_profiles.append(attr)
print(f"{RED}FAIL {attr}:{RESET}", file=sys.stderr)
print(f"{RED}{data['error']}{RESET}", file=sys.stderr)
else:
print(f"{GREEN}OK {attr}{RESET}")
return failed_profiles
def main() -> None: def main() -> None:
args = parse_args() args = parse_args()
profiles = parse_readme() if len(args.profiles) == 0 else args.profiles profiles = parse_readme() if len(args.profiles) == 0 else args.profiles
failed_profiles = [] failed_profiles = []
with TemporaryDirectory() as tmpdir:
eval_test = Path(tmpdir) / "eval-test.nix"
gcroot_dir = Path(tmpdir) / "gcroot"
with eval_test.open("w") as f:
write_eval_test(f, profiles)
failed_profiles = run_eval_test(eval_test, gcroot_dir, args.jobs)
def eval_finished(args: tuple[str, subprocess.CompletedProcess]) -> None:
profile, res = args
if res.returncode == 0:
print(f"{GREEN}OK {profile}{RESET}")
else:
print(f"{RED}FAIL {profile}:{RESET}", file=sys.stderr)
if res.stdout != "":
print(f"{RED}{res.stdout.rstrip()}{RESET}", file=sys.stderr)
print(f"{RED}{res.stderr.rstrip()}{RESET}", file=sys.stderr)
failed_profiles.append(profile)
build = partial(build_profile, verbose=args.verbose)
if len(profiles) == 0 or args.jobs == 1:
for profile in profiles:
eval_finished(build(profile))
else:
pool = multiprocessing.Pool(processes=args.jobs)
for r in pool.imap(build, profiles):
eval_finished(r)
if len(failed_profiles) > 0: if len(failed_profiles) > 0:
print(f"\n{RED}The following {len(failed_profiles)} test(s) failed:{RESET}") print(f"\n{RED}The following {len(failed_profiles)} test(s) failed:{RESET}")
for profile in failed_profiles: for profile in failed_profiles: