Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | |
| 3 | # Copyright 2021 Google, Inc. |
| 4 | # |
| 5 | # Licensed under the Apache License, Version 2.0 (the "License"); |
| 6 | # you may not use this file except in compliance with the License. |
| 7 | # You may obtain a copy of the License at: |
| 8 | # |
| 9 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 10 | # |
| 11 | # Unless required by applicable law or agreed to in writing, software |
| 12 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 14 | # See the License for the specific language governing permissions and |
| 15 | # limitations under the License. |
| 16 | """ Build BT targets on the host system. |
| 17 | |
| 18 | For building, you will first have to stage a platform directory that has the |
| 19 | following structure: |
| 20 | |-common-mk |
| 21 | |-bt |
| 22 | |-external |
| 23 | |-|-rust |
| 24 | |-|-|-vendor |
| 25 | |
| 26 | The simplest way to do this is to check out platform2 to another directory (that |
| 27 | is not a subdir of this bt directory), symlink bt there and symlink the rust |
| 28 | vendor repository as well. |
| 29 | """ |
| 30 | import argparse |
| 31 | import multiprocessing |
| 32 | import os |
| 33 | import shutil |
| 34 | import six |
| 35 | import subprocess |
| 36 | import sys |
| 37 | |
| 38 | # Use flags required by common-mk (find -type f | grep -nE 'use[.]' {}) |
| 39 | COMMON_MK_USES = [ |
| 40 | 'asan', |
| 41 | 'coverage', |
| 42 | 'cros_host', |
| 43 | 'fuzzer', |
| 44 | 'fuzzer', |
| 45 | 'msan', |
| 46 | 'profiling', |
| 47 | 'tcmalloc', |
| 48 | 'test', |
| 49 | 'ubsan', |
| 50 | ] |
| 51 | |
| 52 | # Default use flags. |
| 53 | USE_DEFAULTS = { |
| 54 | 'android': False, |
| 55 | 'bt_nonstandard_codecs': False, |
| 56 | 'test': False, |
| 57 | } |
| 58 | |
| 59 | VALID_TARGETS = [ |
| 60 | 'prepare', # Prepare the output directory (gn gen + rust setup) |
| 61 | 'tools', # Build the host tools (i.e. packetgen) |
| 62 | 'rust', # Build only the rust components + copy artifacts to output dir |
| 63 | 'main', # Build the main C++ codebase |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 64 | 'test', # Run the unit tests |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 65 | 'clean', # Clean up output directory |
| 66 | 'all', # All targets except test and clean |
| 67 | ] |
| 68 | |
Abhishek Pandit-Subedi | 70e4304 | 2021-06-10 21:16:52 +0000 | [diff] [blame^] | 69 | # TODO(b/190750167) - Host tests are disabled until we are full bazel build |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 70 | HOST_TESTS = [ |
Abhishek Pandit-Subedi | 70e4304 | 2021-06-10 21:16:52 +0000 | [diff] [blame^] | 71 | # 'bluetooth_test_common', |
| 72 | # 'bluetoothtbd_test', |
| 73 | # 'net_test_avrcp', |
| 74 | # 'net_test_btcore', |
| 75 | # 'net_test_types', |
| 76 | # 'net_test_btm_iso', |
| 77 | # 'net_test_btpackets', |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 78 | ] |
| 79 | |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 80 | |
| 81 | class UseFlags(): |
| 82 | |
| 83 | def __init__(self, use_flags): |
| 84 | """ Construct the use flags. |
| 85 | |
| 86 | Args: |
| 87 | use_flags: List of use flags parsed from the command. |
| 88 | """ |
| 89 | self.flags = {} |
| 90 | |
| 91 | # Import use flags required by common-mk |
| 92 | for use in COMMON_MK_USES: |
| 93 | self.set_flag(use, False) |
| 94 | |
| 95 | # Set our defaults |
| 96 | for use, value in USE_DEFAULTS.items(): |
| 97 | self.set_flag(use, value) |
| 98 | |
| 99 | # Set use flags - value is set to True unless the use starts with - |
| 100 | # All given use flags always override the defaults |
| 101 | for use in use_flags: |
| 102 | value = not use.startswith('-') |
| 103 | self.set_flag(use, value) |
| 104 | |
| 105 | def set_flag(self, key, value=True): |
| 106 | setattr(self, key, value) |
| 107 | self.flags[key] = value |
| 108 | |
| 109 | |
| 110 | class HostBuild(): |
| 111 | |
| 112 | def __init__(self, args): |
| 113 | """ Construct the builder. |
| 114 | |
| 115 | Args: |
| 116 | args: Parsed arguments from ArgumentParser |
| 117 | """ |
| 118 | self.args = args |
| 119 | |
| 120 | # Set jobs to number of cpus unless explicitly set |
| 121 | self.jobs = self.args.jobs |
| 122 | if not self.jobs: |
| 123 | self.jobs = multiprocessing.cpu_count() |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 124 | print("Number of jobs = {}".format(self.jobs)) |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 125 | |
| 126 | # Normalize all directories |
| 127 | self.output_dir = os.path.abspath(self.args.output) |
| 128 | self.platform_dir = os.path.abspath(self.args.platform_dir) |
| 129 | self.sysroot = self.args.sysroot |
| 130 | self.use_board = os.path.abspath(self.args.use_board) if self.args.use_board else None |
| 131 | self.libdir = self.args.libdir |
| 132 | |
| 133 | # If default target isn't set, build everything |
| 134 | self.target = 'all' |
| 135 | if hasattr(self.args, 'target') and self.args.target: |
| 136 | self.target = self.args.target |
| 137 | |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 138 | target_use = self.args.use if self.args.use else [] |
| 139 | |
| 140 | # Unless set, always build test code |
| 141 | if not self.args.notest: |
| 142 | target_use.append('test') |
| 143 | |
| 144 | self.use = UseFlags(target_use) |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 145 | |
| 146 | # Validate platform directory |
| 147 | assert os.path.isdir(self.platform_dir), 'Platform dir does not exist' |
| 148 | assert os.path.isfile(os.path.join(self.platform_dir, '.gn')), 'Platform dir does not have .gn at root' |
| 149 | |
| 150 | # Make sure output directory exists (or create it) |
| 151 | os.makedirs(self.output_dir, exist_ok=True) |
| 152 | |
| 153 | # Set some default attributes |
| 154 | self.libbase_ver = None |
| 155 | |
| 156 | self.configure_environ() |
| 157 | |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 158 | def _generate_rustflags(self): |
| 159 | """ Rustflags to include for the build. |
| 160 | """ |
| 161 | rust_flags = [ |
| 162 | '-L', |
| 163 | '{}/out/Default/'.format(self.output_dir), |
| 164 | '-C', |
| 165 | 'link-arg=-Wl,--allow-multiple-definition', |
| 166 | ] |
| 167 | |
| 168 | return ' '.join(rust_flags) |
| 169 | |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 170 | def configure_environ(self): |
| 171 | """ Configure environment variables for GN and Cargo. |
| 172 | """ |
| 173 | self.env = os.environ.copy() |
| 174 | |
| 175 | # Make sure cargo home dir exists and has a bin directory |
| 176 | cargo_home = os.path.join(self.output_dir, 'cargo_home') |
| 177 | os.makedirs(cargo_home, exist_ok=True) |
| 178 | os.makedirs(os.path.join(cargo_home, 'bin'), exist_ok=True) |
| 179 | |
| 180 | # Configure Rust env variables |
| 181 | self.env['CARGO_TARGET_DIR'] = self.output_dir |
| 182 | self.env['CARGO_HOME'] = os.path.join(self.output_dir, 'cargo_home') |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 183 | self.env['RUSTFLAGS'] = self._generate_rustflags() |
Abhishek Pandit-Subedi | 1927afa | 2021-04-28 21:16:18 -0700 | [diff] [blame] | 184 | self.env['CXX_ROOT_PATH'] = os.path.join(self.platform_dir, 'bt') |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 185 | |
| 186 | # Configure some GN variables |
| 187 | if self.use_board: |
| 188 | self.env['PKG_CONFIG_PATH'] = os.path.join(self.use_board, self.libdir, 'pkgconfig') |
| 189 | libdir = os.path.join(self.use_board, self.libdir) |
| 190 | if self.env.get('LIBRARY_PATH'): |
| 191 | libpath = self.env['LIBRARY_PATH'] |
| 192 | self.env['LIBRARY_PATH'] = '{}:{}'.format(libdir, libpath) |
| 193 | else: |
| 194 | self.env['LIBRARY_PATH'] = libdir |
| 195 | |
| 196 | def run_command(self, target, args, cwd=None, env=None): |
| 197 | """ Run command and stream the output. |
| 198 | """ |
| 199 | # Set some defaults |
| 200 | if not cwd: |
| 201 | cwd = self.platform_dir |
| 202 | if not env: |
| 203 | env = self.env |
| 204 | |
| 205 | log_file = os.path.join(self.output_dir, '{}.log'.format(target)) |
| 206 | with open(log_file, 'wb') as lf: |
| 207 | rc = 0 |
| 208 | process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE) |
| 209 | while True: |
| 210 | line = process.stdout.readline() |
| 211 | print(line.decode('utf-8'), end="") |
| 212 | lf.write(line) |
| 213 | if not line: |
| 214 | rc = process.poll() |
| 215 | if rc is not None: |
| 216 | break |
| 217 | |
| 218 | time.sleep(0.1) |
| 219 | |
| 220 | if rc != 0: |
| 221 | raise Exception("Return code is {}".format(rc)) |
| 222 | |
| 223 | def _get_basever(self): |
| 224 | if self.libbase_ver: |
| 225 | return self.libbase_ver |
| 226 | |
| 227 | self.libbase_ver = os.environ.get('BASE_VER', '') |
| 228 | if not self.libbase_ver: |
| 229 | base_file = os.path.join(self.sysroot, 'usr/share/libchrome/BASE_VER') |
| 230 | try: |
| 231 | with open(base_file, 'r') as f: |
| 232 | self.libbase_ver = f.read().strip('\n') |
| 233 | except: |
| 234 | self.libbase_ver = 'NOT-INSTALLED' |
| 235 | |
| 236 | return self.libbase_ver |
| 237 | |
| 238 | def _gn_default_output(self): |
| 239 | return os.path.join(self.output_dir, 'out/Default') |
| 240 | |
| 241 | def _gn_configure(self): |
| 242 | """ Configure all required parameters for platform2. |
| 243 | |
| 244 | Mostly copied from //common-mk/platform2.py |
| 245 | """ |
| 246 | clang = self.args.clang |
| 247 | |
| 248 | def to_gn_string(s): |
| 249 | return '"%s"' % s.replace('"', '\\"') |
| 250 | |
| 251 | def to_gn_list(strs): |
| 252 | return '[%s]' % ','.join([to_gn_string(s) for s in strs]) |
| 253 | |
| 254 | def to_gn_args_args(gn_args): |
| 255 | for k, v in gn_args.items(): |
| 256 | if isinstance(v, bool): |
| 257 | v = str(v).lower() |
| 258 | elif isinstance(v, list): |
| 259 | v = to_gn_list(v) |
| 260 | elif isinstance(v, six.string_types): |
| 261 | v = to_gn_string(v) |
| 262 | else: |
| 263 | raise AssertionError('Unexpected %s, %r=%r' % (type(v), k, v)) |
| 264 | yield '%s=%s' % (k.replace('-', '_'), v) |
| 265 | |
| 266 | gn_args = { |
| 267 | 'platform_subdir': 'bt', |
| 268 | 'cc': 'clang' if clang else 'gcc', |
| 269 | 'cxx': 'clang++' if clang else 'g++', |
| 270 | 'ar': 'llvm-ar' if clang else 'ar', |
| 271 | 'pkg-config': 'pkg-config', |
| 272 | 'clang_cc': clang, |
| 273 | 'clang_cxx': clang, |
| 274 | 'OS': 'linux', |
| 275 | 'sysroot': self.sysroot, |
| 276 | 'libdir': os.path.join(self.sysroot, self.libdir), |
| 277 | 'build_root': self.output_dir, |
| 278 | 'platform2_root': self.platform_dir, |
| 279 | 'libbase_ver': self._get_basever(), |
| 280 | 'enable_exceptions': os.environ.get('CXXEXCEPTIONS', 0) == '1', |
| 281 | 'external_cflags': [], |
| 282 | 'external_cxxflags': [], |
| 283 | 'enable_werror': False, |
| 284 | } |
| 285 | |
| 286 | if clang: |
| 287 | # Make sure to mark the clang use flag as true |
| 288 | self.use.set_flag('clang', True) |
| 289 | gn_args['external_cxxflags'] += ['-I/usr/include/'] |
| 290 | |
| 291 | # EXTREME HACK ALERT |
| 292 | # |
| 293 | # In my laziness, I am supporting building against an already built |
| 294 | # sysroot path (i.e. chromeos board) so that I don't have to build |
| 295 | # libchrome or modp_b64 locally. |
| 296 | if self.use_board: |
| 297 | includedir = os.path.join(self.use_board, 'usr/include') |
| 298 | gn_args['external_cxxflags'] += [ |
| 299 | '-I{}'.format(includedir), |
| 300 | '-I{}/libchrome'.format(includedir), |
| 301 | '-I{}/gtest'.format(includedir), |
| 302 | '-I{}/gmock'.format(includedir), |
| 303 | '-I{}/modp_b64'.format(includedir), |
| 304 | ] |
| 305 | gn_args_args = list(to_gn_args_args(gn_args)) |
| 306 | use_args = ['%s=%s' % (k, str(v).lower()) for k, v in self.use.flags.items()] |
| 307 | gn_args_args += ['use={%s}' % (' '.join(use_args))] |
| 308 | |
| 309 | gn_args = [ |
| 310 | 'gn', |
| 311 | 'gen', |
| 312 | ] |
| 313 | |
| 314 | if self.args.verbose: |
| 315 | gn_args.append('-v') |
| 316 | |
| 317 | gn_args += [ |
| 318 | '--root=%s' % self.platform_dir, |
| 319 | '--args=%s' % ' '.join(gn_args_args), |
| 320 | self._gn_default_output(), |
| 321 | ] |
| 322 | |
Sonny Sasaka | 706ec3b | 2021-03-25 05:39:20 -0700 | [diff] [blame] | 323 | if 'PKG_CONFIG_PATH' in self.env: |
| 324 | print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH']) |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 325 | |
| 326 | self.run_command('configure', gn_args) |
| 327 | |
| 328 | def _gn_build(self, target): |
| 329 | """ Generate the ninja command for the target and run it. |
| 330 | """ |
| 331 | args = ['%s:%s' % ('bt', target)] |
| 332 | ninja_args = ['ninja', '-C', self._gn_default_output()] |
| 333 | if self.jobs: |
| 334 | ninja_args += ['-j', str(self.jobs)] |
| 335 | ninja_args += args |
| 336 | |
| 337 | if self.args.verbose: |
| 338 | ninja_args.append('-v') |
| 339 | |
| 340 | self.run_command('build', ninja_args) |
| 341 | |
| 342 | def _rust_configure(self): |
| 343 | """ Generate config file at cargo_home so we use vendored crates. |
| 344 | """ |
| 345 | template = """ |
| 346 | [source.systembt] |
| 347 | directory = "{}/external/rust/vendor" |
| 348 | |
| 349 | [source.crates-io] |
| 350 | replace-with = "systembt" |
| 351 | local-registry = "/nonexistent" |
| 352 | """ |
Sonny Sasaka | c1335a2 | 2021-03-25 07:10:47 -0700 | [diff] [blame] | 353 | |
| 354 | if self.args.vendored_rust: |
| 355 | contents = template.format(self.platform_dir) |
| 356 | with open(os.path.join(self.env['CARGO_HOME'], 'config'), 'w') as f: |
| 357 | f.write(contents) |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 358 | |
| 359 | def _rust_build(self): |
| 360 | """ Run `cargo build` from platform2/bt directory. |
| 361 | """ |
| 362 | self.run_command('rust', ['cargo', 'build'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env) |
| 363 | |
| 364 | def _target_prepare(self): |
| 365 | """ Target to prepare the output directory for building. |
| 366 | |
| 367 | This runs gn gen to generate all rquired files and set up the Rust |
| 368 | config properly. This will be run |
| 369 | """ |
| 370 | self._gn_configure() |
| 371 | self._rust_configure() |
| 372 | |
| 373 | def _target_tools(self): |
| 374 | """ Build the tools target in an already prepared environment. |
| 375 | """ |
| 376 | self._gn_build('tools') |
| 377 | |
| 378 | # Also copy bluetooth_packetgen to CARGO_HOME so it's available |
| 379 | shutil.copy( |
| 380 | os.path.join(self._gn_default_output(), 'bluetooth_packetgen'), os.path.join(self.env['CARGO_HOME'], 'bin')) |
| 381 | |
| 382 | def _target_rust(self): |
| 383 | """ Build rust artifacts in an already prepared environment. |
| 384 | """ |
| 385 | self._rust_build() |
Sonny Sasaka | c1335a2 | 2021-03-25 07:10:47 -0700 | [diff] [blame] | 386 | rust_dir = os.path.join(self._gn_default_output(), 'rust') |
| 387 | if os.path.exists(rust_dir): |
| 388 | shutil.rmtree(rust_dir) |
| 389 | shutil.copytree(os.path.join(self.output_dir, 'debug'), rust_dir) |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 390 | |
| 391 | def _target_main(self): |
| 392 | """ Build the main GN artifacts in an already prepared environment. |
| 393 | """ |
| 394 | self._gn_build('all') |
| 395 | |
| 396 | def _target_test(self): |
| 397 | """ Runs the host tests. |
| 398 | """ |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 399 | # Rust tests first |
| 400 | self.run_command('test', ['cargo', 'test'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env) |
| 401 | |
| 402 | # Host tests second based on host test list |
| 403 | for t in HOST_TESTS: |
| 404 | self.run_command( |
| 405 | 'test', [os.path.join(self.output_dir, 'out/Default', t)], |
| 406 | cwd=os.path.join(self.output_dir), |
| 407 | env=self.env) |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 408 | |
| 409 | def _target_clean(self): |
| 410 | """ Delete the output directory entirely. |
| 411 | """ |
| 412 | shutil.rmtree(self.output_dir) |
| 413 | |
| 414 | def _target_all(self): |
| 415 | """ Build all common targets (skipping test and clean). |
| 416 | """ |
| 417 | self._target_prepare() |
| 418 | self._target_tools() |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 419 | self._target_main() |
Abhishek Pandit-Subedi | a7b57b7 | 2021-04-01 15:33:05 -0700 | [diff] [blame] | 420 | self._target_rust() |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 421 | |
| 422 | def build(self): |
| 423 | """ Builds according to self.target |
| 424 | """ |
| 425 | print('Building target ', self.target) |
| 426 | |
| 427 | if self.target == 'prepare': |
| 428 | self._target_prepare() |
| 429 | elif self.target == 'tools': |
| 430 | self._target_tools() |
| 431 | elif self.target == 'rust': |
| 432 | self._target_rust() |
| 433 | elif self.target == 'main': |
| 434 | self._target_main() |
| 435 | elif self.target == 'test': |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 436 | self._target_test() |
| 437 | elif self.target == 'clean': |
| 438 | self._target_clean() |
| 439 | elif self.target == 'all': |
| 440 | self._target_all() |
| 441 | |
| 442 | |
| 443 | if __name__ == '__main__': |
| 444 | parser = argparse.ArgumentParser(description='Simple build for host.') |
| 445 | parser.add_argument('--output', help='Output directory for the build.', required=True) |
| 446 | parser.add_argument('--platform-dir', help='Directory where platform2 is staged.', required=True) |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 447 | parser.add_argument('--clang', help='Use clang compiler.', default=False, action='store_true') |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 448 | parser.add_argument('--use', help='Set a specific use flag.') |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 449 | parser.add_argument('--notest', help="Don't compile test code.", default=False, action='store_true') |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 450 | parser.add_argument('--target', help='Run specific build target') |
| 451 | parser.add_argument('--sysroot', help='Set a specific sysroot path', default='/') |
| 452 | parser.add_argument('--libdir', help='Libdir - default = usr/lib64', default='usr/lib64') |
| 453 | parser.add_argument('--use-board', help='Use a built x86 board for dependencies. Provide path.') |
| 454 | parser.add_argument('--jobs', help='Number of jobs to run', default=0, type=int) |
Abhishek Pandit-Subedi | d801b12 | 2021-04-10 00:41:07 +0000 | [diff] [blame] | 455 | parser.add_argument('--vendored-rust', help='Use vendored rust crates', default=False, action='store_true') |
Abhishek Pandit-Subedi | f52cf66 | 2021-03-02 22:33:25 +0000 | [diff] [blame] | 456 | parser.add_argument('--verbose', help='Verbose logs for build.') |
| 457 | |
| 458 | args = parser.parse_args() |
| 459 | build = HostBuild(args) |
| 460 | build.build() |