#!/usr/bin/python """ SPDX-License-Identifier: Apache-2.0 Copyright (c) 2019 STMicroelectronics. This script define Stm32SerieUpdate class to be used by update_stm32_package.py """ import os import shutil import subprocess import re from pathlib import Path import logging from jinja2 import Environment, FileSystemLoader import ble_library from common_utils import common_utils STM32_CUBE_REPO_BASE = "https://github.com/STMicroelectronics/STM32Cube" """GitHub URL to get STM32Cube""" SCRIPT_DIR = Path(__file__).absolute().parent """Script directory.""" REPO_ROOT = SCRIPT_DIR / ".." """Repository root (used for input/output default folders).""" # list of created files. It is necessary to remove all of them # as they are fully created when applying zephyr patch zephyr_file_created = [ "CMakeLists.txt", "README", "drivers/include/stm32_assert.h", ] def version_tuple(version): """Remove 'v' in front of version and convert it to tuple, so that versions can be compared """ v = re.sub("v", r"", version) return tuple(map(int, (v.split(".")))) class Stm32SerieUpdate: """class Stm32SerieUpdate""" def __init__( self, stm32_serie, stm32cube_repo_path, noclean, version_update, debug, ): """Class Stm32SerieUpdate constructor Args: stm32_serie: stm32 serie ex:stm32f3xx stm32cube_repo_path: directory path where to fetch github repo noclean: boolean to clean or not github repo after update done version_update: string to force a specified version to be updated debug: boolean to set log debug level Returns: return previous zephyr cube version. Raises: ValueError: If stm32 serie is not recognised. FileNotFoundError: If Zphyr STM32 cube path is not found """ if not stm32_serie.startswith("stm32"): raise ValueError( f"Error: Unknown stm32 serie: {stm32_serie}. Must start with 'stm32'" ) # Set serie variables self.stm32_serie = stm32_serie self.stm32_seriexx = stm32_serie + "xx" # ex:stm32f3xx self.stm32_serie_upper = stm32_serie.upper() # ex:STM32F3 self.stm32_seriexx_upper = self.stm32_serie_upper + "xx" # ex:STM32F3xx self.serie = self.stm32_serie_upper[5:] self.noclean = noclean self.version_update = version_update self.debug = debug self.module_patch = f"module_{self.stm32_serie}.patch" # ##### 3 root directories to work with ######## # 1: STM32Cube repo Default $HOME/STM32Cube_repo # 2 : zephyr stm32 path : ex: .../zephyr_project/module/hal/stm32 # 3: Temporary directory to construct the update # (within STM32Cube repo dir) self.stm32cube_repo_path = stm32cube_repo_path if not self.stm32cube_repo_path.exists(): self.stm32cube_repo_path.mkdir() self.zephyr_hal_stm32_path = REPO_ROOT if not self.zephyr_hal_stm32_path.exists(): raise FileNotFoundError("Error: cannot find zephyr project") self.stm32cube_temp = self.stm32cube_repo_path / "temp_stm32xx_update" if self.stm32cube_temp.exists(): shutil.rmtree( str(self.stm32cube_temp), onerror=common_utils.remove_readonly ) self.stm32cube_temp.mkdir() # subdir specific to a stm32 serie self.stm32cube_serie_path = self.stm32cube_repo_path / Path( "STM32Cube" + self.serie ) self.zephyr_module_serie_path = ( self.zephyr_hal_stm32_path / "stm32cube" / self.stm32_seriexx ) self.stm32cube_temp_serie = ( self.stm32cube_temp / "stm32cube" / self.stm32_seriexx ) shutil.rmtree(str(self.stm32cube_temp), onerror=common_utils.remove_readonly) self.stm32cube_temp_serie.mkdir(parents=True) self.readme_file_path = self.zephyr_module_serie_path / "README" self.version_tag = [] self.current_version = "" self.update_commit = "" if self.debug: logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.DEBUG) self.std_dest = None else: logging.basicConfig(format="%(levelname)s:%(message)s", level=logging.INFO) self.std_dest = subprocess.DEVNULL def os_cmd(self, cmd, cwd=None, shell=False): """Execute a command with subprocess.check_call() Args: cmd: string command to execute. cwd: directory where to run command shell: boolean to enable command interpretation by the shell Returns: return the returncode of the command after execution. """ logging.debug("%s", f"{str(cmd)} cwd:{str(cwd)}") return subprocess.check_call( cmd, shell=shell, stdout=self.std_dest, stderr=self.std_dest, cwd=cwd, ) def rename_conf_template(self, path): """renames hal_conf_template.h to hal_conf.h ... Args: path: path where to apply the files processing """ # except for _hal_conf_template.h which is renamed hal_conf_template_fullpath = Path( path / (self.stm32_seriexx + "_hal_conf_template.h") ) if hal_conf_template_fullpath.is_file(): hal_conf_fullpath = Path( re.sub("_template", r"", str(hal_conf_template_fullpath)) ) if hal_conf_fullpath.exists(): hal_conf_fullpath.unlink() hal_conf_template_fullpath.rename(hal_conf_fullpath) def major_branch(self): # check whether master branch exist, otherwise use main branch master_branch_exist = subprocess.check_output( ("git", "ls-remote", "--heads", "origin", "master"), cwd=self.stm32cube_serie_path, ).decode("utf-8") if master_branch_exist: return "master" else: return "main" def clone_cube_repo(self): """Clone or fetch a stm32 serie repo""" repo_name = STM32_CUBE_REPO_BASE + self.serie + ".git" logging.info( "%s", "Cloning/fetching repo " + repo_name + " in " + str(self.stm32cube_serie_path), ) if self.stm32cube_serie_path.exists(): # if already exists, then just clean and fetch self.os_cmd(("git", "clean", "-fdx"), cwd=self.stm32cube_serie_path) self.os_cmd(("git", "fetch"), cwd=self.stm32cube_serie_path) branch = self.major_branch() self.os_cmd( ("git", "reset", "--hard", branch), cwd=self.stm32cube_serie_path, ) else: self.os_cmd( ("git", "clone", repo_name), cwd=self.stm32cube_repo_path, ) branch = self.major_branch() logging.info("%s", f"Branch used: {branch}") # get the latest version of cube, # with the most recent one created being the last entry. self.os_cmd(("git", "checkout", branch), cwd=self.stm32cube_serie_path) self.version_tag = subprocess.check_output( ("git", "tag", "-l"), cwd=self.stm32cube_serie_path ).splitlines() self.version_tag = [x.decode("utf-8") for x in self.version_tag] # Search latest version if self.version_update == "": self.version_update = self.version_tag[0] for tag in self.version_tag: if version_tuple(tag) > version_tuple(self.version_update): self.version_update = tag def get_zephyr_current_version(self): """Look for current zephyr hal version Returns: return previous zephyr cube version. Raises: ValueError: If version is not found. """ with open(str(self.readme_file_path), "r") as f: for line in f: # pattern : "version " follow by optional "v", # followed by x.y or x.y.z x,y,z may represent several digits # ex: 'version v1.8.9', 'version 10.20.25' pattern = r".*version v?(\d+\.\d+\.?\d*).*$" if re.match(pattern, line): previous_version = re.sub(pattern, r"\1", line).rstrip("\n") break # Match previous version and list of existing tags # which could be vx.y or x.y pos_version = [ i for i, a in enumerate(self.version_tag) if previous_version in a ] if pos_version: # return previous zephyr version return self.version_tag[pos_version[0]] else: self.clean_files() raise ValueError( f"Error: cannot find version {previous_version} in STM32Cube_repo" ) def extract_source(self): """Extract sources and includes files from STM32Cube repo and copy them in temporary directory """ # for CMSIS files temp_cmsis_soc_path = self.stm32cube_temp_serie / "soc" Path.mkdir(temp_cmsis_soc_path, parents=True) stm32cube_cmsis_include_path = ( self.stm32cube_serie_path / "Drivers" / "CMSIS" / "Device" / "ST" / self.stm32_seriexx_upper / "Include" ) shutil.rmtree(temp_cmsis_soc_path, onerror=common_utils.remove_readonly) shutil.copytree(stm32cube_cmsis_include_path, temp_cmsis_soc_path) stm32cube_cmsis_templates_path = ( self.stm32cube_serie_path / "Drivers" / "CMSIS" / "Device" / "ST" / self.stm32_seriexx_upper / "Source" / "Templates" ) for repo_file in stm32cube_cmsis_templates_path.iterdir(): repo_src = stm32cube_cmsis_templates_path / repo_file if repo_src.is_file(): shutil.copy(str(repo_src), str(temp_cmsis_soc_path)) # for hal and ll drivers temp_drivers_include_path = self.stm32cube_temp_serie / "drivers" / "include" temp_drivers_include_path.parent.mkdir(parents=True) stm32cube_driver_inc = ( self.stm32cube_serie_path / "Drivers" / Path(self.stm32_seriexx_upper + "_HAL_Driver") / "Inc" ) if temp_drivers_include_path.exists(): shutil.rmtree( temp_drivers_include_path, onerror=common_utils.remove_readonly ) shutil.copytree(stm32cube_driver_inc, temp_drivers_include_path) # except for _hal_conf_template.h which is renamed self.rename_conf_template(temp_drivers_include_path) temp_drivers_src_path = self.stm32cube_temp_serie / "drivers" / "src" temp_drivers_src_path.mkdir() stm32cube_drivers_src_path = ( self.stm32cube_serie_path / "Drivers" / Path(self.stm32_seriexx_upper + "_HAL_Driver") / "Src" ) shutil.rmtree(temp_drivers_src_path, onerror=common_utils.remove_readonly) shutil.copytree(stm32cube_drivers_src_path, temp_drivers_src_path) def build_from_current_cube_version(self): """Build a commit in temporary dir with STM32Cube version corresponding to zephyr current hal version """ # reset the STM32Cube repo to this current version self.os_cmd( ("git", "reset", "--hard", self.current_version), cwd=self.stm32cube_serie_path, ) # build the zephyr module from the stm32cube self.extract_source() logging.info( "%s", "Building module from STM32Cube_repo " + self.current_version ) if not self.stm32cube_temp_serie.parent.exists(): self.stm32cube_temp_serie.parent.mkdir(parents=True) self.os_cmd( ("git", "add", "-A", "stm32cube/" + self.stm32_seriexx + "/*"), cwd=self.stm32cube_temp, ) self.os_cmd( ("git", "commit", "-am", '"module' + self.current_version + '"'), cwd=self.stm32cube_temp, ) # Remove trailing whitespaces self.os_cmd( ("git", "rebase", "--whitespace=fix", "HEAD~1"), cwd=self.stm32cube_temp, ) def build_patch_from_current_zephyr_version(self): """Build patch between zephyr current hal version and corresponding official STM32Cube version """ # clean-up the module shutil.rmtree( str(self.stm32cube_temp_serie), onerror=common_utils.remove_readonly ) # populate the new repo with this current zephyr module shutil.copytree(self.zephyr_module_serie_path, self.stm32cube_temp_serie) # commit this current version module self.os_cmd(("git", "add", "*"), cwd=self.stm32cube_temp) self.os_cmd(("git", "commit", "-am", '"module"'), cwd=self.stm32cube_temp) # Remove trailing space self.os_cmd( ("git", "rebase", "--whitespace=fix", "HEAD~1"), cwd=self.stm32cube_temp, ) # generate a patch for files and _hal.conf.h file in the module logging.info( "%s", "Building patch from official " + self.current_version + " to current zephyr module", ) # For unclear reason, using tuple ("git", "diff", ...) is failing on Linux # especially for this command. Keep a single string. self.os_cmd( ("git diff --ignore-space-at-eol HEAD~1 --output=" + self.module_patch), shell=True, cwd=self.stm32cube_temp, ) self.os_cmd(("dos2unix", self.module_patch), cwd=self.stm32cube_temp) def update_readme(self, make_version, make_commit): """Update README file Args: make_version: latest STM32Cube version. make_commit: Commit corresponding to latest STM32Cube version. """ see_release_note = True readme_path = self.zephyr_module_serie_path / "README" with readme_path.open(mode="r") as readme_prev: lines = (x for x in readme_prev.read().splitlines()) readme_path.unlink() # Write README from previous one if exists with open(str(readme_path), "w") as readme_file: for LineItem in lines: # change version nb if "status" in LineItem.lower(): readme_file.write("Status:\n") readme_file.write(f" version {make_version}\n") next(lines) # skip next line elif "commit" in LineItem.lower(): readme_file.write("Commit:\n") readme_file.write(f" {make_commit}") next(lines) # skip next line elif "URL" in LineItem.upper(): readme_file.write("URL:\n") readme_file.write( " https://github.com/STMicroelectronics/" + f"STM32Cube{self.serie}\n" ) next(lines) # skip next line # change patch list with a link to the release_note.html elif "Patch List" in LineItem: readme_file.write("Patch List:\n") readme_file.write( "--> please check that the following list " + "is still valid:\n" ) else: if "See release_note.html from STM32Cube" in LineItem: see_release_note = False readme_file.write(f"{LineItem}\n") # at the very end of the file : if see_release_note: readme_file.write("\n See release_note.html from STM32Cube\n") readme_file.flush() self.os_cmd(("dos2unix", str(readme_path))) def copy_release_note(self): """Copy release_note.html file from STM32Cube to zephyr""" release_note_src = self.stm32cube_serie_path / "Release_Notes.html" release_note_dst = self.zephyr_module_serie_path / "release_note.html" if release_note_dst.exists(): release_note_dst.unlink() if release_note_src.exists: release_note_src.rename(release_note_dst) self.os_cmd(("dos2unix", str(release_note_dst))) def update_cmakelist(self): """Update CMakeLists.txt file""" cmakelists_path = self.zephyr_module_serie_path / "CMakeLists.txt" if cmakelists_path.exists(): # build new CMakeLists.txt with cmakelists_path.open("r") as cmakelists_old: # this line is the copyright line first_line = cmakelists_old.readline() if "STMicroelectronics" in first_line: first_line = "" cmakelists_path.unlink() else: first_line = "" logging.info("%s", "Create a new CMakeLists.txt file") with cmakelists_path.open("w") as cmakelists_new: if first_line: cmakelists_new.write(first_line) cmakelists_new.write("# Copyright (c) 2020 STMicroelectronics\n") cmakelists_new.write("#\n") cmakelists_new.write("# SPDX-License-Identifier: Apache-2.0\n") cmakelists_new.write("\n") src_path = self.stm32cube_temp_serie / "drivers" / "src" source_files = list(src_path.iterdir()) source_files.sort() cmakelists_new.write( f"zephyr_library_sources(soc/system_{self.stm32_seriexx}.c)\n" ) cmakelists_new.write( f"zephyr_library_sources(drivers/src/{self.stm32_seriexx}_hal.c)\n" ) cmakelists_new.write( f"zephyr_library_sources(drivers/src/{self.stm32_seriexx}_hal_rcc.c)\n" ) cmakelists_new.write( "zephyr_library_sources(drivers/src/" + f"{self.stm32_seriexx}_hal_rcc_ex.c)\n" ) for filename in source_files: # also skipping '_xxx_hal.c' if "template" in filename.name: continue # also skipping '_xxx_hal_rcc.c' and '_xxx_hal_rcc_ex.c' if "_hal_rcc" in filename.name: continue if "_hal_" in filename.name: # retrieve IP name from filename, like adc, # which is delimited by # * _hal_ on one side # * and file extension on the other side pattern = r".*_hal_(.*)\..*" cmakelists_new.write( "zephyr_library_sources_ifdef(CONFIG_USE_STM32_HAL_" + re.sub(pattern, r"\1", filename.name).upper() + " drivers/src/" + filename.name + ")\n" ) if "_ll_" in filename.name: # retrieve IP name from filename, like adc, # which is delimited by # * _ll_ on one side # * and file extension on the other side pattern = r".*_ll_(.*)\..*" cmakelists_new.write( "zephyr_library_sources_ifdef(CONFIG_USE_STM32_LL_" + re.sub(pattern, r"\1", filename.name).upper() + " drivers/src/" + filename.name + ")\n" ) self.os_cmd(("dos2unix", str(cmakelists_path))) def generate_assert_file(self): """Remove stm32_assert_template.h file and create stm32_assert.h file""" # remove stm32_assert_template.h stm32_assert_template = ( self.stm32cube_temp_serie / "drivers" / "include" / "stm32_assert_template.h" ) if stm32_assert_template.exists(): stm32_assert_template.unlink() # create stm32_assert.h from Jinja2 template # Create the jinja2 environment. templates_dir = self.zephyr_hal_stm32_path / "scripts" j2_env = Environment( loader=FileSystemLoader(str(templates_dir)), trim_blocks=True, lstrip_blocks=True, ) stm32_assert_j2_template = j2_env.get_template("stm32_assert_template.txt") stm32_assert_h = ( self.stm32cube_temp_serie / "drivers" / "include" / "stm32_assert.h" ) with open(stm32_assert_h, "w") as stm32_assert_file: stm32_assert_file.write( stm32_assert_j2_template.render(stm32serie=self.stm32_serie) ) def build_from_version_update(self): """Build a commit in temporary dir with STM32Cube version corresponding to zephyr latest hal version """ # reset the STM32Cube repo to this latest version self.os_cmd( ("git", "reset", "--hard", self.version_update), cwd=self.stm32cube_serie_path, ) # Get the commit id of this latest version self.update_commit = subprocess.check_output( ("git", "rev-parse", "HEAD"), cwd=self.stm32cube_serie_path ).decode("utf-8") # clear previous version content before populating with latest version shutil.rmtree( str(self.stm32cube_temp_serie), onerror=common_utils.remove_readonly ) # populate temporary directory with latest version self.extract_source() # Commit files except log or patch files self.os_cmd(("git", "add", "*"), cwd=self.stm32cube_serie_path) self.os_cmd(("git", "reset", "--", "*.patch"), cwd=self.stm32cube_serie_path) self.os_cmd(("git", "reset", "--", "*.log"), cwd=self.stm32cube_serie_path) self.os_cmd( ("git", "commit", "-am", '"module' + self.version_update + '"'), cwd=self.stm32cube_temp_serie, ) def apply_zephyr_patch(self): """Apply zephyr stm32 patch to latest stm32Cube version""" logging.info("%s", "Apply zephyr patches to " + self.version_update) # Copy from stm32cube_temp shutil.rmtree( str(self.zephyr_module_serie_path), onerror=common_utils.remove_readonly, ) shutil.copytree(self.stm32cube_temp_serie, self.zephyr_module_serie_path) # apply dos2unix to whole zephyr hal serie sub directory for child in self.zephyr_module_serie_path.glob("**/*"): if child.is_file: self.os_cmd(("dos2unix", child), cwd=self.zephyr_module_serie_path) # Remove file that will be fully created by zephyr patch # (otherwise applying patch will report error) for file in zephyr_file_created: if Path(self.zephyr_module_serie_path, file).exists(): Path(self.zephyr_module_serie_path, file).unlink() # Apply patch from new repo common_utils.apply_patch( str(self.stm32cube_temp / self.module_patch), self.zephyr_hal_stm32_path ) # save patch file so that it can be analysed in case of error patch_path = self.zephyr_hal_stm32_path / self.module_patch if patch_path.exists(): os.remove(patch_path) shutil.copy(str(self.stm32cube_temp / self.module_patch), patch_path) # Update README and CMakeList, copy release note self.update_readme(self.version_update, self.update_commit) self.update_cmakelist() self.copy_release_note() # remove stm32_assert_template.h and create stm32_assert.h self.generate_assert_file() # Add files but do not commit self.os_cmd(("git", "add", "*"), cwd=self.zephyr_hal_stm32_path) self.os_cmd(("git", "reset", "--", "*.patch"), cwd=self.zephyr_hal_stm32_path) self.os_cmd(("git", "reset", "--", "*.log"), cwd=self.zephyr_hal_stm32_path) self.os_cmd(("git", "reset", "--", "*.rej"), cwd=self.zephyr_hal_stm32_path) logging.warning( "%s", "README file : --> please check that the Patch list is still valid", ) def merge_commit(self, lib=False): """Apply zephyr stm32 patch to latest stm32Cube version""" # Merge & commit # to clean the .rej files, uncomment line: reject() # reject() if lib: logging.info("%s", "commit BLE library update") commit_msg = "lib/stm32: " else: logging.info("%s", "commit HAL/LL Cube update ") commit_msg = "stm32cube: " commit_file_path = self.zephyr_module_serie_path / "commit.msg" with open(commit_file_path, "w") as commit: commit.write( commit_msg + "update " + self.stm32_serie + " to cube version " + self.version_update.upper() + "\n" ) commit.write("\n") commit.write( "Update Cube version for " + self.stm32_seriexx_upper + " series" + "\n" ) commit.write("on https://github.com/STMicroelectronics" + "\n") commit.write(f"from version {self.current_version}\n") commit.write(f"to version {self.version_update}\n") self.os_cmd( ("git", "commit", "-as", "-F", commit_file_path), cwd=self.zephyr_module_serie_path, ) self.os_cmd( ("git", "rebase", "--whitespace=fix", "HEAD~1"), cwd=self.zephyr_module_serie_path, ) Path(commit_file_path).unlink() def reject(self): """Clean *.rej files""" reject_files = [f for f in self.zephyr_module_serie_path.glob("**/*.rej")] if reject_files: for f in reject_files: f.unlink() def cleanup_stm32cube_repo(self): """clean the STM32Cube repo""" self.os_cmd(("git", "reset", "--hard", "HEAD"), cwd=self.stm32cube_serie_path) def clean_files(self): """Clean repo file if required""" # Remove temporary files unconditionally os.chdir(os.getenv("HOME")) shutil.rmtree(str(self.stm32cube_temp), onerror=common_utils.remove_readonly) # remove STM32Cube repo only if required if not self.noclean: self.cleanup_stm32cube_repo() shutil.rmtree( str(self.stm32cube_repo_path), onerror=common_utils.remove_readonly ) else: self.os_cmd( ("git", "reset", "--hard", "HEAD"), cwd=self.stm32cube_serie_path, ) def update_stm32_hal_serie(self): """Update one single STM32Cube""" # 1) clone full repo from github : get the repo STM32CubeXX self.clone_cube_repo() # 2) prepare a repo where to store module versions self.os_cmd(("git", "init"), cwd=self.stm32cube_temp) self.os_cmd( ("git", "commit", "--allow-empty", "-m", "'Trigger notification'"), cwd=self.stm32cube_temp, ) # 3) get the version of cube which is in the zephyr module self.current_version = self.get_zephyr_current_version() logging.info( f"Version {self.current_version} is the current zephyr version for " + self.zephyr_module_serie_path.name ) # do not process if versions are similar if (self.current_version in self.version_update) or ( self.version_update in self.current_version ): logging.warning( "%s", f"*** Update abandoned: identical versions {self.version_update} ***\n", ) self.clean_files() return logging.info( f"*** Updating zephyr {self.stm32_serie} " + f"from version {self.current_version} to {self.version_update}" ) # 4) build the module from this previous version self.build_from_current_cube_version() # 5) build the patch from the current zephyr version self.build_patch_from_current_zephyr_version() # 6) build the module from this latest version self.build_from_version_update() # 7) apply zephyr patch : in the zephyr module repo self.apply_zephyr_patch() self.merge_commit() # 8) In case of stm32wb, update ble library if self.stm32_serie == "stm32wb": ble_library.update( self.stm32cube_serie_path, Path(self.zephyr_hal_stm32_path / "lib"), self.stm32cube_temp, self.current_version, self.version_update, self.update_commit, ) self.merge_commit(lib=True) # 9) clean self.clean_files() logging.info("%s", f"Done {self.stm32_serie}\n")