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