twrpdtgen-spreadtrum/twrpdtgen/device_tree.py

304 lines
12 KiB
Python

#
# Copyright (C) 2022 The Android Open Source Project
#
# SPDX-License-Identifier: Apache-2.0
#
from datetime import datetime
from git import Repo
from os import chmod
from pathlib import Path
from sebaubuntu_libs.libaik import AIKManager
from sebaubuntu_libs.libandroid.device_info import DeviceInfo
from sebaubuntu_libs.libandroid.fstab import Fstab
from sebaubuntu_libs.libandroid.props import BuildProp
from sebaubuntu_libs.liblogging import LOGD
from shutil import copyfile, copytree, rmtree
from stat import S_IRWXU, S_IRGRP, S_IROTH
from re import sub
from twrpdtgen import __version__ as version
from twrpdtgen.sprd import SprdBuildProfile
from twrpdtgen.templates import render_template
from typing import List
from twrpdtgen.vendor_boot import VendorBootImage
BUILDPROP_LOCATIONS = [Path() / "default.prop",
Path() / "prop.default",]
BUILDPROP_LOCATIONS += [Path() / dir / "build.prop"
for dir in ["system", "vendor"]]
BUILDPROP_LOCATIONS += [Path() / dir / "etc" / "build.prop"
for dir in ["system", "vendor"]]
FSTAB_LOCATIONS = [Path() / "etc" / "recovery.fstab"]
FSTAB_LOCATIONS += [Path() / dir / "etc" / "recovery.fstab"
for dir in ["system", "vendor"]]
INIT_RC_LOCATIONS = [Path()]
INIT_RC_LOCATIONS += [Path() / dir / "etc" / "init"
for dir in ["system", "vendor"]]
class DeviceTree:
"""
A class representing a device tree
It initialize a basic device tree structure
and save the location of some important files
"""
def __init__(self, image: Path, codename: str = None,
manufacturer: str = None):
"""Initialize the device tree class."""
self.image = image
self.aik_manager = None
self.vendor_boot = None
self.sprd_profile = None
self.current_year = str(datetime.now().year)
# Check if the image exists
if not self.image.is_file():
raise FileNotFoundError("Specified file doesn't exist")
# vendor_boot must be handled separately: AIK exposes it as a generic
# ramdisk and loses the v4 fragment table and bootconfig information.
if VendorBootImage.is_vendor_boot(image):
self.vendor_boot = VendorBootImage(image)
self.image_info = self.vendor_boot.info
else:
self.aik_manager = AIKManager()
self.image_info = self.aik_manager.unpackimg(image)
assert self.image_info.ramdisk, "Ramdisk not found"
LOGD("Getting device infos...")
self.build_prop = BuildProp()
for build_prop in [self.image_info.ramdisk / location for location in BUILDPROP_LOCATIONS]:
if not build_prop.is_file():
continue
self.build_prop.import_props(build_prop)
self.device_info = DeviceInfo(self.build_prop)
if codename:
self.device_info.codename = codename
if manufacturer:
self.device_info.manufacturer = manufacturer
if self.vendor_boot is not None:
self.sprd_profile = SprdBuildProfile.from_build_prop(self.build_prop)
# Generate fstab
fstab = None
for fstab_location in [self.image_info.ramdisk / location for location in FSTAB_LOCATIONS]:
if not fstab_location.is_file():
continue
LOGD(f"Generating fstab using {fstab} as reference...")
fstab = Fstab(fstab_location)
break
if fstab is None:
raise AssertionError("fstab not found")
self.fstab = fstab
# Search for init rc files
self.init_rcs: List[Path] = []
for init_rc_path in [self.image_info.ramdisk / location for location in INIT_RC_LOCATIONS]:
if not init_rc_path.is_dir():
continue
self.init_rcs += [init_rc for init_rc in init_rc_path.iterdir()
if init_rc.name.endswith(".rc") and init_rc.name != "init.rc"]
def dump_to_folder(self, output_path: Path, git: bool = False) -> Path:
if self.vendor_boot is not None:
return self._dump_sprd_vendor_boot(output_path, git)
device_tree_folder = output_path / self.device_info.manufacturer / self.device_info.codename
prebuilt_path = device_tree_folder / "prebuilt"
recovery_root_path = device_tree_folder / "recovery" / "root"
LOGD("Creating device tree folders...")
if device_tree_folder.is_dir():
rmtree(device_tree_folder, ignore_errors=True)
device_tree_folder.mkdir(parents=True)
prebuilt_path.mkdir(parents=True)
recovery_root_path.mkdir(parents=True)
LOGD("Writing makefiles/blueprints")
self._render_template(device_tree_folder, "Android.bp", comment_prefix="//")
self._render_template(device_tree_folder, "Android.mk")
self._render_template(device_tree_folder, "AndroidProducts.mk")
self._render_template(device_tree_folder, "BoardConfig.mk")
self._render_template(device_tree_folder, "device.mk")
self._render_template(device_tree_folder, "extract-files.sh")
self._render_template(device_tree_folder, "omni_device.mk", out_file=f"omni_{self.device_info.codename}.mk")
self._render_template(device_tree_folder, "README.md")
self._render_template(device_tree_folder, "setup-makefiles.sh")
self._render_template(device_tree_folder, "vendorsetup.sh")
# Set permissions
chmod(device_tree_folder / "extract-files.sh", S_IRWXU | S_IRGRP | S_IROTH)
chmod(device_tree_folder / "setup-makefiles.sh", S_IRWXU | S_IRGRP | S_IROTH)
LOGD("Copying kernel...")
if self.image_info.kernel is not None:
copyfile(self.image_info.kernel, prebuilt_path / "kernel")
if self.image_info.dt is not None:
copyfile(self.image_info.dt, prebuilt_path / "dt.img")
if self.image_info.dtb is not None:
copyfile(self.image_info.dtb, prebuilt_path / "dtb.img")
if self.image_info.dtbo is not None:
copyfile(self.image_info.dtbo, prebuilt_path / "dtbo.img")
LOGD("Copying fstab...")
(device_tree_folder / "recovery.fstab").write_text(self.fstab.format(twrp=True))
LOGD("Copying init scripts...")
for init_rc in self.init_rcs:
copyfile(init_rc, recovery_root_path / init_rc.name, follow_symlinks=True)
if git:
self._initialize_git_repo(device_tree_folder)
return device_tree_folder
def _dump_sprd_vendor_boot(self, output_path: Path, git: bool) -> Path:
"""Generate a vendor_boot TWRP device tree for Unisoc devices."""
device_tree_folder = output_path / self.device_info.manufacturer / self.device_info.codename
prebuilt_path = device_tree_folder / "prebuilt"
recovery_root_path = device_tree_folder / "recovery" / "root"
LOGD("Creating SPRD vendor_boot device tree folders...")
if device_tree_folder.is_dir():
rmtree(device_tree_folder, ignore_errors=True)
prebuilt_path.mkdir(parents=True)
recovery_root_path.mkdir(parents=True)
LOGD("Writing SPRD vendor_boot makefiles")
self._render_template(device_tree_folder, "Android.bp", comment_prefix="//")
self._render_template(device_tree_folder, "Android.mk")
self._render_template(device_tree_folder, "sprd_AndroidProducts.mk", out_file="AndroidProducts.mk")
self._render_template(device_tree_folder, "sprd_BoardConfig.mk", out_file="BoardConfig.mk")
self._render_template(device_tree_folder, "sprd_device.mk", out_file="device.mk")
self._render_template(device_tree_folder, "sprd_product.mk",
out_file=f"twrp_{self.device_info.codename}.mk")
self._render_template(device_tree_folder, "sprd_system.prop", out_file="system.prop")
self._render_template(device_tree_folder, "sprd_README.md", out_file="README.md")
LOGD("Copying vendor_boot DTB and ramdisk payload")
copyfile(self.vendor_boot.info.dtb, prebuilt_path / "dtb.img")
self._copy_sprd_vendor_ramdisk(recovery_root_path)
self._write_sprd_twrp_fstab(recovery_root_path)
if git:
self._initialize_git_repo(device_tree_folder)
return device_tree_folder
def _copy_sprd_vendor_ramdisk(self, recovery_root_path: Path):
"""Keep only the factory payload TWRP must retain in vendor_ramdisk."""
stock_root = self.vendor_boot.info.ramdisk
first_stage = stock_root / "first_stage_ramdisk"
if first_stage.is_dir():
for source in first_stage.rglob("*"):
if not source.is_file() or not source.name.startswith("fstab."):
continue
destination = recovery_root_path / source.relative_to(stock_root)
self._copy_fstab_without_avb(source, destination)
modules = stock_root / "lib" / "modules"
if modules.is_dir():
copytree(modules, recovery_root_path / "lib" / "modules")
for relative_path in ("system/etc/recovery.fstab", "system/etc/ueventd.rc"):
source = stock_root / relative_path
if source.is_file():
destination = recovery_root_path / relative_path
if source.name.endswith("fstab"):
self._copy_fstab_without_avb(source, destination)
else:
self._copy_file(source, destination)
# Android 13-and-earlier vendor ramdisks use the TWRP 12.1 stock policy
# path. Android 14 and newer use TWRP 14.1's compatible policy instead.
if self.sprd_profile.copy_stock_selinux:
for source in stock_root.iterdir():
if source.is_file() and (
source.name == "sepolicy" or (
source.name.endswith("_contexts") and
source.name.startswith(("odm_", "plat_", "product_"))
)
):
self._copy_file(source, recovery_root_path / source.name)
init_common = stock_root / "init.recovery.common.rc"
if init_common.is_file():
self._copy_file(init_common, recovery_root_path / init_common.name)
def _write_sprd_twrp_fstab(self, recovery_root_path: Path):
"""Create the TWRP partition table and expose the vendor_boot slot."""
contents = self.fstab.format(twrp=True)
if "/vendor_boot" not in contents:
contents += (
"/vendor_boot emmc /dev/block/by-name/vendor_boot"
" flags=slotselect;backup=1;flashimg=1;display=Vendor Boot\n"
)
(recovery_root_path / "system" / "etc").mkdir(parents=True, exist_ok=True)
(recovery_root_path / "system" / "etc" / "twrp.fstab").write_text(
contents, encoding="utf-8"
)
@staticmethod
def _copy_file(source: Path, destination: Path):
destination.parent.mkdir(parents=True, exist_ok=True)
copyfile(source, destination, follow_symlinks=True)
def _copy_fstab_without_avb(self, source: Path, destination: Path):
# Both supplied trees remove the stock AVB key restrictions so TWRP can
# mount dynamic partitions after bootloader verification has completed.
contents = source.read_text(encoding="utf-8")
contents = sub(r",avb_keys=[^,\s]+", "", contents)
contents = sub(r",avb(?:=[^,\s]+)?", "", contents)
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_text(contents, encoding="utf-8")
def _initialize_git_repo(self, device_tree_folder: Path):
"""Create the optional generated repository with the historic defaults."""
LOGD("Creating git repo...")
git_repo = Repo.init(device_tree_folder)
git_config_reader = git_repo.config_reader()
git_config_writer = git_repo.config_writer()
try:
git_global_email = git_config_reader.get_value('user', 'email')
git_global_name = git_config_reader.get_value('user', 'name')
except Exception:
git_global_email, git_global_name = None, None
if git_global_email is None or git_global_name is None:
git_config_writer.set_value('user', 'email', 'barezzisebastiano@gmail.com')
git_config_writer.set_value('user', 'name', 'Sebastiano Barezzi')
git_repo.index.add(["*"])
commit_message = self._render_template(None, "commit_message", to_file=False)
git_repo.index.commit(commit_message)
def _render_template(self, *args, comment_prefix: str = "#", **kwargs):
return render_template(*args,
comment_prefix=comment_prefix,
current_year=self.current_year,
device_info=self.device_info,
fstab=self.fstab,
image_info=self.image_info,
sprd_profile=self.sprd_profile,
vendor_boot=self.vendor_boot.info if self.vendor_boot else None,
version=version,
**kwargs)
def cleanup(self):
# Cleanup
if self.vendor_boot is not None:
self.vendor_boot.cleanup()
elif self.aik_manager is not None:
self.aik_manager.cleanup()