Newer
Older
#!/usr/bin/env python3
# pylint: disable=broad-except,missing-function-docstring,line-too-long
"""This mainly functions as a shell script, but python is used for its
superior control flow. An important requirement of the CI is easily
reproducible builds, therefore a wrapper is made for running shell
commands so that they are also logged.
The log is printed when build fails/succeeds and needs to perfectly
reproduce the build when pasted into a shell. Therefore all file system
modifying code not executed from shell needs a shell equivalent
explicitly appended to the shell log.
e.g. `os.chdir(x)` requires `cd x` to be appended to the shell log """
import datetime
import os
import shutil
import tarfile
from build_utils import (
cmake_options_from_dict,
die,
download_latest,
load_config,
print_shell_log,
subprocess_with_log,
upload_file,
S3CONTAINER = 'ROOT-build-artifacts' # Used for uploads
S3URL = 'https://s3.cern.ch/swift/v1/' + S3CONTAINER # Used for downloads
try:
CONNECTION = openstack.connect(cloud='envvars')
except:
CONNECTION = None
WINDOWS = (os.name == 'nt')
WORKDIR = '/tmp/workspace' if not WINDOWS else 'C:/ROOT-CI'
COMPRESSIONLEVEL = 6 if not WINDOWS else 1
def main():
# openstack.enable_logging(debug=True)
# accumulates commands executed so they can be displayed as a script on build failure
shell_log = ""
# used when uploading artifacts, calculate early since build times are inconsistent
yyyy_mm_dd = datetime.datetime.today().strftime('%Y-%m-%d')
# it is difficult to use boolean flags from github actions, use strings to convey
# true/false for boolean arguments instead.
parser.add_argument("--platform", default="centos8", help="Platform to build on")
parser.add_argument("--incremental", default="false", help="Do incremental build")
parser.add_argument("--buildtype", default="Release", help="Release|Debug|RelWithDebInfo")
parser.add_argument("--base_ref", default=None, help="Ref to target branch")
parser.add_argument("--head_ref", default=None, help="Ref to feature branch")
parser.add_argument("--architecture", default=None, help="Windows only, target arch")
parser.add_argument("--repository", default="https://github.com/root-project/root.git",
help="url to repository")
args = parser.parse_args()
args.incremental = args.incremental.lower() in ('yes', 'true', '1', 'on')
die(os.EX_USAGE, "base_ref not specified")
pull_request = args.head_ref and args.head_ref != args.base_ref
if not pull_request:
print_info("head_ref same as base_ref, assuming non-PR build")
shell_log = cleanup_previous_build(shell_log)
# Load CMake options from .github/workflows/root-ci-config/buildconfig/[platform].txt
this_script_dir = os.path.dirname(os.path.abspath(__file__))
**load_config(f'{this_script_dir}/buildconfig/global.txt'),
# file below overwrites values from above
**load_config(f'{this_script_dir}/buildconfig/{args.platform}.txt')
if args.architecture == 'x86':
options = "-AWin32 " + options
# The sha1 of the build option string is used to find existing artifacts
# with matching build options on s3 storage.
option_hash = sha1(options.encode('utf-8')).hexdigest()
obj_prefix = f'{args.platform}/{args.base_ref}/{args.buildtype}/{option_hash}'
# Make testing of CI in forks not impact artifacts
if 'root-project/root' not in args.repository:
obj_prefix = f"ci-testing/{args.repository.split('/')[-2]}/" + obj_prefix
print("Attempting to download")
shell_log += download_artifacts(obj_prefix, shell_log)
except Exception as err:
print_warning(f'Failed to download: {err}')
args.incremental = False
shell_log = git_pull(args.repository, args.base_ref, shell_log)
if pull_request:
shell_log = rebase(args.base_ref, args.head_ref, shell_log)
shell_log = build(options, args.buildtype, shell_log)
testing: bool = options_dict['testing'].lower() == "on" and options_dict['roottest'].lower() == "on"
if testing:
extra_ctest_flags = ""
if WINDOWS:
extra_ctest_flags += "--repeat until-pass:3 "
extra_ctest_flags += "--build-config " + args.buildtype
shell_log = run_ctest(shell_log, extra_ctest_flags)
if CONNECTION:
archive_and_upload(yyyy_mm_dd, obj_prefix)
@github_log_group("Clean up from previous runs")
def cleanup_previous_build(shell_log):
# runners should never have root permissions but be on the safe side
if WORKDIR == "" or WORKDIR == "/":
die(1, "WORKDIR not set", "")
if WINDOWS:
# windows
os.environ['COMSPEC'] = 'powershell.exe'
$ErrorActionPreference = 'Stop'
if (Test-Path {WORKDIR}) {{
Remove-Item -Recurse -Force -Path {WORKDIR}
}}
New-Item -Force -Type directory -Path {WORKDIR}
result, shell_log = subprocess_with_log(f"""
rm -rf {WORKDIR}
mkdir -p {WORKDIR}
die(result, "Failed to clean up previous artifacts", shell_log)
return shell_log
@github_log_group("Pull/clone branch")
def git_pull(repository:str, branch: str, shell_log: str):
returncode = 1
for attempts in range(5):
if returncode == 0:
break
if os.path.exists(f"{WORKDIR}/src/.git"):
returncode, shell_log = subprocess_with_log(f"""
cd '{WORKDIR}/src'
git checkout {branch}
git fetch
git reset --hard @{{u}}
""", shell_log)
else:
returncode, shell_log = subprocess_with_log(f"""
git clone --branch {branch} --single-branch {repository} "{WORKDIR}/src"
""", shell_log)
if returncode != 0:
die(returncode, f"Failed to pull {branch}", shell_log)
return shell_log
@github_log_group("Download previous build artifacts")
def download_artifacts(obj_prefix: str, shell_log: str):
try:
tar_path, shell_log = download_latest(S3URL, obj_prefix, WORKDIR, shell_log)
print(f"\nExtracting archive {tar_path}")
with tarfile.open(tar_path) as tar:
tar.extractall(WORKDIR)
shell_log += f'\ntar -xf {tar_path}\n'
except Exception as err:
print_warning("failed to download/extract:", err)
shutil.rmtree(f'{WORKDIR}/src', ignore_errors=True)
shutil.rmtree(f'{WORKDIR}/build', ignore_errors=True)
raise err
return shell_log
@github_log_group("Run tests")
def run_ctest(shell_log: str, extra_ctest_flags: str) -> str:
ctest --parallel {os.cpu_count()} --output-junit TestResults.xml {extra_ctest_flags}
print_warning("Some tests failed")
@github_log_group("Archive and upload")
def archive_and_upload(archive_name, prefix):
os.chdir(WORKDIR)
with tarfile.open(f"{WORKDIR}/{new_archive}", "x:gz", compresslevel=COMPRESSIONLEVEL) as targz:
targz.add("src")
targz.add("build")
upload_file(
dest_object=f"{prefix}/{new_archive}",
src_file=f"{WORKDIR}/{new_archive}"
@github_log_group("Build")
def build(options, buildtype, shell_log):
generator_flags = "-- '-verbosity:minimal'" if WINDOWS else ""
if not os.path.isdir(f'{WORKDIR}/build'):
result, shell_log = subprocess_with_log(f"mkdir {WORKDIR}/build", shell_log)
if result != 0:
die(result, "Failed to create build directory", shell_log)
if not os.path.exists(f'{WORKDIR}/build/CMakeCache.txt'):
cmake -S '{WORKDIR}/src' -B '{WORKDIR}/build' {options} -DCMAKE_BUILD_TYPE={buildtype}
""", shell_log)
if result != 0:
die(result, "Failed cmake generation step", shell_log)
result, shell_log = subprocess_with_log(f"""
cmake --build '{WORKDIR}/build' --config '{buildtype}' --parallel '{os.cpu_count()}' {generator_flags}
die(result, "Failed to build", shell_log)
@github_log_group("Rebase")
def rebase(base_ref, head_ref, shell_log) -> str:
# This mental gymnastics is neccessary because the the CMake build fetches
# roottest based on the current branch name of ROOT
#
# rebase fails unless user.email and user.name is set
git config user.email "rootci@root.cern"
git config user.name 'ROOT Continous Integration'
git fetch origin {head_ref}:__tmp
git checkout __tmp
git rebase {base_ref}
git checkout {base_ref}
git reset --hard __tmp