# Copyright 2023 Oliver Smith # SPDX-License-Identifier: GPL-3.0-or-later import collections import glob import logging import os import shlex import pmb.chroot import pmb.helpers.cli def get_ci_scripts(topdir): """ Find 'pmbootstrap ci'-compatible scripts inside a git repository, and parse their metadata (description, options). The reference is at: https://postmarketos.org/pmb-ci :param topdir: top directory of the git repository, get it with: pmb.helpers.git.get_topdir() :returns: a dict of CI scripts found in the git repository, e.g. {"ruff": {"description": "lint all python scripts", "options": []}, ...} """ ret = {} for script in glob.glob(f"{topdir}/.ci/*.sh"): is_pmb_ci_script = False description = "" options = [] with open(script) as handle: for line in handle: if line.startswith("# https://postmarketos.org/pmb-ci"): is_pmb_ci_script = True elif line.startswith("# Description: "): description = line.split(": ", 1)[1].rstrip() elif line.startswith("# Options: "): options = line.split(": ", 1)[1].rstrip().split(" ") elif not line.startswith("#"): # Stop parsing after the block of comments on top break if not is_pmb_ci_script: continue if not description: logging.error(f"ERROR: {script}: missing '# Description: …' line") exit(1) for option in options: if option not in pmb.config.ci_valid_options: raise RuntimeError(f"{script}: unsupported option '{option}'." " Typo in script or pmbootstrap too old?") short_name = os.path.basename(script).split(".", -1)[0] ret[short_name] = {"description": description, "options": options} return ret def sort_scripts_by_speed(scripts): """ Order the scripts, so fast scripts run before slow scripts. Whether a script is fast or not is determined by the '# Options: slow' comment in the file. :param scripts: return of get_ci_scripts() :returns: same format as get_ci_scripts(), but as ordered dict with fast scripts before slow scripts """ ret = collections.OrderedDict() # Fast scripts first for script_name, script in scripts.items(): if "slow" in script["options"]: continue ret[script_name] = script # Then slow scripts for script_name, script in scripts.items(): if "slow" not in script["options"]: continue ret[script_name] = script return ret def ask_which_scripts_to_run(scripts_available): """ Display an interactive prompt about which of the scripts the user wishes to run, or all of them. :param scripts_available: same format as get_ci_scripts() :returns: either full scripts_available (all selected), or a subset """ count = len(scripts_available.items()) choices = ["all"] logging.info(f"Available CI scripts ({count}):") for script_name, script in scripts_available.items(): extra = "" if "slow" in script["options"]: extra += " (slow)" logging.info(f"* {script_name}: {script['description']}{extra}") choices += [script_name] selection = pmb.helpers.cli.ask("Which script?", None, "all", complete=choices) if selection == "all": return scripts_available ret = {} ret[selection] = scripts_available[selection] return ret def copy_git_repo_to_chroot(args, topdir): """ Create a tarball of the git repo (including unstaged changes and new files) and extract it in chroot_native. :param topdir: top directory of the git repository, get it with: pmb.helpers.git.get_topdir() """ pmb.chroot.init(args) tarball_path = f"{args.work}/chroot_native/tmp/git.tar.gz" files = pmb.helpers.git.get_files(args, topdir) with open(f"{tarball_path}.files", "w") as handle: for file in files: handle.write(file) handle.write("\n") pmb.helpers.run.user(args, ["tar", "-cf", tarball_path, "-T", f"{tarball_path}.files"], topdir) ci_dir = "/home/pmos/ci" pmb.chroot.user(args, ["rm", "-rf", ci_dir]) pmb.chroot.user(args, ["mkdir", ci_dir]) pmb.chroot.user(args, ["tar", "-xf", "/tmp/git.tar.gz"], working_dir=ci_dir) def run_scripts(args, topdir, scripts): """ Run one of the given scripts after another, either natively or in a chroot. Display a progress message and stop on error (without printing a python stack trace). :param topdir: top directory of the git repository, get it with: pmb.helpers.git.get_topdir() :param scripts: return of get_ci_scripts() """ steps = len(scripts) step = 0 repo_copied = False for script_name, script in scripts.items(): step += 1 where = "pmbootstrap chroot" if "native" in script["options"]: where = "native" script_path = f".ci/{script_name}.sh" logging.info(f"*** ({step}/{steps}) RUNNING CI SCRIPT: {script_path}" f" [{where}] ***") if "native" in script["options"]: rc = pmb.helpers.run.user(args, [script_path], topdir, output="tui") continue else: # Run inside pmbootstrap chroot if not repo_copied: copy_git_repo_to_chroot(args, topdir) repo_copied = True env = {"TESTUSER": "pmos"} rc = pmb.chroot.root(args, [script_path], check=False, env=env, working_dir="/home/pmos/ci", output="tui") if rc: logging.error(f"ERROR: CI script failed: {script_name}") exit(1)