170 lines
6.1 KiB
Python
170 lines
6.1 KiB
Python
# 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)
|