blob: dcf75de8ef97caabf8af70f1234f655c28bb3516 [file] [log] [blame]
Minos Galanakisf4ca6ac2017-12-11 02:39:21 +01001#!/usr/bin/env python3
2
3""" tfm_build_manager.py:
4
5 Controlling class managing multiple build configruations for tfm """
6
7from __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
23import os
24import sys
25from pprint import pprint
26from copy import deepcopy
27from .utils import gen_cfg_combinations, list_chunks, load_json,\
28 save_json, print_test
29from .structured_task import structuredTask
30from .tfm_builder import TFM_Builder
31
32
33class TFM_Build_Manager(structuredTask):
34 """ Class that will load a configuration out of a json file, schedule
35 the builds, and produce a report """
36
37 def __init__(self,
38 tfm_dir, # TFM root directory
39 work_dir, # Current working directory(ie logs)
40 cfg_dict, # Input config dictionary of the following form
41 # input_dict = {"PROJ_CONFIG": "ConfigRegression",
42 # "TARGET_PLATFORM": "MUSCA_A",
43 # "COMPILER": "ARMCLANG",
44 # "CMAKE_BUILD_TYPE": "Debug"}
45 report=None, # File to produce report
46 parallel_builds=3, # Number of builds to run in parallel
47 build_threads=4, # Number of threads used per build
48 markdown=True, # Create markdown report
49 html=True, # Create html report
50 ret_code=True, # Set ret_code of script if build failed
51 install=False): # Install libraries after build
52
53 self._tbm_build_threads = build_threads
54 self._tbm_conc_builds = parallel_builds
55 self._tbm_install = install
56 self._tbm_markdown = markdown
57 self._tbm_html = html
58 self._tbm_ret_code = ret_code
59
60 # Required by other methods, always set working directory first
61 self._tbm_work_dir = os.path.abspath(os.path.expanduser(work_dir))
62
63 self._tbm_tfm_dir = os.path.abspath(os.path.expanduser(tfm_dir))
64
65 # Entries will be filled after sanity test on cfg_dict dring pre_exec
66 self._tbm_build_dir = None
67 self._tbm_report = report
68
69 # TODO move them to pre_eval
70 self._tbm_cfg = self.load_config(cfg_dict, self._tbm_work_dir)
71 self._tbm_build_cfg_list = self.parse_config(self._tbm_cfg)
72
73 super(TFM_Build_Manager, self).__init__(name="TFM_Build_Manager")
74
75 def pre_eval(self):
76 """ Tests that need to be run in set-up state """
77 return True
78
79 def pre_exec(self, eval_ret):
80 """ """
81
82 def task_exec(self):
83 """ Create a build pool and execute them in parallel """
84
85 build_pool = []
86 for i in self._tbm_build_cfg_list:
87
88 name = "%s_%s_%s_%s_%s" % (i.TARGET_PLATFORM,
89 i.COMPILER,
90 i.PROJ_CONFIG,
91 i.CMAKE_BUILD_TYPE,
92 "BL2" if i.WITH_MCUBOOT else "NOBL2")
93 print("Loading config %s" % name)
94 build_pool.append(TFM_Builder(name,
95 self._tbm_tfm_dir,
96 self._tbm_work_dir,
97 dict(i._asdict()),
98 self._tbm_install,
99 self._tbm_build_threads))
100
101 status_rep = {}
102 full_rep = {}
103 print("Build: Running %d parallel build jobs" % self._tbm_conc_builds)
104 for build_pool_slice in list_chunks(build_pool, self._tbm_conc_builds):
105
106 # Start the builds
107 for build in build_pool_slice:
108 # Only produce output for the first build
109 if build_pool_slice.index(build) != 0:
110 build.mute()
111 print("Build: Starting %s" % build.get_name())
112 build.start()
113
114 # Wait for the builds to complete
115 for build in build_pool_slice:
116 # Wait for build to finish
117 build.join()
118 # Similarly print the logs of the other builds as they complete
119 if build_pool_slice.index(build) != 0:
120 build.log()
121 print("Build: Finished %s" % build.get_name())
122
123 # Store status in report
124 status_rep[build.get_name()] = build.get_status()
125 full_rep[build.get_name()] = build.report()
126 # Store the report
127 self.stash("Build Status", status_rep)
128 self.stash("Build Report", full_rep)
129
130 if self._tbm_report:
131 print("Exported build report to file:", self._tbm_report)
132 save_json(self._tbm_report, full_rep)
133
134 def post_eval(self):
135 """ If a single build failed fail the test """
136 try:
137 retcode_sum = sum(self.unstash("Build Status").values())
138 if retcode_sum != 0:
139 raise Exception()
140 return True
141 except Exception as e:
142 return False
143
144 def post_exec(self, eval_ret):
145 """ Generate a report and fail the script if build == unsuccessfull"""
146
147 self.print_summary()
148 if not eval_ret:
149 print("ERROR: ====> Build Failed! %s" % self.get_name())
150 self.set_status(1)
151 else:
152 print("SUCCESS: ====> Build Complete!")
153 self.set_status(0)
154
155 def get_report(self):
156 """ Expose the internal report to a new object for external classes """
157 return deepcopy(self.unstash("Build Report"))
158
159 def print_summary(self):
160 """ Print an comprehensive list of the build jobs with their status """
161
162 full_rep = self.unstash("Build Report")
163
164 # Filter out build jobs based on status
165 fl = ([k for k, v in full_rep.items() if v['status'] == 'Failed'])
166 ps = ([k for k, v in full_rep.items() if v['status'] == 'Success'])
167
168 print_test(t_list=fl, status="failed", tname="Builds")
169 print_test(t_list=ps, status="passed", tname="Builds")
170
171 def gen_cfg_comb(self, platform_l, compiler_l, config_l, build_l, boot_l):
172 """ Generate all possible configuration combinations from a group of
173 lists of compiler options"""
174 return gen_cfg_combinations("TFM_Build_CFG",
175 ("TARGET_PLATFORM COMPILER PROJ_CONFIG"
176 " CMAKE_BUILD_TYPE WITH_MCUBOOT"),
177 platform_l,
178 compiler_l,
179 config_l,
180 build_l,
181 boot_l)
182
183 def load_config(self, config, work_dir):
184 try:
185 # passing config_name param supersseeds fileparam
186 if isinstance(config, dict):
187 ret_cfg = deepcopy(config)
188 elif isinstance(config, str):
189 # If the string does not descrive a file try to look for it in
190 # work directory
191 if not os.path.isfile(config):
192 # remove path from file
193 config_2 = os.path.split(config)[-1]
194 # look in the current working directory
195 config_2 = os.path.join(work_dir, config_2)
196 if not os.path.isfile(config_2):
197 m = "Could not find cfg in %s or %s " % (config,
198 config_2)
199 raise Exception(m)
200 # If fille exists in working directory
201 else:
202 config = config_2
203 ret_cfg = load_json(config)
204
205 else:
206 raise Exception("Need to provide a valid config name or file."
207 "Please use --config/--config-file parameter.")
208 except Exception as e:
209 print("Error:%s \nCould not load a valid config" % e)
210 sys.exit(1)
211
212 pprint(ret_cfg)
213 return ret_cfg
214
215 def parse_config(self, cfg):
216 """ Parse a valid configuration file into a set of build dicts """
217
218 # Generate a list of all possible confugration combinations
219 full_cfg = self.gen_cfg_comb(cfg["platform"],
220 cfg["compiler"],
221 cfg["config"],
222 cfg["build"],
223 cfg["with_mcuboot"])
224
225 # Generate a list of all invalid combinations
226 rejection_cfg = []
227
228 for k in cfg["invalid"]:
229 # Pad the omitted values with wildcard char *
230 res_list = list(k) + ["*"] * (5 - len(k))
231
232 print("Working on rejection input: %s" % (res_list))
233
234 # Key order matters. Use index to retrieve default values When
235 # wildcard * char is present
236 _cfg_keys = ["platform",
237 "compiler",
238 "config",
239 "build",
240 "with_mcuboot"]
241
242 # Replace wildcard ( "*") entries with every inluded in cfg variant
243 for n in range(len(res_list)):
244 res_list[n] = [res_list[n]] if res_list[n] != "*" \
245 else cfg[_cfg_keys[n]]
246
247 rejection_cfg += self.gen_cfg_comb(*res_list)
248
249 # Notfy the user for the rejected configuations
250 for i in rejection_cfg:
251
252 name = "%s_%s_%s_%s_%s" % (i.TARGET_PLATFORM,
253 i.COMPILER,
254 i.PROJ_CONFIG,
255 i.CMAKE_BUILD_TYPE,
256 "BL2" if i.WITH_MCUBOOT else "NOBL2")
257 print("Rejecting config %s" % name)
258
259 # Subtract the two lists and convert to dictionary
260 return list(set(full_cfg) - set(rejection_cfg))