From 65753f5d1144e3bf468a6bfe464011b12c840792 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Thalheim?= Date: Mon, 25 Dec 2023 22:45:38 +0100 Subject: [PATCH] speed up ci using nix-eval-jobs --- tests/build-profile.nix | 21 +++--- tests/run.py | 152 +++++++++++++++++++++++----------------- 2 files changed, 95 insertions(+), 78 deletions(-) diff --git a/tests/build-profile.nix b/tests/build-profile.nix index bdde5df..e4e1cdb 100644 --- a/tests/build-profile.nix +++ b/tests/build-profile.nix @@ -1,7 +1,9 @@ -{ profile }: +{ profile, pkgs }: -let - shim = { config, lib, pkgs, ... }: { +(pkgs.nixos [ + profile + ({ config, lib, ... }: { + nixpkgs.pkgs = pkgs; 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. boot.loader.grub.enable = false; @@ -14,13 +16,6 @@ let device = "/dev/disk/by-uuid/00000000-0000-0000-0000-000000000000"; fsType = "btrfs"; }; - - nixpkgs.config = { - allowBroken = true; - allowUnfree = true; - nvidia.acceptLicense = true; - }; - }; -in (import { - configuration.imports = [ profile shim ]; -}).system + system.stateVersion = lib.version; + }) +]).config.system.build.toplevel diff --git a/tests/run.py b/tests/run.py index 3971e7a..8ad6025 100755 --- a/tests/run.py +++ b/tests/run.py @@ -1,13 +1,16 @@ #!/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 json import multiprocessing import re import subprocess import sys -from functools import partial +import textwrap from pathlib import Path +from tempfile import TemporaryDirectory +from typing import IO TEST_ROOT = Path(__file__).resolve().parent ROOT = TEST_ROOT.parent @@ -16,58 +19,18 @@ GREEN = "\033[92m" RED = "\033[91m" RESET = "\033[0m" +re_nixos_hardware = re.compile(r"]+)>") + def parse_readme() -> list[str]: profiles = set() with ROOT.joinpath("README.md").open() as f: for line in f: - results = re.findall(r"]+>", line) - profiles.update(results) + if (m := re_nixos_hardware.search(line)) is not None: + profiles.add(m.group(1).strip()) 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 != "": - 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: parser = argparse.ArgumentParser(description="Run hardware tests") parser.add_argument( @@ -86,31 +49,90 @@ def parse_args() -> argparse.Namespace: 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 {{ + 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: args = parse_args() profiles = parse_readme() if len(args.profiles) == 0 else args.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: print(f"\n{RED}The following {len(failed_profiles)} test(s) failed:{RESET}") for profile in failed_profiles: