#!/usr/bin/env python3 # Copyright 2021 Oliver Smith # SPDX-License-Identifier: GPL-3.0-or-later # Various functions used in CI scripts import configparser import os import subprocess import sys cache = {} def get_pmaports_dir(): global cache if "pmaports_dir" in cache: return cache["pmaports_dir"] ret = os.path.realpath(os.path.join(os.path.dirname(__file__) + "/../..")) cache["pmaports_dir"] = ret return ret def run_git(parameters, check=True, stderr=None): """ Run git in the pmaports dir and return the output """ cmd = ["git", "-C", get_pmaports_dir()] + parameters try: return subprocess.check_output(cmd, stderr=stderr).decode() except subprocess.CalledProcessError: if check: raise return None def add_upstream_git_remote(): """ Add a remote pointing to postmarketOS/pmaports. """ run_git(["remote", "add", "upstream", "https://gitlab.com/postmarketOS/pmaports.git"], False) run_git(["fetch", "-q", "upstream"]) def commit_message_has_string(needle): return needle in run_git(["show", "-s", "--format=full", "HEAD"]) def run_pmbootstrap(parameters, output_return=False): """ Run pmbootstrap with the pmaports dir as --aports """ cmd = ["pmbootstrap", "--aports", get_pmaports_dir()] + parameters stdout = subprocess.PIPE if output_return else None result = subprocess.run(cmd, stdout=stdout, universal_newlines=True) result.check_returncode() if output_return: return result.stdout def get_upstream_branch(): """ Use pmaports.cfg from current branch (e.g. "v20.05_fix-ci") and channels.cfg from master to retrieve the upstream branch. :returns: branch name, e.g. "v20.05" """ global cache if "upstream_branch" in cache: return cache["upstream_branch"] # Get channel (e.g. "stable") from pmaports.cfg # https://postmarketos.org/pmaports.cfg pmaports_dir = get_pmaports_dir() pmaports_cfg = configparser.ConfigParser() pmaports_cfg.read(f"{pmaports_dir}/pmaports.cfg") channel = pmaports_cfg["pmaports"]["channel"] # Get branch_pmaports (e.g. "v20.05") from channels.cfg # https://postmarketos.org/channels.cfg channels_cfg_str = run_git(["show", "upstream/master:channels.cfg"]) channels_cfg = configparser.ConfigParser() channels_cfg.read_string(channels_cfg_str) assert channel in channels_cfg, \ f"Channel '{channel}' from pmaports.cfg in your branch is unknown." \ " This appears to be an old branch, consider recreating your change" \ " on top of master." ret = channels_cfg[channel]["branch_pmaports"] cache["upstream_branch"] = ret return ret def get_changed_files(removed=True): """ Get all changed files and print them, as well as the branch and the commit that was used for the diff. :param removed: also return removed files (default: True) :returns: set of changed files """ branch_upstream = f"upstream/{get_upstream_branch()}" commit_head = run_git(["rev-parse", "HEAD"])[:-1] commit_upstream = run_git(["rev-parse", branch_upstream])[:-1] print("commit HEAD: " + commit_head) print(f"commit {branch_upstream}: f{commit_upstream}") # Check if we are HEAD on the upstream branch if commit_head == commit_upstream: # then compare with previous commit commit = "HEAD~1" else: # otherwise compare with latest common ancestor commit = run_git(["merge-base", branch_upstream, "HEAD"])[:-1] print("comparing HEAD with: " + commit) # Changed files ret = set() print("changed file(s):") for file in run_git(["diff", "--name-only", commit, "HEAD"]).splitlines(): message = " " + file if not os.path.exists(file): message += " (deleted)" if removed: ret.add(file) else: ret.add(file) print(message) return ret def get_changed_packages_sanity_check(count): for mark in ["[ci:ignore-count]", "[ci:skip-build]"]: if commit_message_has_string(mark): print("NOTE: package count sanity check skipped (" + mark + ").") return if count <= 10: return branch = get_upstream_branch() print() print("ERROR: Too many packages have changed!") print() print("This is a sanity check, so we don't end up building packages that") print("have not been modified. CI won't run for more than three hours") print("anyway.") print() print("Your options:") print("a) If you *did not* modify everything listed above, then rebase") print(f" your branch on the official postmarketOS/pmaports.git {branch}") print(" branch. Feel free to ask in the chat for help if you need any.") print("b) If you *did* modify all these packages, and you assume that") print(" they will build within one hour: skip this sanity check by") print(" adding '[ci:ignore-count]' to the commit message (then force") print(" push).") print("c) If you *did* modify all these packages, and you are sure that") print(" they won't build in time, please add '[ci:skip-build]' to the") print(" commit message (then force push). Make sure that all packages") print(" build with 'pmbootstrap build --strict'!") print() print("Thank you and sorry for the inconvenience.") sys.exit(1) def get_changed_packages(): ret = set() for file in get_changed_files(): dirname, filename = os.path.split(file) # Skip files: # * in the root dir of pmaports (e.g. README.md) # * path with a dot (e.g. .ci/, device/.shared-patches/) if not dirname or file.startswith(".") or "/." in file: continue if filename != "APKBUILD": # Walk up directories until we (eventually) find the package # the file belongs to (could be in a subdirectory of a package) while dirname and not os.path.exists(os.path.join(dirname, "APKBUILD")): dirname = os.path.dirname(dirname) # Unable to find APKBUILD the file belong to if not dirname: # ... maybe the package was deleted entirely? if not os.path.exists(file): continue # Weird, file does not belong to any package? # Here we just warn, there is an extra check # to make sure that files are organized properly. print(f"WARNING: Changed file {file} does not belong to any package") continue elif not os.path.exists(file): continue # APKBUILD was deleted ret.add(os.path.basename(dirname)) return ret def get_changed_kernels(): ret = [] for pkgname in get_changed_packages(): if pkgname.startswith("linux-"): ret += [pkgname] return ret