blob: d4e79d4228d6231889e12c9d4df98cffe91acd33 [file] [log] [blame]
"""Module for running CI targets."""
import argparse
import os
import platform
import uuid
import subprocess
import sys
from typing import Callable, List
from tools.base.bazel.ci import bazel
from tools.base.bazel.ci import errors
from tools.base.bazel.ci import query_checks
from tools.base.bazel.ci import studio_linux
from tools.base.bazel.ci import studio_win
class CI:
"""Continuous Integration wrapper.
This is used to run multiple functions that may fail, but
should be aggregated and displayed before exiting.
"""
exceptions: List[BaseException] = []
build_env: bazel.BuildEnv
def __init__(self, build_env: bazel.BuildEnv):
self.build_env = build_env
def run(self, func: Callable[[bazel.BuildEnv], None]):
"""Runs callables that might raise exceptions."""
try:
func(self.build_env)
except errors.CIError as e:
self.exceptions.append(e)
except subprocess.CalledProcessError as e:
msg = f'#### internal subprocess error\n{e}\n{e.stderr.decode("utf8")}'
self.exceptions.append(RuntimeError(msg))
def has_errors(self) -> bool:
"""Returns true if there were exceptions."""
return bool(self.exceptions)
def print_errors(self):
"""Write CI exceptions to stderr."""
for err in self.exceptions:
print(err, file=sys.stderr)
def exit_code(self):
"""Returns the exit code of the last known exception, otherwise 0."""
return self.exceptions[-1].exit_code if self.exceptions else 0
def find_workspace() -> str:
"""Returns the path of the WORKSPACE directory."""
bazel_workspace = os.environ.get("BUILD_WORKSPACE_DIRECTORY")
if bazel_workspace:
return bazel_workspace
raise RuntimeError("Missing environment variable: BUILD_WORKSPACE_DIRECTORY")
def studio_build_checks(ci: CI):
"""Runs checks against the build graph."""
ci.run(query_checks.no_local_genrules)
ci.run(query_checks.require_cpu_tags)
ci.run(query_checks.gradle_requires_cpu4_or_more)
def validate_coverage_graph(env: bazel.BuildEnv):
inv_id = uuid.uuid4()
result = bazel.BazelCmd(env).build(
'--config=ci', '--nobuild', f'--invocation_id={inv_id}',
'--', '@cov//:all.suite')
if result.returncode:
raise RuntimeError((
'Coverage build configuration is broken; you may need to update'
' tools/base/bazel/coverage/BUILD\n'
f'\n See https://fusion2.corp.google.com/invocations/{inv_id}'
))
ci.run(validate_coverage_graph)
def main():
"""Runs the CI target command.
If any commands raise a CIError, this exits with the exit code of the last
exception raised.
"""
parser = argparse.ArgumentParser()
parser.add_argument("target", help="The name of the CI target")
args = parser.parse_args()
bazel_name = "bazel.cmd" if platform.system() == "Windows" else "bazel"
bazel_path = os.path.join(find_workspace(), f"tools/base/bazel/{bazel_name}")
build_env = bazel.BuildEnv(bazel_path=bazel_path)
ci = CI(build_env=build_env)
match args.target:
case "studio-build-checks":
studio_build_checks(ci)
case "studio-linux":
ci.run(studio_linux.studio_linux)
case "studio-win":
ci.run(studio_win.studio_win)
case _:
raise NotImplementedError(f'target: "{args.target}" does not exist')
if ci.has_errors():
ci.print_errors()
sys.exit(ci.exit_code())
if __name__ == "__main__":
main()