Files
TheRock/build_tools/teatime.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

222 lines
7.5 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""teatime.py - Combination of "tee" and "time" for managing build logs, while
indicating that if you are watching them, it might be time for tea.
Usage in a pipeline:
teatime.py args...
Usage to manage a subprocess:
teatime.py args... -- child_command child_args...
With no further arguments, teatime will just forward the child output to its
output (if launching a child process, it will combine stderr and stdout).
Like with `tee`, a log file can be passed as `teatime.py output.log -- child`,
causing output to be written to the log file in addition to the console. Some
additional information, like execution command line are also written to the
log file, so it is not a verbatim copy of the output.
Other arguments:
* --label 'some label': Prefixes console output lines with `[label] ` and writes
a summary line with total execution time.
* --no-interactive: Disables interactive console output. Output will only be
written to the console if the child returns a non zero exit code.
* --log-timestamps: Log lines will be written with a starting column of the
time in seconds since start, and a header/trailer will be added with more
timing information.
CI systems can set `TEATIME_LABEL_GH_GROUP=1` in the environment, which will
cause labeled console output to be printed using GitHub Actions group markers
instead of line prefixes. This causes the output to show in the log viewer
with interactive group handles.
"""
import argparse
import io
import os
from pathlib import Path
import shlex
import subprocess
import sys
import time
class OutputSink:
def __init__(self, args: argparse.Namespace):
self.start_time = time.time()
self.interactive: bool = args.interactive
if self.interactive:
self.out = sys.stdout.buffer
else:
self.out = io.BytesIO()
# Label management.
self.label: str | None = args.label
if self.label is not None:
self.label = self.label.encode()
self.gh_group_enable = False
try:
self.gh_group_enable = bool(int(os.getenv("TEATIME_LABEL_GH_GROUP", "0")))
except ValueError:
print(
"warning: TEATIME_LABEL_GH_GROUP env var must be an integer "
"(not emitting GH actions friendly groups)",
file=sys.stderr,
)
self.gh_group_enable = False
self.gh_group_label: bytes | None = None
self.interactive_prefix: bytes | None = None
if self.label is not None:
if self.gh_group_enable:
self.gh_group_label = self.label
else:
self.interactive_prefix = b"[" + self.label + b"] "
# Log file.
self.log_path: Path | None = args.file
self.log_file = None
if self.log_path is not None:
self.log_path.parent.mkdir(parents=True, exist_ok=True)
self.log_file = open(self.log_path, "wb")
self.log_timestamps: bool = args.log_timestamps
def start(self):
if self.gh_group_label is not None:
self.out.write(b"::group::" + self.gh_group_label + b"\n")
if self.log_file and self.log_timestamps:
self.log_file.write(f"BEGIN\t{self.start_time}\n".encode())
def finish(self, rc: int):
end_time = time.time()
if self.log_file is not None:
if self.log_timestamps:
self.log_file.write(
f"END\t{end_time}\t{end_time - self.start_time}\t{rc}\n".encode()
)
self.log_file.close()
if self.gh_group_label is not None:
self.out.write(b"::endgroup::\n")
elif self.interactive_prefix is not None and self.label is not None:
run_pretty = f"{round(end_time - self.start_time)} seconds"
if rc == 0:
status_msg = f" SUCCEEDED in "
else:
status_msg = f" FAILED WITH CODE {rc} in "
self.out.write(
b"[" + self.label + status_msg.encode() + run_pretty.encode() + b"]\n"
)
def writeline(self, line: bytes):
if self.interactive_prefix is not None:
self.out.write(self.interactive_prefix)
self.out.write(line)
if self.interactive:
self.out.flush()
if self.log_file is not None:
if self.log_timestamps:
now = time.time()
self.log_file.write(
f"{round((now - self.start_time) * 10) / 10}\t".encode()
)
self.log_file.write(line)
self.log_file.flush()
def run(args: argparse.Namespace, child_arg_list: list[str] | None, sink: OutputSink):
child: subprocess.Popen | None = None
if child_arg_list is None:
# Pipeline mode.
child_stream = sys.stdin.buffer
else:
# Subprocess mode.
if sink.log_file:
child_arg_list_pretty = shlex.join(child_arg_list)
sink.log_file.write(
f"EXEC\t{os.getcwd()}\t{child_arg_list_pretty}\n".encode()
)
child = subprocess.Popen(
child_arg_list, stderr=subprocess.STDOUT, stdout=subprocess.PIPE
)
child_stream = child.stdout
try:
for line in child_stream:
sink.writeline(line)
except KeyboardInterrupt:
if child:
child.terminate()
if child:
rc = child.wait()
if rc != 0 and not args.interactive:
# Dump all output on failure.
sys.stdout.buffer.write(sink.out.getvalue())
sys.exit(rc)
def main(cl_args: list[str]):
# If the command line contains a "--" then we are in subprocess execution
# mode: capture the child arguments explicitly before parsing ours.
child_arg_list: list[str] | None = None
try:
child_sep_pos = cl_args.index("--")
except ValueError:
pass
else:
child_arg_list = cl_args[child_sep_pos + 1 :]
cl_args = cl_args[0:child_sep_pos]
p = argparse.ArgumentParser(
"teatime.py", usage="teatime.py {command} [-- {child args...}]"
)
p.add_argument("--label", help="Apply a label prefix to interactive output")
p.add_argument(
"--interactive",
action=argparse.BooleanOptionalAction,
default=True,
help="Enable interactive output (if disabled console output will only be emitted on failure)",
)
p.add_argument(
"--log-timestamps",
action=argparse.BooleanOptionalAction,
default=False,
help="Log timestamps along with log lines to the log file",
)
p.add_argument("file", type=Path, help="Also log output to this file")
args = p.parse_args(cl_args)
# Allow some things to be overriden by env vars.
force_interactive = os.getenv("TEATIME_FORCE_INTERACTIVE")
if force_interactive:
try:
force_interactive = int(force_interactive)
except ValueError as e:
raise ValueError(
"Expected 'TEATIME_FORCE_INTERACTIVE' env var to be an int"
) from e
args.interactive = bool(force_interactive)
sink = OutputSink(args)
sink.start()
rc = 0
try:
run(args, child_arg_list, sink)
except SystemExit as e:
# See docs for interpretation of e.code
if e.code is None:
rc = 0
elif isinstance(e.code, int):
rc = e.code
else:
rc = 1
raise
finally:
sink.finish(rc)
if __name__ == "__main__":
main(sys.argv[1:])