blob: 3db28688a5294da3a72678f3507d4dbe8ff07a2d [file] [log] [blame]
#!/usr/bin/env fuchsia-vendored-python
#
# Copyright 2025 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import argparse
import re
import subprocess
import sys
# (filename):(line number).(starting character)-(end character)[: ](error message)
# Warnings do not have a prefix, but Errors do. This is managed by the non-capturing
# group at the front.
TARGET_REGEX = r"(?:Error: )?(.*?):(\d+)\.(\d+)-\d+[: ]\s?(.*)$"
LINES_OF_CONTEXT = 5
LINENUM_WIDTH = 6 # 5 digits plus :
RED = "\x1b[0;31m"
YELLOW = "\x1b[0;33m"
RESET = "\x1b[0m"
def print_context(
src_lines: list[str], error_line: int, start_char: int, errmsg: str
) -> None:
"""Prints the context and error/warning raised by dtc"""
start_line = max(0, error_line - LINES_OF_CONTEXT)
end_line = error_line + LINES_OF_CONTEXT
for i, content in enumerate(
src_lines, start=1
): # enumerate starts at 0, lines start at 1
if start_line <= i <= end_line:
print(f"{i:5d}: {content.rstrip()}", file=sys.stderr)
if i == error_line:
print(
f"{' ' * (LINENUM_WIDTH + start_char)}{RED}^--- {errmsg}{RESET}",
file=sys.stderr,
)
def print_dtc_stderr(dtc_stderr: str) -> None:
"""Annotates stderr for users"""
has_warnings = False
for line in dtc_stderr.strip().split("\n"):
print(f"{YELLOW}{line}{RESET}", file=sys.stderr)
if match := re.search(TARGET_REGEX, line):
dts_file, line_num, start_char, errmsg = match.groups()
src_lines = []
try:
with open(dts_file, "r", encoding="ascii") as f:
src_lines = f.readlines()
except FileNotFoundError:
print(f"Error: '{dts_file}' not found.", file=sys.stderr)
return
print_context(
src_lines,
int(line_num),
int(start_char),
errmsg,
)
elif "Warning" in line:
has_warnings = True
# Make it clear to the user that warnings are considered errors by our build.
if has_warnings:
print(
f"{RED}Returning failure due to warnings in dts compilation{RESET}",
file=sys.stderr,
)
# A newline separator between blocks
print()
def dts_compile(args: argparse.Namespace, extra_dtc_args: list[str]) -> bool:
"""Compiles a dts to a dtbo and processes any output.
Args:
args: Necessary dtc args.
extra_dtc_args: Additional dtc args that can be optionally provided in devicetree targets.
Returns:
True if there were any errors or warnings.
False if successful.
"""
# dtc [options] <input_file>
cmd_args = [
args.dtc_path,
"--out-dependency=" + args.dep_file,
"--out=" + args.dest_file,
]
# An --include for each file listed in the includes_file
cmd_args.extend(
["--include=" + line.strip() for line in args.includes_file]
)
cmd_args.extend(extra_dtc_args)
cmd_args.append(args.src_file)
command = subprocess.run(
cmd_args, capture_output=True, text=True, check=False
)
if command.returncode or command.stderr:
print("%s" % " \\ \r\n ".join(command.args), file=sys.stderr)
print_dtc_stderr(command.stderr)
return True
return False
def dtb_decompile(args: argparse.Namespace, extra_dtc_args: list[str]) -> bool:
"""Decompiles dtb to dts.
Args:
args: Necessary dtc args.
extra_dtc_args: Additional dtc args that can be optionally provided in devicetree targets.
Returns:
True if there were any errors or warnings.
False if successful.
"""
cmd_args = [
args.dtc_path,
"--sort",
"--in-format=dtb",
"--out-format=dts",
"--out=" + args.dest_file,
]
cmd_args.extend(extra_dtc_args)
cmd_args.append(args.src_file)
command = subprocess.run(
cmd_args, capture_output=True, text=True, check=False
)
if command.returncode or command.stderr:
print("%s" % "\r\n\t".join(command.args), file=sys.stderr)
print_dtc_stderr(command.stderr)
return True
return False
def main() -> int:
"""Handles argument parsing and then either compiles dts -> dtbo or decompiles dtb -> dts"""
parser = argparse.ArgumentParser(prog="dtc.py")
subparsers = parser.add_subparsers(dest="command")
compile_parser = subparsers.add_parser(
"compile", help="Compile a dts to dtb"
)
compile_parser.add_argument("dtc_path", help="devicetree compiler path")
compile_parser.add_argument("src_file", help="dts path")
compile_parser.add_argument("dest_file", help="dtb path")
compile_parser.add_argument("dep_file", help="dependency file path")
compile_parser.add_argument(
"includes_file", type=argparse.FileType("r"), help="includes file path"
)
decompile_parser = subparsers.add_parser(
"decompile", help="Decompile a dtb to dts"
)
decompile_parser.add_argument("dtc_path", help="devicetree compiler path")
decompile_parser.add_argument("src_file", help="dtb path")
decompile_parser.add_argument("dest_file", help="dts path")
py_args, extra_dtc_args = parser.parse_known_args()
if py_args.command == "decompile":
return dtb_decompile(py_args, extra_dtc_args)
return dts_compile(py_args, extra_dtc_args)
if __name__ == "__main__":
sys.exit(main())