Files
build/lib/tools/common/dt_makefile_patcher.py

262 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#
# SPDX-License-Identifier: GPL-2.0
# Copyright (c) 2023 Ricardo Pardini <ricardo@pardini.net>
# This file is a part of the Armbian Build Framework https://github.com/armbian/build/
#
import logging
import os
import re
import shutil
import git
from git import Actor
from common.patching_config import PatchingConfig
from common.patching_utils import PatchRootDir
log: logging.Logger = logging.getLogger("dt_makefile_patcher")
class AutoPatcherParams:
def __init__(
self,
pconfig: PatchingConfig,
git_work_dir: str,
root_types_order: list[str],
root_dirs_by_root_type: dict[str, list[PatchRootDir]],
apply_patches_to_git: bool,
git_repo: git.Repo):
self.pconfig = pconfig
self.git_work_dir = git_work_dir
self.root_types_order = root_types_order
self.root_dirs_by_root_type = root_dirs_by_root_type
self.apply_patches_to_git = apply_patches_to_git
self.git_repo = git_repo
class AutomaticPatchDescription:
def __init__(self):
self.name = "Not initted name"
self.description = "Not initted desc"
self.files = []
def rich_name_status(self):
return f"[bold][blue]{self.name}"
def rich_diffstats(self):
files_bare = []
max_files_to_show = 15 # show max 15
for one_file in self.files[:max_files_to_show]:
files_bare.append(os.path.basename(one_file))
if len(self.files) > max_files_to_show:
files_bare.append(f"and {len(self.files) - max_files_to_show} more")
return ", ".join(files_bare)
def rich_subject(self):
return f"Armbian Autopatcher: {self.description}"
def auto_patch_dt_makefile(git_work_dir: str, dt_rel_dir: str, config_var: str) -> dict[str, str]:
ret: dict[str, str] = {}
dt_path = os.path.join(git_work_dir, dt_rel_dir)
# Bomb if it does not exist or is not a directory
if not os.path.isdir(dt_path):
raise ValueError(f"DT_PATH={dt_path} is not a directory")
makefile_path = os.path.join(dt_path, "Makefile")
# Bomb if it does not exist or is not a file
if not os.path.isfile(makefile_path):
raise ValueError(f"MAKEFILE_PATH={makefile_path} is not a file")
ret["MAKEFILE_PATH"] = makefile_path
# Grab the contents of the Makefile
with open(makefile_path, "r") as f:
makefile_contents = f.read()
log.info(f"Read {len(makefile_contents)} bytes from {makefile_path}")
log.debug(f"Contents:\n{makefile_contents}")
# Parse it into a list of lines
makefile_lines = makefile_contents.splitlines()
log.info(f"Read {len(makefile_lines)} lines from {makefile_path}")
regex_dtb = r"(.*)\s(([a-zA-Z0-9-_]+)\.dtb)(.*)"
regex_configopt = r"^dtb-\$\(([a-zA-Z_]+)\)\s+"
# For each line, check if it matches the regex_dtb, extract the groups
line_counter = 0
line_first_match = 0
line_last_match = 0
list_dts_basenames: list[str] = []
list_configvars: list[str] = []
for line in makefile_lines:
line_counter += 1
match_dtb = re.match(regex_dtb, line)
match_configopt = re.match(regex_configopt, line)
if match_dtb or match_configopt:
line_first_match = line_counter if line_first_match == 0 else line_first_match
line_last_match = line_counter
if match_dtb:
list_dts_basenames.append(match_dtb.group(3))
if match_configopt:
list_configvars.append(match_configopt.group(1))
dict_dts_basenames = set(list_dts_basenames) # reduce list to set
# Sanity checks
# SC: make sure dict_dts_basenames has at least one element
if len(dict_dts_basenames) < 1:
raise ValueError(
f"dict_dts_basenames={dict_dts_basenames} -- found {len(dict_dts_basenames)} dtbs, expected more than zero in {makefile_path}")
dict_configvars = set(list_configvars) # reduce list to set
# SC: make sure dict_configvars has exactly one element
if len(dict_configvars) != 1:
raise ValueError(f"dict_configvars={dict_configvars} -- found {len(dict_configvars)} configvars, expected exactly one in {makefile_path}")
# Now compute the preambles and the postamble
preamble_lines = makefile_lines[:line_first_match - 1]
postamble_lines = makefile_lines[line_last_match:]
# Find all .dts files in DT_PATH (not subdirectories), but add to the list without .dts suffix
dts_files = []
listdir: list[str] = os.listdir(dt_path)
for file in listdir:
if file.endswith(".dts"):
dts_files.append(file[:-4])
# sort the list. alpha-sort: `meson-sm1-a95xf3-air-gbit` should come sooner than `meson-sm1-a95xf3-air`? why?
dts_files.sort()
log.info(f"Found {len(dts_files)} .dts files in {dt_path}")
# Show them all
for dts_file in dts_files:
log.debug(f"Found {dts_file}")
# Create the mid-amble, which is the list of .dtb files to be built
midamble_lines = []
# If we've found an equal number of dtbs and configvars, means one-rule-per-dtb (arm64) style
if len(list_dts_basenames) == len(list_configvars):
ret["extra_desc"] = "one-rule-per-dtb (arm64) style"
for dts_file in dts_files:
midamble_lines.append(f"dtb-$({config_var}) += {dts_file}.dtb")
# Otherwise one-rule-for-all-dtbs (arm 32-bit) style, where the last one hasn't a trailing backslash
# Important, this requires 6.5-rc1's move to subdir-per-vendor and can't handle the all-in-one Makefile before it
else:
ret["extra_desc"] = "one-rule-for-all-dtbs (arm 32-bit) style"
midamble_lines.append(f"dtb-$({config_var}) += \\")
dtb_single_rule_list = []
for dts_file in dts_files:
dtb_single_rule_list.append(f"\t{dts_file}.dtb")
midamble_lines.append(" \\\n".join(dtb_single_rule_list))
# Late to the game: if DT_DIR/overlay/Makefile exists, add it.
overlay_lines = []
DT_OVERLAY_PATH = os.path.join(dt_path, "overlay")
DT_OVERLAY_MAKEFILE_PATH = os.path.join(DT_OVERLAY_PATH, "Makefile")
if os.path.isfile(DT_OVERLAY_MAKEFILE_PATH):
ret["DT_OVERLAY_MAKEFILE_PATH"] = DT_OVERLAY_MAKEFILE_PATH
ret["DT_OVERLAY_PATH"] = DT_OVERLAY_PATH
overlay_lines.append("")
overlay_lines.append("subdir-y := $(dts-dirs) overlay")
# Now join the preambles, midamble, postamble and overlay stuff into a single list
new_makefile_lines = preamble_lines + midamble_lines + postamble_lines + overlay_lines
# Rewrite the Makefile with the new contents
with open(makefile_path, "w") as f:
f.write("\n".join(new_makefile_lines))
log.info(f"Wrote {len(new_makefile_lines)} lines to {makefile_path}")
return ret
def copy_bare_files(autopatcher_params: AutoPatcherParams, type: str) -> list[AutomaticPatchDescription]:
ret_desc_list: list[AutomaticPatchDescription] = []
# group the pconfig.dts_directories by target dir
dts_dirs_by_target = {}
if type == "dt":
config_dirs = autopatcher_params.pconfig.dts_directories
elif type == "overlay":
config_dirs = autopatcher_params.pconfig.overlay_directories
else:
raise ValueError(f"Unknown copy_bare_files::type {type}")
for one_dts_dir in config_dirs:
if one_dts_dir.target not in dts_dirs_by_target:
dts_dirs_by_target[one_dts_dir.target] = []
dts_dirs_by_target[one_dts_dir.target].append(one_dts_dir.source)
# for each target....
for target_dir in dts_dirs_by_target:
all_files_to_copy = []
dts_source_dirs = dts_dirs_by_target[target_dir]
full_path_target_dir = os.path.join(autopatcher_params.git_work_dir, target_dir)
if not os.path.exists(full_path_target_dir):
os.makedirs(full_path_target_dir)
for one_dts_dir in dts_source_dirs:
for type_in_order in autopatcher_params.root_types_order:
root_dirs = autopatcher_params.root_dirs_by_root_type[type_in_order]
for root_dir in root_dirs:
full_path_source = os.path.join(root_dir.abs_dir, one_dts_dir)
log.debug(f"Will copy {full_path_source} to {full_path_target_dir}...")
if not os.path.isdir(full_path_source):
continue
# get a list of regular files in the source directory
files_to_copy = [
os.path.join(full_path_source, f) for f in os.listdir(full_path_source)
if os.path.isfile(os.path.join(full_path_source, f))
]
all_files_to_copy.extend(files_to_copy)
# Create a dict of base filename -> list of full paths; this way userpatches with same name take precedence
all_files_to_copy_dict = {}
for one_file in all_files_to_copy:
base_filename = os.path.basename(one_file)
all_files_to_copy_dict[base_filename] = one_file
# do the actual copy
all_copied_files = []
for one_file in all_files_to_copy_dict:
log.debug(f"Copy '{one_file}' (from {all_files_to_copy_dict[one_file]}) to '{full_path_target_dir}'...")
full_path_target_file = os.path.join(full_path_target_dir, one_file)
shutil.copyfile(all_files_to_copy_dict[one_file], full_path_target_file)
all_copied_files.append(full_path_target_file)
# If more than 0 files were copied, commit them if we're doing commits
desc = AutomaticPatchDescription()
desc.name = f"Armbian Bare {type.upper()} auto-patch"
desc.description = f"Armbian Bare {type.upper()} files for {target_dir}"
desc.files = all_copied_files
ret_desc_list.append(desc)
if autopatcher_params.apply_patches_to_git and len(all_copied_files) > 0:
autopatcher_params.git_repo.git.add(all_copied_files)
maintainer_actor: Actor = Actor(f"Armbian Bare {type.upper()} AutoPatcher", "patching@armbian.com")
commit = autopatcher_params.git_repo.index.commit(
message=f"Armbian Bare Device Tree files for {target_dir}"
, author=maintainer_actor, committer=maintainer_actor, skip_hooks=True
)
log.info(f"Committed Bare {type.upper()} changes to git: {commit.hexsha} for {target_dir}")
log.info(f"Done with Bare {type.upper()} autopatch commit for {target_dir}.")
return ret_desc_list
def auto_patch_all_dt_makefiles(autopatcher_params: AutoPatcherParams) -> list[AutomaticPatchDescription]:
ret_desc_list: list[AutomaticPatchDescription] = []
for one_autopatch_config in autopatcher_params.pconfig.autopatch_makefile_dt_configs:
log.warning(f"Autopatching DT Makefile in {one_autopatch_config.directory} with config '{one_autopatch_config.config_var}'...")
autopatch_makefile_info = auto_patch_dt_makefile(
autopatcher_params.git_work_dir, one_autopatch_config.directory, one_autopatch_config.config_var)
desc = AutomaticPatchDescription()
desc.name = "Armbian DT Makefile auto-patch"
desc.description = f"Armbian DT Makefile AutoPatch for {one_autopatch_config.directory}; {autopatch_makefile_info['extra_desc']}"
desc.files = [autopatch_makefile_info["MAKEFILE_PATH"]]
ret_desc_list.append(desc)
if autopatcher_params.apply_patches_to_git:
autopatcher_params.git_repo.git.add(autopatch_makefile_info["MAKEFILE_PATH"])
maintainer_actor: Actor = Actor("Armbian DT Makefile AutoPatcher", "patching@armbian.com")
commit = autopatcher_params.git_repo.index.commit(
message=f"Armbian automatic DT Makefile patch for {one_autopatch_config.directory}",
author=maintainer_actor, committer=maintainer_actor, skip_hooks=True
)
log.info(f"Committed changes to git: {commit.hexsha} for {one_autopatch_config.directory}")
log.info(f"Done with Makefile autopatch commit for {one_autopatch_config.directory}.")
return ret_desc_list