Minos Galanakis | f4ca6ac | 2017-12-11 02:39:21 +0100 | [diff] [blame^] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | """ tfm_builder.py: |
| 4 | |
| 5 | Build wrapping class that builds a specific tfm configuration """ |
| 6 | |
| 7 | from __future__ import print_function |
| 8 | |
| 9 | __copyright__ = """ |
| 10 | /* |
| 11 | * Copyright (c) 2018-2019, Arm Limited. All rights reserved. |
| 12 | * |
| 13 | * SPDX-License-Identifier: BSD-3-Clause |
| 14 | * |
| 15 | */ |
| 16 | """ |
| 17 | __author__ = "Minos Galanakis" |
| 18 | __email__ = "minos.galanakis@linaro.org" |
| 19 | __project__ = "Trusted Firmware-M Open CI" |
| 20 | __status__ = "stable" |
| 21 | __version__ = "1.0" |
| 22 | |
| 23 | import os |
| 24 | from .utils import * |
| 25 | import shutil |
| 26 | from .structured_task import structuredTask |
| 27 | |
| 28 | |
| 29 | class TFM_Builder(structuredTask): |
| 30 | """ Wrap around tfm cmake system and spawn a thread to build the project. |
| 31 | """ |
| 32 | _tfb_build_params = ["TARGET_PLATFORM", |
| 33 | "COMPILER", |
| 34 | "PROJ_CONFIG", |
| 35 | "CMAKE_BUILD_TYPE", |
| 36 | "WITH_MCUBOOT" |
| 37 | ] |
| 38 | |
| 39 | _tfb_build_template = ("cmake -G \"Unix Makefiles\" -DPROJ_CONFIG=`" |
| 40 | "readlink -f %(PROJ_CONFIG)s.cmake` " |
| 41 | "-DTARGET_PLATFORM=%(TARGET_PLATFORM)s " |
| 42 | "-DCOMPILER=%(COMPILER)s " |
| 43 | "-DCMAKE_BUILD_TYPE=%(CMAKE_BUILD_TYPE)s " |
| 44 | "-DBL2=%(WITH_MCUBOOT)s " |
| 45 | "%(TFM_ROOT)s") |
| 46 | |
| 47 | def __init__(self, |
| 48 | name, # Proccess name |
| 49 | tfm_dir, # TFM root directory |
| 50 | work_dir, # Current working directory(ie logs) |
| 51 | cfg_dict, # Input config dictionary of the following form |
| 52 | # input_dict = {"PROJ_CONFIG": "ConfigRegression", |
| 53 | # "TARGET_PLATFORM": "MUSCA_A", |
| 54 | # "COMPILER": "ARMCLANG", |
| 55 | # "CMAKE_BUILD_TYPE": "Debug"} |
| 56 | install=False, # Install library after build |
| 57 | build_threads=4, # Number of CPU thrads used in build |
| 58 | silent=False): # Silence stdout ouptut |
| 59 | |
| 60 | self._tfb_cfg = cfg_dict |
| 61 | self._tfb_build_threads = build_threads |
| 62 | self._tfb_install = install |
| 63 | self._tfb_silent = silent |
| 64 | self._tfb_binaries = [] |
| 65 | |
| 66 | # Required by other methods, always set working directory first |
| 67 | self._tfb_work_dir = os.path.abspath(os.path.expanduser(work_dir)) |
| 68 | |
| 69 | self._tfb_tfm_dir = os.path.abspath(os.path.expanduser(tfm_dir)) |
| 70 | # Entries will be filled after sanity test on cfg_dict dring pre_exec |
| 71 | self._tfb_build_dir = None |
| 72 | self._tfb_log_f = None |
| 73 | super(TFM_Builder, self).__init__(name=name) |
| 74 | |
| 75 | def mute(self): |
| 76 | self._tfb_silent = True |
| 77 | |
| 78 | def log(self): |
| 79 | """ Print and return the contents of log file """ |
| 80 | with open(self._tfb_log_f, "r") as F: |
| 81 | log = F.read() |
| 82 | print(log) |
| 83 | return log |
| 84 | |
| 85 | def report(self): |
| 86 | """Return the report on the job """ |
| 87 | return self.unstash("Build Report") |
| 88 | |
| 89 | def pre_eval(self): |
| 90 | """ Tests that need to be run in set-up state """ |
| 91 | |
| 92 | # Test that all required entries exist in config |
| 93 | diff = list(set(self._tfb_build_params) - set(self._tfb_cfg.keys())) |
| 94 | if diff: |
| 95 | print("Cound't find require build entry: %s in config" % diff) |
| 96 | return False |
| 97 | # TODO check validity of passed config values |
| 98 | # TODO test detection of srec |
| 99 | # self.srec_path = shutil.which("srec_cat") |
| 100 | return True |
| 101 | |
| 102 | def pre_exec(self, eval_ret): |
| 103 | """ Create all required directories, files if they do not exist """ |
| 104 | |
| 105 | self._tfb_build_dir = os.path.join(self._tfb_work_dir, |
| 106 | self.get_name()) |
| 107 | # Ensure we have a clean build directory |
| 108 | shutil.rmtree(self._tfb_build_dir, ignore_errors=True) |
| 109 | |
| 110 | self._tfb_cfg["TFM_ROOT"] = self._tfb_tfm_dir |
| 111 | |
| 112 | # Append the path for the config |
| 113 | self._tfb_cfg["PROJ_CONFIG"] = os.path.join(self._tfb_tfm_dir, |
| 114 | self._tfb_cfg[("PROJ_" |
| 115 | "CONFIG")]) |
| 116 | |
| 117 | # Log will be placed in work directory, named as the build dir |
| 118 | self._tfb_log_f = "%s.log" % self._tfb_build_dir |
| 119 | |
| 120 | # Confirm that the work/build directory exists |
| 121 | for p in [self._tfb_work_dir, self._tfb_build_dir]: |
| 122 | if not os.path.exists(p): |
| 123 | os.makedirs(p) |
| 124 | |
| 125 | # Calcuate a list of expected binaries |
| 126 | binaries = [] |
| 127 | |
| 128 | # If install is asserted pick the iems from the appropriate location |
| 129 | if self._tfb_install: |
| 130 | |
| 131 | fvp_path = os.path.join(self._tfb_build_dir, |
| 132 | "install", "outputs", "fvp") |
| 133 | platform_path = os.path.join(self._tfb_build_dir, |
| 134 | "install", |
| 135 | "outputs", |
| 136 | self._tfb_cfg["TARGET_PLATFORM"]) |
| 137 | |
| 138 | # Generate a list of binaries included in both directories |
| 139 | common_bin_list = ["tfm_%s.%s" % (s, e) for s in ["s", "ns"] |
| 140 | for e in ["bin", "axf"]] |
| 141 | if self._tfb_cfg["WITH_MCUBOOT"]: |
| 142 | common_bin_list += ["mcuboot.%s" % e for e in ["bin", "axf"]] |
| 143 | |
| 144 | # When building with bootloader extra binaries are expected |
| 145 | binaries += [os.path.join(platform_path, b) for b in |
| 146 | ["tfm_sign.bin", |
| 147 | "tfm_full.bin"]] |
| 148 | binaries += [os.path.join(fvp_path, b) for b in |
| 149 | ["tfm_s_ns_concatenated.bin", |
| 150 | "tfm_s_ns_signed.bin"]] |
| 151 | |
| 152 | binaries += [os.path.join(p, b) for p in [fvp_path, platform_path] |
| 153 | for b in common_bin_list] |
| 154 | |
| 155 | # Add Musca required binaries |
| 156 | if self._tfb_cfg["TARGET_PLATFORM"] == "MUSCA_A": |
| 157 | binaries += [os.path.join(platform_path, |
| 158 | "musca_firmware.hex")] |
| 159 | |
| 160 | self._tfb_binaries = binaries |
| 161 | |
| 162 | else: |
| 163 | binaries += [os.path.join(self._tfb_build_dir, "app", "tfm_ns")] |
| 164 | if "ConfigCoreTest" in self._tfb_build_dir: |
| 165 | binaries += [os.path.join(self._tfb_build_dir, |
| 166 | "unit_test", "tfm_s")] |
| 167 | else: |
| 168 | binaries += [os.path.join(self._tfb_build_dir, "app", |
| 169 | "secure_fw", "tfm_s")] |
| 170 | if self._tfb_cfg["WITH_MCUBOOT"]: |
| 171 | binaries += [os.path.join(self._tfb_build_dir, |
| 172 | "bl2", "ext", "mcuboot", "mcuboot")] |
| 173 | |
| 174 | ext = ['.bin', '.axf'] |
| 175 | self._tfb_binaries = ["%s%s" % (n, e) for n in binaries |
| 176 | for e in ext] |
| 177 | |
| 178 | # Add Musca required binaries |
| 179 | if self._tfb_cfg["TARGET_PLATFORM"] == "MUSCA_A": |
| 180 | self._tfb_binaries += [os.path.join(self._tfb_build_dir, |
| 181 | "tfm_sign.bin")] |
| 182 | self._tfb_binaries += [os.path.join(self._tfb_build_dir, |
| 183 | "musca_firmware.hex")] |
| 184 | |
| 185 | def get_binaries(self, |
| 186 | bootl=None, |
| 187 | bin_s=None, |
| 188 | bin_ns=None, |
| 189 | bin_sign=None, |
| 190 | filt=None): |
| 191 | """ Return the absolute location of binaries (from config) |
| 192 | if they exist. Can add a filter parameter which will only |
| 193 | consider entries with /filter/ in their path as a directory """ |
| 194 | ret_boot = None |
| 195 | ret_bin_ns = None |
| 196 | ret_bin_s = None |
| 197 | ret_bin_sign = None |
| 198 | |
| 199 | # Apply filter as a /filter/ string to the binary list |
| 200 | filt = "/" + filt + "/" if filter else None |
| 201 | binaries = list(filter(lambda x: filt in x, self._tfb_binaries)) \ |
| 202 | if filt else self._tfb_binaries |
| 203 | |
| 204 | for obj_file in binaries: |
| 205 | fname = os.path.split(obj_file)[-1] |
| 206 | if bootl: |
| 207 | if fname == bootl: |
| 208 | ret_boot = obj_file |
| 209 | continue |
| 210 | if bin_s: |
| 211 | if fname == bin_s: |
| 212 | ret_bin_s = obj_file |
| 213 | continue |
| 214 | |
| 215 | if bin_ns: |
| 216 | if fname == bin_ns: |
| 217 | ret_bin_ns = obj_file |
| 218 | continue |
| 219 | if bin_sign: |
| 220 | if fname == bin_sign: |
| 221 | ret_bin_sign = obj_file |
| 222 | continue |
| 223 | return [ret_boot, ret_bin_s, ret_bin_ns, ret_bin_sign] |
| 224 | |
| 225 | def task_exec(self): |
| 226 | """ Main tasks """ |
| 227 | |
| 228 | # Mark proccess running as status |
| 229 | self.set_status(-1) |
| 230 | # Go to build directory |
| 231 | os.chdir(self._tfb_build_dir) |
| 232 | # Compile the build commands |
| 233 | cmake_cmd = self._tfb_build_template % self._tfb_cfg |
| 234 | build_cmd = "cmake --build ./ -- -j %s" % self._tfb_build_threads |
| 235 | |
| 236 | # Pass the report to later stages |
| 237 | rep = {"build_cmd": "%s" % build_cmd, |
| 238 | "cmake_cmd": "%s" % cmake_cmd} |
| 239 | self.stash("Build Report", rep) |
| 240 | |
| 241 | # Calll camke to configure the project |
| 242 | if not subprocess_log(cmake_cmd, |
| 243 | self._tfb_log_f, |
| 244 | prefix=cmake_cmd, |
| 245 | silent=self._tfb_silent): |
| 246 | # Build it |
| 247 | if subprocess_log(build_cmd, |
| 248 | self._tfb_log_f, |
| 249 | append=True, |
| 250 | prefix=build_cmd, |
| 251 | silent=self._tfb_silent): |
| 252 | raise Exception("Build Failed please check log: %s" % |
| 253 | self._tfb_log_f) |
| 254 | else: |
| 255 | raise Exception("Cmake Failed please check log: %s" % |
| 256 | self._tfb_log_f) |
| 257 | |
| 258 | if self._tfb_install: |
| 259 | install_cmd = "cmake --build ./ -- -j install" |
| 260 | if subprocess_log(install_cmd, |
| 261 | self._tfb_log_f, |
| 262 | append=True, |
| 263 | prefix=install_cmd, |
| 264 | silent=self._tfb_silent): |
| 265 | raise Exception(("Make install Failed." |
| 266 | " please check log: %s") % self._tfb_log_f) |
| 267 | if self._tfb_cfg["TARGET_PLATFORM"] == "MUSCA_A": |
| 268 | boot_f, s_bin, ns_bin, sns_signed_bin = self.get_binaries( |
| 269 | bootl="mcuboot.bin", |
| 270 | bin_s="tfm_s.bin", |
| 271 | bin_ns="tfm_ns.bin", |
| 272 | bin_sign="tfm_sign.bin", |
| 273 | filt="MUSCA_A") |
| 274 | self.convert_to_hex(boot_f, sns_signed_bin) |
| 275 | self._t_stop() |
| 276 | |
| 277 | def sign_img(self, secure_bin, non_secure_bin): |
| 278 | """Join a secure and non secure image and sign them""" |
| 279 | |
| 280 | imgtool_dir = os.path.join(self._tfb_tfm_dir, |
| 281 | "bl2/ext/mcuboot/scripts/") |
| 282 | flash_layout = os.path.join(self._tfb_tfm_dir, |
| 283 | "platform/ext/target/musca_a/" |
| 284 | "partition/flash_layout.h") |
| 285 | sign_cert = os.path.join(self._tfb_tfm_dir, |
| 286 | "bl2/ext/mcuboot/root-rsa-2048.pem") |
| 287 | sns_unsigned_bin = os.path.join(self._tfb_build_dir, |
| 288 | "sns_unsigned.bin") |
| 289 | sns_signed_bin = os.path.join(self._tfb_build_dir, "sns_signed.bin") |
| 290 | |
| 291 | # Early versions of the tool hard relative imports, run from its dir |
| 292 | os.chdir(imgtool_dir) |
| 293 | assemble_cmd = ("python3 assemble.py -l %(layout)s -s %(s)s " |
| 294 | "-n %(ns)s -o %(sns)s") % {"layout": flash_layout, |
| 295 | "s": secure_bin, |
| 296 | "ns": non_secure_bin, |
| 297 | "sns": sns_unsigned_bin |
| 298 | } |
| 299 | sign_cmd = ("python3 imgtool.py sign -k %(cert)s --align 1 -v " |
| 300 | "1.0 -H 0x400 --pad 0x30000 " |
| 301 | "%(sns)s %(sns_signed)s") % {"cert": sign_cert, |
| 302 | "sns": sns_unsigned_bin, |
| 303 | "sns_signed": sns_signed_bin |
| 304 | } |
| 305 | run_proccess(assemble_cmd) |
| 306 | run_proccess(sign_cmd) |
| 307 | # Return to build directory |
| 308 | os.chdir(self._tfb_build_dir) |
| 309 | return sns_signed_bin |
| 310 | |
| 311 | def convert_to_hex(self, |
| 312 | boot_bin, |
| 313 | sns_signed_bin, |
| 314 | qspi_base=0x200000, |
| 315 | boot_size=0x10000): |
| 316 | """Convert a signed image to an intel hex format with mcuboot """ |
| 317 | if self._tfb_install: |
| 318 | platform_path = os.path.join(self._tfb_build_dir, |
| 319 | "install", |
| 320 | "outputs", |
| 321 | self._tfb_cfg["TARGET_PLATFORM"]) |
| 322 | firmware_hex = os.path.join(platform_path, "musca_firmware.hex") |
| 323 | else: |
| 324 | firmware_hex = os.path.join(self._tfb_build_dir, |
| 325 | "musca_firmware.hex") |
| 326 | |
| 327 | img_offset = qspi_base + boot_size |
| 328 | merge_cmd = ("srec_cat %(boot)s -Binary -offset 0x%(qspi_offset)x " |
| 329 | "%(sns_signed)s -Binary -offset 0x%(img_offset)x " |
| 330 | "-o %(hex)s -Intel") % {"boot": boot_bin, |
| 331 | "sns_signed": sns_signed_bin, |
| 332 | "hex": firmware_hex, |
| 333 | "qspi_offset": qspi_base, |
| 334 | "img_offset": img_offset |
| 335 | } |
| 336 | run_proccess(merge_cmd) |
| 337 | return |
| 338 | |
| 339 | def post_eval(self): |
| 340 | """ Verify that the artefacts exist """ |
| 341 | print("%s Post eval" % self.get_name()) |
| 342 | |
| 343 | ret_eval = False |
| 344 | rep = self.unstash("Build Report") |
| 345 | missing_binaries = list(filter(lambda x: not os.path.isfile(x), |
| 346 | self._tfb_binaries)) |
| 347 | |
| 348 | if len(missing_binaries): |
| 349 | print("ERROR: Could not locate the following binaries:") |
| 350 | print("\n".join(missing_binaries)) |
| 351 | |
| 352 | # Update the artifacts to not include missing ones |
| 353 | artf = [n for n in self._tfb_binaries if n not in missing_binaries] |
| 354 | # TODO update self._tfb_binaries |
| 355 | ret_eval = False |
| 356 | else: |
| 357 | print("SUCCESS: Produced binaries:") |
| 358 | print("\n".join(self._tfb_binaries)) |
| 359 | ret_eval = True |
| 360 | |
| 361 | artf = self._tfb_binaries |
| 362 | |
| 363 | # Add artefact related information to report |
| 364 | rep["log"] = self._tfb_log_f |
| 365 | rep["missing_artefacts"] = missing_binaries |
| 366 | rep["artefacts"] = artf |
| 367 | |
| 368 | rep["status"] = "Success" if ret_eval else "Failed" |
| 369 | self.stash("Build Report", rep) |
| 370 | return ret_eval |
| 371 | |
| 372 | def post_exec(self, eval_ret): |
| 373 | """ """ |
| 374 | |
| 375 | if eval_ret: |
| 376 | print("TFM Builder %s was Successful" % self.get_name()) |
| 377 | else: |
| 378 | print("TFM Builder %s was UnSuccessful" % self.get_name()) |