1#!/usr/bin/env python3 2# SPDX-License-Identifier: BSD-3-Clause 3 4# Too much noise for now, these can be re-enabled after they've been 5# fixed (if that does not break `git blame` too much) 6 7# W0311, W0312, W0603 8# pylint:disable=bad-indentation 9# pylint:disable=mixed-indentation 10# pylint:disable=global-statement 11 12# C0103, C0114, C0116 13# pylint:disable=invalid-name 14# pylint:disable=missing-module-docstring 15# pylint:disable=missing-function-docstring 16 17# Non-indentation whitespace has been removed from newer pylint. It does 18# not hurt to keep them for older versions. The recommendation is to use 19# a formatter like `black` instead, unfortunately this would totally 20# destroy git blame, git revert, etc. 21 22# C0326, C0330 23# pylint:disable=bad-whitespace 24# pylint:disable=bad-continuation 25 26import argparse 27import shlex 28import subprocess 29import pathlib 30import errno 31import platform as py_platform 32import sys 33import shutil 34import os 35import warnings 36import fnmatch 37import hashlib 38import gzip 39import dataclasses 40import concurrent.futures as concurrent 41 42# anytree module is defined in Zephyr build requirements 43from anytree import AnyNode, RenderTree, render 44from packaging import version 45 46# https://chrisyeh96.github.io/2017/08/08/definitive-guide-python-imports.html#case-3-importing-from-parent-directory 47sys.path.insert(1, os.path.join(sys.path[0], '..')) 48from tools.sof_ri_info import sof_ri_info 49 50MIN_PYTHON_VERSION = 3, 8 51assert sys.version_info >= MIN_PYTHON_VERSION, \ 52 f"Python {MIN_PYTHON_VERSION} or above is required." 53 54# Version of this script matching Major.Minor.Patch style. 55VERSION = version.Version("2.0.0") 56 57# Constant value resolves SOF_TOP directory as: "this script directory/.." 58SOF_TOP = pathlib.Path(__file__).parents[1].resolve() 59west_top = pathlib.Path(SOF_TOP, "..").resolve() 60default_rimage_key = pathlib.Path(SOF_TOP, "keys", "otc_private_key.pem") 61 62sof_fw_version = None 63sof_build_version = None 64 65if py_platform.system() == "Windows": 66 xtensa_tools_version_postfix = "-win32" 67elif py_platform.system() == "Linux": 68 xtensa_tools_version_postfix = "-linux" 69else: 70 xtensa_tools_version_postfix = "-unsupportedOS" 71 warnings.warn(f"Your operating system: {py_platform.system()} is not supported") 72 73 74@dataclasses.dataclass 75class PlatformConfig: 76 "Product parameters" 77 name: str 78 PLAT_CONFIG: str 79 XTENSA_TOOLS_VERSION: str 80 XTENSA_CORE: str 81 DEFAULT_TOOLCHAIN_VARIANT: str = "xt-clang" 82 RIMAGE_KEY: pathlib.Path = pathlib.Path(SOF_TOP, "keys", "otc_private_key_3k.pem") 83 IPC4_RIMAGE_DESC: str = None 84 IPC4_CONFIG_OVERLAY: str = "ipc4_overlay.conf" 85 86platform_configs = { 87 # Intel platforms 88 "tgl" : PlatformConfig( 89 "tgl", "intel_adsp_cavs25", 90 f"RG-2017.8{xtensa_tools_version_postfix}", 91 "cavs2x_LX6HiFi3_2017_8", 92 "xcc", 93 IPC4_RIMAGE_DESC = "tgl-cavs.toml", 94 ), 95 "tgl-h" : PlatformConfig( 96 "tgl-h", "intel_adsp_cavs25_tgph", 97 f"RG-2017.8{xtensa_tools_version_postfix}", 98 "cavs2x_LX6HiFi3_2017_8", 99 "xcc", 100 IPC4_RIMAGE_DESC = "tgl-h-cavs.toml", 101 ), 102 "mtl" : PlatformConfig( 103 "mtl", "intel_adsp_ace15_mtpm", 104 f"RI-2022.10{xtensa_tools_version_postfix}", 105 "ace10_LX7HiFi4_2022_10", 106 ), 107 # NXP platforms 108 "imx8" : PlatformConfig( 109 "imx8", "nxp_adsp_imx8", 110 None, None, 111 RIMAGE_KEY = "key param ignored by imx8", 112 ), 113 "imx8x" : PlatformConfig( 114 "imx8x", "nxp_adsp_imx8x", 115 None, None, 116 RIMAGE_KEY = "key param ignored by imx8x" 117 ), 118 "imx8m" : PlatformConfig( 119 "imx8m", "nxp_adsp_imx8m", 120 None, None, 121 RIMAGE_KEY = "key param ignored by imx8m" 122 ), 123} 124 125platform_names = list(platform_configs) 126 127class validate_platforms_arguments(argparse.Action): 128 """Validates positional platform arguments whether provided platform name is supported.""" 129 def __call__(self, parser, namespace, values, option_string=None): 130 if values: 131 for value in values: 132 if value not in platform_names: 133 raise argparse.ArgumentError(self, f"Unsupported platform: {value}") 134 setattr(namespace, "platforms", values) 135 136args = None 137def parse_args(): 138 global args 139 global west_top 140 parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, 141 epilog=("This script supports XtensaTools but only when installed in a specific\n" + 142 "directory structure, example:\n" + 143 "myXtensa/\n" + 144 "└── install/\n" + 145 " ├── builds/\n" + 146 " │ ├── RD-2012.5{}/\n".format(xtensa_tools_version_postfix) + 147 " │ │ └── Intel_HiFiEP/\n" + 148 " │ └── RG-2017.8{}/\n".format(xtensa_tools_version_postfix) + 149 " │ ├── LX4_langwell_audio_17_8/\n" + 150 " │ └── X4H3I16w2D48w3a_2017_8/\n" + 151 " └── tools/\n" + 152 " ├── RD-2012.5{}/\n".format(xtensa_tools_version_postfix) + 153 " │ └── XtensaTools/\n" + 154 " └── RG-2017.8{}/\n".format(xtensa_tools_version_postfix) + 155 " └── XtensaTools/\n" + 156 "$XTENSA_TOOLS_ROOT=/path/to/myXtensa ...\n" + 157 f"Supported platforms {platform_names}")) 158 159 parser.add_argument("-a", "--all", required=False, action="store_true", 160 help="Build all currently supported platforms") 161 parser.add_argument("platforms", nargs="*", action=validate_platforms_arguments, 162 help="List of platforms to build") 163 parser.add_argument("-d", "--debug", required=False, action="store_true", 164 help="Enable debug build") 165 parser.add_argument("-i", "--ipc", required=False, choices=["IPC4"], 166 help="""Applies --overlay <platform>/ipc4_overlay.conf 167and a different rimage config. Valid only for IPC3 platforms supporting IPC4 too.""") 168 # NO SOF release will ever user the option --fw-naming. 169 # This option is only for disguising SOF IPC4 as CAVS IPC4 and only in cases where 170 # the kernel 'ipc_type' expects CAVS IPC4. In this way, developers and CI can test 171 # IPC4 on older platforms. 172 parser.add_argument("--fw-naming", required=False, choices=["AVS", "SOF"], 173 default="SOF", help=""" 174Determine firmware naming conversion and folder structure 175For SOF: 176 /lib/firmware/intel/sof 177 └───────community 178 │ └── sof-tgl.ri 179 ├── dbgkey 180 │ └── sof-tgl.ri 181 └── sof-tgl.ri 182For AVS(filename dsp_basefw.bin): 183Noted that with fw_naming set as 'AVS', there will be output subdirectories for each platform 184 /lib/firmware/intel/sof-ipc4 185 └── tgl 186 ├── community 187 │ └── dsp_basefw.bin 188 ├── dbgkey 189 │ └── dsp_basefw.bin 190 └── dsp_basefw.bin""" 191 ) 192 parser.add_argument("-j", "--jobs", required=False, type=int, 193 help="Number of concurrent jobs. Passed to west build and" 194 " to cmake (for rimage)") 195 parser.add_argument("-k", "--key", type=pathlib.Path, required=False, 196 help="Path to a non-default rimage signing key.") 197 parser.add_argument("-o", "--overlay", type=pathlib.Path, required=False, action='append', 198 default=[], help="Paths to overlays") 199 parser.add_argument("-p", "--pristine", required=False, action="store_true", 200 help="Perform pristine build removing build directory.") 201 parser.add_argument("-u", "--update", required=False, action="store_true", 202 help="""Runs west update command - clones SOF dependencies. Downloads next to this sof clone a new Zephyr 203project with its required dependencies. Creates a modules/audio/sof symbolic link pointing 204back at this sof clone. All projects are checkout out to 205revision defined in manifests of SOF and Zephyr.""") 206 parser.add_argument('-v', '--verbose', default=0, action='count', 207 help="""Verbosity level. Repetition of the flag increases verbosity. 208The same number of '-v' is passed to "west". 209""", 210 ) 211 # Cannot use a standard -- delimiter because argparse deletes it. 212 parser.add_argument("-C", "--cmake-args", action='append', default=[], 213 help="""Cmake arguments passed as is to cmake configure step. 214Can be passed multiple times; whitespace is preserved Example: 215 216 -C=--warn-uninitialized -C '-DEXTRA_FLAGS=-Werror -g0' 217 218Note '-C --warn-uninitialized' is not supported by argparse, an equal 219sign must be used (https://bugs.python.org/issue9334)""", 220 ) 221 222 parser.add_argument("--key-type-subdir", default="community", 223 choices=["community", "none", "dbgkey"], 224 help="""Output subdirectory for rimage signing key type. 225Default key type subdirectory is \"community\".""") 226 227 228 parser.add_argument("--use-platform-subdir", default = False, 229 action="store_true", 230 help="""Use an output subdirectory for each platform. 231Otherwise, all firmware files are installed in the same staging directory by default.""") 232 233 parser.add_argument("--no-interactive", default=False, action="store_true", 234 help="""Run script in non-interactive mode when user input can not be provided. 235This should be used with programmatic script invocations (eg. Continuous Integration). 236 """) 237 parser.add_argument("--version", required=False, action="store_true", 238 help="Prints version of this script.") 239 240 args = parser.parse_args() 241 242 if args.all: 243 args.platforms = platform_names 244 245 # print help message if no arguments provided 246 if len(sys.argv) == 1: 247 parser.print_help() 248 sys.exit(0) 249 250 if args.fw_naming == 'AVS': 251 if not args.use_platform_subdir: 252 args.use_platform_subdir=True 253 warnings.warn("The option '--fw-naming AVS' has to be used with '--use-platform-subdir'. Enable '--use-platform-subdir' automatically.") 254 if args.ipc != "IPC4": 255 args.ipc="IPC4" 256 warnings.warn("The option '--fw-naming AVS' has to be used with '-i IPC4'. Enable '-i IPC4' automatically.") 257 258 259def execute_command(*run_args, **run_kwargs): 260 """[summary] Provides wrapper for subprocess.run that prints 261 command executed when 'more verbose' verbosity level is set.""" 262 command_args = run_args[0] 263 264 # If you really need the shell in some non-portable section then 265 # invoke subprocess.run() directly. 266 if run_kwargs.get('shell') or not isinstance(command_args, list): 267 raise RuntimeError("Do not rely on non-portable shell parsing") 268 269 if args.verbose >= 0: 270 cwd = run_kwargs.get('cwd') 271 print_cwd = f"In dir: {cwd}" if cwd else f"in current dir: {os.getcwd()}" 272 print_args = shlex.join(command_args) 273 output = f"{print_cwd}; running command:\n {print_args}" 274 env_arg = run_kwargs.get('env') 275 env_change = set(env_arg.items()) - set(os.environ.items()) if env_arg else None 276 if env_change: 277 output += "\n... with extra/modified environment:" 278 for k_v in env_change: 279 output += f"\n{k_v[0]}={k_v[1]}" 280 print(output, flush=True) 281 282 283 if run_kwargs.get('check') is None: 284 run_kwargs['check'] = True 285 #pylint:disable=subprocess-run-check 286 287 return subprocess.run(*run_args, **run_kwargs) 288 289 290def show_installed_files(): 291 """[summary] Scans output directory building binary tree from files and folders 292 then presents them in similar way to linux tree command.""" 293 graph_root = AnyNode(name=STAGING_DIR.name, long_name=".", parent=None) 294 relative_entries = [ 295 entry.relative_to(STAGING_DIR) for entry in sorted(STAGING_DIR.glob("**/*")) 296 ] 297 nodes = [ graph_root ] 298 for entry in relative_entries: 299 # Node's documentation does allow random attributes 300 # pylint: disable=no-member 301 # sorted() makes sure our parent is already there. 302 # This is slightly awkward, a recursive function would be more readable 303 matches = [node for node in nodes if node.long_name == str(entry.parent)] 304 assert len(matches) == 1, f'"{entry}" does not have exactly one parent' 305 nodes.append(AnyNode(name=entry.name, long_name=str(entry), parent=matches[0])) 306 307 for pre, _, node in RenderTree(graph_root, render.AsciiStyle): 308 fpath = STAGING_DIR / node.long_name 309 stem = node.name[:-3] if node.name.endswith(".gz") else node.name 310 311 shasum_trailer = "" 312 if checksum_wanted(stem) and fpath.is_file() and not fpath.is_symlink(): 313 shasum_trailer = "\tsha256=" + checksum(fpath) 314 315 print(f"{pre}{node.name} {shasum_trailer}") 316 317 318# TODO: among other things in this file it should be less SOF-specific; 319# try to move as much as possible to generic Zephyr code. See 320# discussions in https://github.com/zephyrproject-rtos/zephyr/pull/51954 321def checksum_wanted(stem): 322 for pattern in CHECKSUM_WANTED: 323 if fnmatch.fnmatch(stem, pattern): 324 return True 325 return False 326 327 328def checksum(fpath): 329 if fpath.suffix == ".gz": 330 inputf = gzip.GzipFile(fpath, "rb") 331 else: 332 inputf = open(fpath, "rb") 333 chksum = hashlib.sha256(inputf.read()).hexdigest() 334 inputf.close() 335 return chksum 336 337 338def check_west_installation(): 339 west_path = shutil.which("west") 340 if not west_path: 341 raise FileNotFoundError("Install west and a west toolchain," 342 "https://docs.zephyrproject.org/latest/getting_started/index.html") 343 print(f"Found west: {west_path}") 344 345def west_reinitialize(west_root_dir: pathlib.Path, west_manifest_path: pathlib.Path): 346 """[summary] Performs west reinitialization to SOF manifest file asking user for permission. 347 Prints error message if script is running in non-interactive mode. 348 349 :param west_root_dir: directory where is initialized. 350 :type west_root_dir: pathlib.Path 351 :param west_manifest_path: manifest file to which west is initialized. 352 :type west_manifest_path: pathlib.Path 353 :raises RuntimeError: Raised when west is initialized to wrong manifest file 354 (not SOFs manifest) and script is running in non-interactive mode. 355 """ 356 global west_top 357 message = "West is initialized to manifest other than SOFs!\n" 358 message += f"Initialized to manifest: {west_manifest_path}." + "\n" 359 dot_west_directory = pathlib.Path(west_root_dir.resolve(), ".west") 360 if args.no_interactive: 361 message += f"Try deleting {dot_west_directory } directory and rerun this script." 362 raise RuntimeError(message) 363 question = message + "Reinitialize west to SOF manifest? [Y/n] " 364 print(f"{question}") 365 while True: 366 reinitialize_answer = input().lower() 367 if reinitialize_answer in ["y", "n"]: 368 break 369 sys.stdout.write('Please respond with \'Y\' or \'n\'.\n') 370 371 if reinitialize_answer != 'y': 372 sys.exit("Can not proceed. Reinitialize your west manifest to SOF and rerun this script.") 373 shutil.rmtree(dot_west_directory) 374 execute_command(["west", "init", "-l", f"{SOF_TOP}"], cwd=west_top) 375 376def west_init_if_needed(): 377 """[summary] Validates whether west workspace had been initialized and points to SOF manifest. 378 Peforms west initialization if needed. 379 """ 380 global west_top, SOF_TOP 381 west_manifest_path = pathlib.Path(SOF_TOP, "west.yml") 382 result_rootdir = execute_command(["west", "topdir"], capture_output=True, cwd=west_top, 383 timeout=10, check=False) 384 if result_rootdir.returncode != 0: 385 execute_command(["west", "init", "-l", f"{SOF_TOP}"], cwd=west_top) 386 return 387 west_root_dir = pathlib.Path(result_rootdir.stdout.decode().rstrip()).resolve() 388 result_manifest_dir = execute_command(["west", "config", "manifest.path"], capture_output=True, 389 cwd=west_top, timeout=10, check=True) 390 west_manifest_dir = pathlib.Path(west_root_dir, result_manifest_dir.stdout.decode().rstrip()).resolve() 391 manifest_file_result = execute_command(["west", "config", "manifest.file"], capture_output=True, 392 cwd=west_top, timeout=10, check=True) 393 returned_manifest_path = pathlib.Path(west_manifest_dir, manifest_file_result.stdout.decode().rstrip()) 394 if not returned_manifest_path.samefile(west_manifest_path): 395 west_reinitialize(west_root_dir, returned_manifest_path) 396 else: 397 print(f"West workspace: {west_root_dir}") 398 print(f"West manifest path: {west_manifest_path}") 399 400def create_zephyr_directory(): 401 global west_top 402 # Do not fail when there's only an empty directory left over 403 # (because of some early interruption of this script or proxy 404 # misconfiguration, etc.) 405 try: 406 # rmdir() is safe: it deletes empty directories ONLY. 407 west_top.rmdir() 408 except OSError as oserr: 409 if oserr.errno not in [errno.ENOTEMPTY, errno.ENOENT]: 410 raise oserr 411 # else when not empty then let the next line fail with a 412 # _better_ error message: 413 # "zephyrproject already exists" 414 415 west_top.mkdir(parents=False, exist_ok=False) 416 west_top = west_top.resolve(strict=True) 417 418def create_zephyr_sof_symlink(): 419 global west_top, SOF_TOP 420 if not west_top.exists(): 421 raise FileNotFoundError("No west top: {}".format(west_top)) 422 audio_modules_dir = pathlib.Path(west_top, "modules", "audio") 423 audio_modules_dir.mkdir(parents=True, exist_ok=True) 424 sof_symlink = pathlib.Path(audio_modules_dir, "sof") 425 # Symlinks creation requires administrative privileges in Windows or special user rights 426 try: 427 if not sof_symlink.exists(): 428 sof_symlink.symlink_to(SOF_TOP, target_is_directory=True) 429 except: 430 print(f"Failed to create symbolic link: {sof_symlink} to {SOF_TOP}." 431 "\nIf you run script on Windows run it with administrative privileges or\n" 432 "grant user symlink creation rights -" 433 "see: https://docs.microsoft.com/en-us/windows/security/threat-protection/" 434 "security-policy-settings/create-symbolic-links") 435 raise 436 437def west_update(): 438 """[summary] Clones all west manifest projects to specified revisions""" 439 global west_top 440 execute_command(["west", "update"], check=True, timeout=3000, cwd=west_top) 441 442 443def get_build_and_sof_version(abs_build_dir): 444 """[summary] Get version string major.minor.micro and build of SOF 445 firmware file. When building multiple platforms from the same SOF 446 commit, all platforms share the same version. So for the 1st platform, 447 generate the version string from sof_version.h and later platforms will 448 reuse it. 449 """ 450 global sof_fw_version 451 global sof_build_version 452 if sof_fw_version and sof_build_version: 453 return sof_fw_version, sof_build_version 454 455 versions = {} 456 with open(pathlib.Path(abs_build_dir, 457 "zephyr/include/generated/sof_versions.h"), encoding="utf8") as hfile: 458 for hline in hfile: 459 words = hline.split() 460 if words[0] == '#define': 461 versions[words[1]] = words[2] 462 sof_fw_version = versions['SOF_MAJOR'] + '.' + versions['SOF_MINOR'] + '.' + \ 463 versions['SOF_MICRO'] 464 sof_build_version = versions['SOF_BUILD'] 465 466 return sof_fw_version, sof_build_version 467 468def rmtree_if_exists(directory): 469 "This is different from ignore_errors=False because it deletes everything or nothing" 470 if os.path.exists(directory): 471 shutil.rmtree(directory) 472 473def clean_staging(platform): 474 print(f"Cleaning {platform} from {STAGING_DIR}") 475 476 rmtree_if_exists(STAGING_DIR / "sof-info" / platform) 477 478 sof_output_dir = STAGING_DIR / "sof" 479 480 # --use-platform-subdir 481 rmtree_if_exists(sof_output_dir / platform) 482 483 # Remaining .ri and .ldc files 484 for f in sof_output_dir.glob(f"**/sof-{platform}.*"): 485 os.remove(f) 486 487 488RIMAGE_BUILD_DIR = west_top / "build-rimage" 489 490# Paths in `west.yml` must be "static", we cannot have something like a 491# variable "$my_sof_path/rimage/" checkout. In the future "rimage/" will 492# be moved one level up and it won't be nested inside "sof/" anymore. But 493# for now we must stick to `sof/rimage/[tomlc99]` for 494# backwards-compatibility with XTOS platforms and git submodules, see more 495# detailed comments in west.yml 496RIMAGE_SOURCE_DIR = west_top / "sof" / "rimage" 497 498def build_rimage(): 499 500 # Detect non-west rimage duplicates, example: git submdule 501 # SOF_TOP/rimage = sof2/rimage 502 nested_rimage = pathlib.Path(SOF_TOP, "rimage") 503 if nested_rimage.is_dir() and not nested_rimage.samefile(RIMAGE_SOURCE_DIR): 504 raise RuntimeError( 505 f"""Two rimage source directories found. 506 Move non-west {nested_rimage} out of west workspace {west_top}. 507 See output of 'west list'.""" 508 ) 509 rimage_dir_name = RIMAGE_BUILD_DIR.name 510 # CMake build rimage module 511 if not (RIMAGE_BUILD_DIR / "CMakeCache.txt").is_file(): 512 execute_command(["cmake", "-B", rimage_dir_name, "-G", "Ninja", 513 "-S", str(RIMAGE_SOURCE_DIR)], 514 cwd=west_top) 515 rimage_build_cmd = ["cmake", "--build", rimage_dir_name] 516 if args.jobs is not None: 517 rimage_build_cmd.append(f"-j{args.jobs}") 518 if args.verbose > 1: 519 rimage_build_cmd.append("-v") 520 execute_command(rimage_build_cmd, cwd=west_top) 521 522 523STAGING_DIR = None 524def build_platforms(): 525 global west_top, SOF_TOP 526 print(f"SOF_TOP={SOF_TOP}") 527 print(f"west_top={west_top}") 528 529 global STAGING_DIR 530 STAGING_DIR = pathlib.Path(west_top, "build-sof-staging") 531 # Don't leave the install of an old build behind 532 if args.pristine: 533 rmtree_if_exists(STAGING_DIR) 534 else: 535 # This is important in (at least) two use cases: 536 # - when switching `--use-platform-subdir` on/off or changing key subdir, 537 # - when the build starts failing after a code change. 538 # Do not delete platforms that were not requested so this script can be 539 # invoked once per platform. 540 for platform in args.platforms: 541 clean_staging(platform) 542 rmtree_if_exists(STAGING_DIR / "tools") 543 544 545 # smex does not use 'install -D' 546 sof_output_dir = pathlib.Path(STAGING_DIR, "sof") 547 sof_output_dir.mkdir(parents=True, exist_ok=True) 548 for platform in args.platforms: 549 platf_build_environ = os.environ.copy() 550 if args.use_platform_subdir: 551 sof_platform_output_dir = pathlib.Path(sof_output_dir, platform) 552 sof_platform_output_dir.mkdir(parents=True, exist_ok=True) 553 else: 554 sof_platform_output_dir = sof_output_dir 555 556 # For now convert the new dataclass to what it used to be 557 _dict = dataclasses.asdict(platform_configs[platform]) 558 platform_dict = { k:v for (k,v) in _dict.items() if _dict[k] is not None } 559 560 xtensa_tools_root_dir = os.getenv("XTENSA_TOOLS_ROOT") 561 # when XTENSA_TOOLS_ROOT environmental variable is set, 562 # use user installed Xtensa tools not Zephyr SDK 563 if "XTENSA_TOOLS_VERSION" in platform_dict and xtensa_tools_root_dir: 564 xtensa_tools_root_dir = pathlib.Path(xtensa_tools_root_dir) 565 if not xtensa_tools_root_dir.is_dir(): 566 raise RuntimeError(f"Platform {platform} uses Xtensa toolchain." 567 "\nVariable XTENSA_TOOLS_VERSION points path that does not exist\n" 568 "or is not a directory") 569 570 # set variables expected by zephyr/cmake/toolchain/xcc/generic.cmake 571 platf_build_environ["ZEPHYR_TOOLCHAIN_VARIANT"] = platf_build_environ.get("ZEPHYR_TOOLCHAIN_VARIANT", 572 platform_dict["DEFAULT_TOOLCHAIN_VARIANT"]) 573 XTENSA_TOOLCHAIN_PATH = str(pathlib.Path(xtensa_tools_root_dir, "install", 574 "tools").absolute()) 575 platf_build_environ["XTENSA_TOOLCHAIN_PATH"] = XTENSA_TOOLCHAIN_PATH 576 TOOLCHAIN_VER = platform_dict["XTENSA_TOOLS_VERSION"] 577 XTENSA_CORE = platform_dict["XTENSA_CORE"] 578 platf_build_environ["TOOLCHAIN_VER"] = TOOLCHAIN_VER 579 580 # Set variables expected by xcc toolchain. CMake cannot set (evil) build-time 581 # environment variables at configure time: 582 # https://gitlab.kitware.com/cmake/community/-/wikis/FAQ#how-can-i-get-or-set-environment-variables 583 XTENSA_BUILDS_DIR=str(pathlib.Path(xtensa_tools_root_dir, "install", "builds", 584 TOOLCHAIN_VER).absolute()) 585 XTENSA_SYSTEM = str(pathlib.Path(XTENSA_BUILDS_DIR, XTENSA_CORE, "config").absolute()) 586 platf_build_environ["XTENSA_SYSTEM"] = XTENSA_SYSTEM 587 588 platform_build_dir_name = f"build-{platform}" 589 590 # https://docs.zephyrproject.org/latest/guides/west/build-flash-debug.html#one-time-cmake-arguments 591 # https://github.com/zephyrproject-rtos/zephyr/pull/40431#issuecomment-975992951 592 abs_build_dir = pathlib.Path(west_top, platform_build_dir_name) 593 if (pathlib.Path(abs_build_dir, "build.ninja").is_file() 594 or pathlib.Path(abs_build_dir, "Makefile").is_file()): 595 if args.cmake_args and not args.pristine: 596 print(args.cmake_args) 597 raise RuntimeError("Some CMake arguments are ignored in incremental builds, " 598 + f"you must delete {abs_build_dir} first") 599 600 PLAT_CONFIG = platform_dict["PLAT_CONFIG"] 601 build_cmd = ["west"] 602 build_cmd += ["-v"] * args.verbose 603 build_cmd += ["build", "--build-dir", platform_build_dir_name] 604 source_dir = pathlib.Path(SOF_TOP, "app") 605 build_cmd += ["--board", PLAT_CONFIG, str(source_dir)] 606 if args.pristine: 607 build_cmd += ["-p", "always"] 608 609 if args.jobs is not None: 610 build_cmd += [f"--build-opt=-j{args.jobs}"] 611 612 build_cmd.append('--') 613 if args.cmake_args: 614 build_cmd += args.cmake_args 615 616 overlays = [str(item.resolve(True)) for item in args.overlay] 617 # The '-d' option is a shortcut for '-o path_to_debug_overlay', we are good 618 # if both are provided, because it's no harm to merge the same overlay twice. 619 if args.debug: 620 overlays.append(str(pathlib.Path(SOF_TOP, "app", "debug_overlay.conf"))) 621 622 # The '-i IPC4' is a shortcut for '-o path_to_ipc4_overlay' (and more), we 623 # are good if both are provided, because it's no harm to merge the same 624 # overlay twice. 625 if args.ipc == "IPC4": 626 overlays.append(str(pathlib.Path(SOF_TOP, "app", "overlays", platform, 627 platform_dict["IPC4_CONFIG_OVERLAY"]))) 628 629 if overlays: 630 overlays = ";".join(overlays) 631 build_cmd.append(f"-DOVERLAY_CONFIG={overlays}") 632 633 # Build 634 try: 635 execute_command(build_cmd, cwd=west_top, env=platf_build_environ) 636 except subprocess.CalledProcessError as cpe: 637 zephyr_path = pathlib.Path(west_top, "zephyr") 638 if not os.path.exists(zephyr_path): 639 sys.exit("Zephyr project not found. Please run this script with -u flag or `west update zephyr` manually.") 640 else: # unknown failure 641 raise cpe 642 643 smex_executable = pathlib.Path(west_top, platform_build_dir_name, "zephyr", "smex_ep", 644 "build", "smex") 645 fw_ldc_file = pathlib.Path(sof_platform_output_dir, f"sof-{platform}.ldc") 646 input_elf_file = pathlib.Path(west_top, platform_build_dir_name, "zephyr", "zephyr.elf") 647 # Extract metadata 648 execute_command([str(smex_executable), "-l", str(fw_ldc_file), str(input_elf_file)]) 649 650 # Sign firmware 651 rimage_executable = shutil.which("rimage", path=RIMAGE_BUILD_DIR) 652 rimage_config = RIMAGE_SOURCE_DIR / "config" 653 sign_cmd = ["west"] 654 sign_cmd += ["-v"] * args.verbose 655 sign_cmd += ["sign", "--build-dir", platform_build_dir_name, "--tool", "rimage"] 656 sign_cmd += ["--tool-path", rimage_executable] 657 signing_key = "" 658 if args.key: 659 signing_key = args.key 660 elif "RIMAGE_KEY" in platform_dict: 661 signing_key = platform_dict["RIMAGE_KEY"] 662 else: 663 signing_key = default_rimage_key 664 665 sign_cmd += ["--tool-data", str(rimage_config), "--", "-k", str(signing_key)] 666 667 sof_fw_vers, sof_build_vers = get_build_and_sof_version(abs_build_dir) 668 669 sign_cmd += ["-f", sof_fw_vers] 670 671 sign_cmd += ["-b", sof_build_vers] 672 673 if args.ipc == "IPC4": 674 rimage_desc = pathlib.Path(SOF_TOP, "rimage", "config", platform_dict["IPC4_RIMAGE_DESC"]) 675 sign_cmd += ["-c", str(rimage_desc)] 676 677 execute_command(sign_cmd, cwd=west_top) 678 679 if platform not in RI_INFO_UNSUPPORTED: 680 reproducible_checksum(platform, west_top / platform_build_dir_name / "zephyr" / "zephyr.ri") 681 682 install_platform(platform, sof_platform_output_dir, platf_build_environ) 683 684 src_dest_list = [] 685 686 # Install sof-logger 687 sof_logger_dir = pathlib.Path(west_top, platform_build_dir_name, "zephyr", 688 "sof-logger_ep", "build", "logger") 689 sof_logger_executable_to_copy = pathlib.Path(shutil.which("sof-logger", path=sof_logger_dir)) 690 tools_output_dir = pathlib.Path(STAGING_DIR, "tools") 691 sof_logger_installed_file = pathlib.Path(tools_output_dir, sof_logger_executable_to_copy.name).resolve() 692 693 src_dest_list += [(sof_logger_executable_to_copy, sof_logger_installed_file)] 694 695 src_dest_list += [(pathlib.Path(SOF_TOP) / 696 "tools" / "mtrace"/ "mtrace-reader.py", 697 tools_output_dir)] 698 699 # Append future files to `src_dest_list` here (but prefer 700 # copying entire directories; more flexible) 701 702 for _src, _dst in src_dest_list: 703 os.makedirs(os.path.dirname(_dst), exist_ok=True) 704 # looses file owner and group - file is commonly accessible 705 shutil.copy2(str(_src), str(_dst)) 706 707 # cavstool and friends 708 shutil.copytree(pathlib.Path(west_top) / 709 "zephyr" / "soc" / "xtensa" / "intel_adsp" / "tools", 710 tools_output_dir, 711 symlinks=True, ignore_dangling_symlinks=True, dirs_exist_ok=True) 712 713 714def install_platform(platform, sof_platform_output_dir, platf_build_environ): 715 716 # Keep in sync with caller 717 platform_build_dir_name = f"build-{platform}" 718 719 # Install to STAGING_DIR 720 abs_build_dir = pathlib.Path(west_top) / platform_build_dir_name / "zephyr" 721 722 if args.fw_naming == "AVS": 723 # Disguise ourselves for local testing purposes 724 output_fwname = "dsp_basefw.bin" 725 else: 726 # Regular name 727 output_fwname = "".join(["sof-", platform, ".ri"]) 728 729 shutil.copy2(abs_build_dir / "zephyr.ri", abs_build_dir / output_fwname) 730 fw_file_to_copy = abs_build_dir / output_fwname 731 732 install_key_dir = sof_platform_output_dir 733 if args.key_type_subdir != "none": 734 install_key_dir = install_key_dir / args.key_type_subdir 735 736 os.makedirs(install_key_dir, exist_ok=True) 737 # looses file owner and group - file is commonly accessible 738 shutil.copy2(fw_file_to_copy, install_key_dir) 739 740 741 # sof-info/ directory 742 743 @dataclasses.dataclass 744 class InstFile: 745 'How to install one file' 746 name: pathlib.Path 747 renameTo: pathlib.Path = None 748 # TODO: upgrade this to 3 states: optional/warning/error 749 optional: bool = False 750 gzip: bool = True 751 txt: bool = False 752 753 installed_files = [ 754 # Fail if one of these is missing 755 InstFile(".config", "config", txt=True), 756 InstFile("misc/generated/configs.c", "generated_configs.c", txt=True), 757 InstFile("include/generated/version.h", "zephyr_version.h", 758 gzip=False, txt=True), 759 InstFile("include/generated/sof_versions.h", "sof_versions.h", 760 gzip=False, txt=True), 761 InstFile(BIN_NAME + ".elf"), 762 InstFile(BIN_NAME + ".lst", txt=True), 763 InstFile(BIN_NAME + ".map", txt=True), 764 765 # CONFIG_BUILD_OUTPUT_STRIPPED 766 # Renaming ELF files highlights the workaround below that strips the .comment section 767 InstFile(BIN_NAME + ".strip", renameTo=f"stripped-{BIN_NAME}.elf"), 768 769 # Not every platform has intermediate rimage modules 770 InstFile("main-stripped.mod", renameTo="stripped-main.elf", optional=True), 771 InstFile("boot.mod", optional=True), 772 InstFile("main.mod", optional=True), 773 ] 774 775 # We cannot import at the start because zephyr may not be there yet 776 sys.path.insert(1, os.path.join(sys.path[0], 777 '..', '..', 'zephyr', 'scripts', 'west_commands')) 778 import zcmake 779 780 cmake_cache = zcmake.CMakeCache.from_build_dir(abs_build_dir.parent) 781 objcopy = cmake_cache.get("CMAKE_OBJCOPY") 782 783 sof_info = pathlib.Path(STAGING_DIR) / "sof-info" / platform 784 sof_info.mkdir(parents=True, exist_ok=True) 785 gzip_threads = concurrent.ThreadPoolExecutor() 786 gzip_futures = [] 787 for f in installed_files: 788 if not pathlib.Path(abs_build_dir / f.name).is_file() and f.optional: 789 continue 790 dstname = f.renameTo or f.name 791 792 src = abs_build_dir / f.name 793 dst = sof_info / dstname 794 795 # Some Xtensa compilers (ab?)use the .ident / .comment 796 # section and append the typically absolute and not 797 # reproducible /path/to/the.c file after the usual 798 # compiler ID. 799 # https://sourceware.org/binutils/docs/as/Ident.html 800 # 801 # --strip-all does not remove the .comment section. 802 # Remove it like some gcc test scripts do: 803 # https://gcc.gnu.org/git/?p=gcc.git;a=commit;h=c7046906c3ae 804 if "strip" in str(dstname): 805 execute_command( 806 [str(x) for x in [objcopy, "--remove-section", ".comment", src, dst]], 807 # Some xtensa installs don't have a 808 # XtensaTools/config/default-params symbolic link 809 env=platf_build_environ, 810 ) 811 elif f.txt: 812 dos2unix(src, dst) 813 else: 814 shutil.copy2(src, dst) 815 if f.gzip: 816 gzip_futures.append(gzip_threads.submit(gzip_compress, dst)) 817 for gzip_res in concurrent.as_completed(gzip_futures): 818 gzip_res.result() # throws exception if gzip unexpectedly failed 819 gzip_threads.shutdown() 820 821 822# Zephyr's CONFIG_KERNEL_BIN_NAME default value 823BIN_NAME = 'zephyr' 824 825CHECKSUM_WANTED = [ 826 # Some .ri files have a deterministic signature, others use 827 # a cryptographic salt. Even for the latter a checksum is still 828 # useful to match an artefact with a specific build log. 829 '*.ri', 830 'dsp_basefw.bin', 831 832 '*version*.h', 833 '*configs.c', # deterministic unlike .config 834 '*.strip', '*stripped*', # stripped ELF files are reproducible 835 'boot.mod', # no debug section -> no need to strip this ELF 836 BIN_NAME + '.lst', # objdump --disassemble 837 '*.ldc', 838] 839 840# Prefer CRLF->LF because unlike LF->CRLF it's (normally) idempotent. 841def dos2unix(in_name, out_name): 842 with open(in_name, "rb") as inf: 843 # must read all at once otherwise could fall between CR and LF 844 content = inf.read() 845 assert content 846 with open(out_name, "wb") as outf: 847 outf.write(content.replace(b"\r\n", b"\n")) 848 849def gzip_compress(fname, gzdst=None): 850 gzdst = gzdst or pathlib.Path(f"{fname}.gz") 851 with open(fname, 'rb') as inputf: 852 # mtime=0 for recursive diff convenience 853 with gzip.GzipFile(gzdst, 'wb', mtime=0) as gzf: 854 shutil.copyfileobj(inputf, gzf) 855 os.remove(fname) 856 857 858# As of October 2022, sof_ri_info.py expects .ri files to include a CSE manifest / signature. 859# Don't run sof_ri_info and ignore silently .ri files that don't have one. 860RI_INFO_UNSUPPORTED = [] 861 862RI_INFO_UNSUPPORTED += ['imx8', 'imx8x', 'imx8m'] 863RI_INFO_UNSUPPORTED += ['rn'] 864RI_INFO_UNSUPPORTED += ['mt8186', 'mt8195'] 865 866# sof_ri_info.py has not caught up with the latest rimage yet: these will print a warning. 867RI_INFO_FIXME = ['mtl'] 868 869def reproducible_checksum(platform, ri_file): 870 871 if platform in RI_INFO_FIXME: 872 print(f"FIXME: sof_ri_info does not support '{platform}'") 873 return 874 875 parsed_ri = sof_ri_info.parse_fw_bin(ri_file, False, False) 876 repro_output = ri_file.parent / ("reproducible-" + ri_file.name) 877 chk256 = sof_ri_info.EraseVariables(ri_file, parsed_ri, west_top / repro_output) 878 print('sha256sum {0}\n{1} {0}'.format(repro_output, chk256)) 879 880 881def main(): 882 parse_args() 883 if args.version: 884 print(VERSION) 885 sys.exit(0) 886 check_west_installation() 887 if len(args.platforms) == 0: 888 print("No platform build requested") 889 else: 890 print("Building platforms: {}".format(" ".join(args.platforms))) 891 892 west_init_if_needed() 893 894 if args.update: 895 # Initialize zephyr project with west 896 west_update() 897 create_zephyr_sof_symlink() 898 899 if args.platforms: 900 build_rimage() 901 build_platforms() 902 show_installed_files() 903 904if __name__ == "__main__": 905 main() 906