yocto-autobuilder-helper/scripts/yocto-supported-distros
Richard Purdie 6d1ea392e5 scripts/yocto-supported-distros: Better integrate for autobuilder use
For autobuilder use, it will be more helpful to print warnings for the issues found
which will show up in the autobuilder UI in this format. We can then save the error
exit code for actual script failures which should improve usability from the autobuilder
perspective.

Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
2025-03-04 15:17:41 +00:00

305 lines
8.6 KiB
Python
Executable File

#!/usr/bin/env python3
#
# SPDX-License-Identifier: GPL-2.0-only
#
# Read config.py from yocto-autobuilder2 and print the list of supported releases
# for each release.
#
# Usage:
#
# ./tools/supported-distros --config /path/to/config.py --releases release1 [release2 ...]
#
# Example:
#
# ./tools/supported-distros --config yocto-autobuilder2/config.py --releases master styhead scarthgap kirkstone
#
# If run with --compare the script with try to run `bitbake-getvar` to obtain the
# value of SANITY_TESTED_DISTROS, and compare that (with some mangling) to the
# configured workers and return 1 in case of difference. Only one release must be
# passed in this mode.
#
# Usage:
#
# ./tools/supported-distros --config /path/to/config.py --releases master --compare
#
# The opts --release-from-env and --config-from-web can also be used to get
# these info using respectively the METADATA_BRANCH variable and the config.py
# from the git web interface.
#
# Example:
#
# ./tools/supported-distros --config-from-web --release-from-env --compare
#
# Will get the current branch from METADATA_BRANCH, fetch config.py from
# git.yoctoproject.org/yocto-autobuilder2, compare and output the differences.
#
# The script will return 1 in case of difference, 0 if the distros match.
# With one exception: if the branch returned by --release-from-env is not
# present in the autobuilder config, just return 0, because this might by run
# from a custom branch.
import argparse
import os
import re
import requests
import sys
import subprocess
import tempfile
from pathlib import Path
from typing import List, Dict, Set
CONFIG_REMOTE_URL = "https://git.yoctoproject.org/yocto-autobuilder2/plain/config.py"
def parse_arguments() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Print supported distributions")
parser.add_argument("--releases",
type=str,
nargs='+',
default=[],
help="Yocto releases")
parser.add_argument("--config",
type=Path,
default=None,
help="Autobuilder config.py input file")
parser.add_argument("--release-from-env",
action="store_true",
help="Get release from METADATA_BRANCH bitbake var")
parser.add_argument("--compare",
action="store_true",
help="Compare to poky.conf releases")
parser.add_argument("--config-from-web",
action="store_true",
help="Get config.py from yoctoproject's git web interface")
return parser.parse_args()
def _possible_workers(all_workers: List[str],
match_workers: List[str]) -> List[str]:
"""
Return workers in match_workers that match the workers in all_workers.
A match is a worker in all_workers that starts with a worker in
match_workers.
This is because workers_prev_releases is defined like so in config.py.
"""
possible_workers = []
for distro_name in all_workers:
for worker in match_workers:
if worker.startswith(distro_name):
possible_workers.append(worker)
return possible_workers
def _print_worker_list(worker_list: List, indent=2):
"""
Helper to print a set nicely.
"""
for w in worker_list:
print(" " * indent + w)
def _print_worker_list_warning(worker_list: List, warning):
"""
Helper to print a set nicely.
"""
for w in worker_list:
print("WARNING: " + warning + ": " + w)
def _print_workers(possible_workers: Dict[str, List]):
"""
Helper to print the workers nicely.
"""
for release in possible_workers:
print(f"{release}:\n")
_print_worker_list(sorted(possible_workers[release]))
print("")
def _get_poky_distros() -> Set[str]:
poky_distros = set()
tested_distros = subprocess.check_output(
["bitbake-getvar", "--value", "SANITY_TESTED_DISTROS"],
encoding="utf-8")
tested_distros = tested_distros.replace("\\n", "")
for distro in tested_distros.split():
if "poky" in distro:
continue
if "almalinux" in distro:
# remove the minor version string
r = re.compile(r"^(almalinux-\d+)\.\d+")
m = re.match(r, distro)
if m:
distro = m.group(1)
poky_distros.add(distro.strip())
return poky_distros
def _get_metadata_branch() -> str:
branch = subprocess.check_output(
["bitbake-getvar", "--value", "METADATA_BRANCH"],
encoding="utf-8")
return branch.strip()
def _mangle_worker(worker: str) -> str:
"""
Mangle the worker name to convert it to an lsb_release type of string.
"""
r = re.compile(r"^alma(\d+)")
m = re.match(r, worker)
if m:
return f"almalinux-{m.group(1)}"
r = re.compile(r"^debian(\d+)")
m = re.match(r, worker)
if m:
return f"debian-{m.group(1)}"
r = re.compile(r"^fedora(\d+)")
m = re.match(r, worker)
if m:
return f"fedora-{m.group(1)}"
r = re.compile(r"^opensuse(\d{2})(\d{1})")
m = re.match(r, worker)
if m:
return f"opensuseleap-{m.group(1)}.{m.group(2)}"
r = re.compile(r"^rocky(\d+)")
m = re.match(r, worker)
if m:
return f"rocky-{m.group(1)}"
r = re.compile(r"^stream(\d+)")
m = re.match(r, worker)
if m:
return f"centosstream-{m.group(1)}"
r = re.compile(r"^ubuntu(\d{2})(\d{2})")
m = re.match(r, worker)
if m:
return f"ubuntu-{m.group(1)}.{m.group(2)}"
return ""
def _compare(ab_workers: set, poky_workers: set):
ok = True
print("Configured on the autobuilder:")
_print_worker_list(sorted(list(ab_workers)))
print()
print("Listed in poky.conf:")
_print_worker_list(sorted(list(poky_workers)))
print()
poky_missing = ab_workers.difference(sorted(list(poky_workers)))
if poky_missing:
_print_worker_list_warning(poky_missing, "Missing in poky.conf but configured on the autobuilder")
print()
ok = False
ab_missing = poky_workers.difference(sorted(list(ab_workers)))
if ab_missing:
_print_worker_list_warning(sorted(list(ab_missing)), "Missing on the autobuilder but listed in poky.conf")
print()
ok = False
return ok
def main():
args = parse_arguments()
if not args.config and not args.config_from_web:
print("Must provide path to config or --config-from-web")
exit(1)
if args.config_from_web:
r = requests.get(CONFIG_REMOTE_URL)
with tempfile.TemporaryDirectory() as tempdir:
with open(Path(tempdir) / "config.py", "wb") as conf:
conf.write(r.content)
sys.path.append(tempdir)
import config
else:
sys.path.append(os.path.dirname(args.config))
import config
releases = None
if args.release_from_env:
releases = [_get_metadata_branch()]
else:
releases = args.releases
if not releases:
print("Must provide one or more release, or --release-from-env")
exit(1)
possible_workers = {}
for release in releases:
if release != "master" and release not in config.workers_prev_releases:
print(f"Release {release} does not exist")
if args.release_from_env:
# Might be a custom branch or something else... safely exiting
exit(0)
else:
exit(1)
if release == "master":
possible_workers.update({release: config.all_workers})
continue
if release not in config.workers_prev_releases:
print(f"Release {release} does not exist, available releases: "
f"{config.workers_prev_releases.keys()}")
exit(1)
possible_workers.update(
{release: _possible_workers(config.workers_prev_releases[release],
config.all_workers)})
if args.compare:
assert len(releases) == 1, "Only one release should be passed for this mode"
release = releases[0]
print(f"Comparing for release {release}...\n")
poky_workers = _get_poky_distros()
ab_workers = set()
for w in possible_workers[release]:
mangled_w = _mangle_worker(w)
if mangled_w:
ab_workers.add(mangled_w)
if not _compare(ab_workers, poky_workers):
print("Mismatches were found")
else:
print("All good!")
else:
_print_workers(possible_workers)
if __name__ == "__main__":
main()