blob: fb209ef83ac86494fb9bf9bb4e0697e7951ed4b8 [file] [log] [blame]
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +00001#!/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
18For building, you will first have to stage a platform directory that has the
19following structure:
20|-common-mk
21|-bt
22|-external
23|-|-rust
24|-|-|-vendor
25
26The simplest way to do this is to check out platform2 to another directory (that
27is not a subdir of this bt directory), symlink bt there and symlink the rust
28vendor repository as well.
29"""
30import argparse
31import multiprocessing
32import os
33import shutil
34import six
35import subprocess
36import sys
37
38# Use flags required by common-mk (find -type f | grep -nE 'use[.]' {})
39COMMON_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.
53USE_DEFAULTS = {
54 'android': False,
55 'bt_nonstandard_codecs': False,
56 'test': False,
57}
58
59VALID_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-Subedid801b122021-04-10 00:41:07 +000064 'test', # Run the unit tests
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000065 'clean', # Clean up output directory
66 'all', # All targets except test and clean
67]
68
Abhishek Pandit-Subedi70e43042021-06-10 21:16:52 +000069# TODO(b/190750167) - Host tests are disabled until we are full bazel build
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +000070HOST_TESTS = [
Abhishek Pandit-Subedi70e43042021-06-10 21:16:52 +000071 # '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-Subedid801b122021-04-10 00:41:07 +000078]
79
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000080
81class 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
110class 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-Subedid801b122021-04-10 00:41:07 +0000124 print("Number of jobs = {}".format(self.jobs))
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000125
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-Subedid801b122021-04-10 00:41:07 +0000138 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-Subedif52cf662021-03-02 22:33:25 +0000145
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-Subedid801b122021-04-10 00:41:07 +0000158 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-Subedif52cf662021-03-02 22:33:25 +0000170 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-Subedid801b122021-04-10 00:41:07 +0000183 self.env['RUSTFLAGS'] = self._generate_rustflags()
Abhishek Pandit-Subedi1927afa2021-04-28 21:16:18 -0700184 self.env['CXX_ROOT_PATH'] = os.path.join(self.platform_dir, 'bt')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000185
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 Sasaka706ec3b2021-03-25 05:39:20 -0700323 if 'PKG_CONFIG_PATH' in self.env:
324 print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH'])
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000325
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 Sasakac1335a22021-03-25 07:10:47 -0700353
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-Subedif52cf662021-03-02 22:33:25 +0000358
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 Sasakac1335a22021-03-25 07:10:47 -0700386 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-Subedif52cf662021-03-02 22:33:25 +0000390
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-Subedid801b122021-04-10 00:41:07 +0000399 # 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-Subedif52cf662021-03-02 22:33:25 +0000408
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-Subedif52cf662021-03-02 22:33:25 +0000419 self._target_main()
Abhishek Pandit-Subedia7b57b72021-04-01 15:33:05 -0700420 self._target_rust()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000421
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-Subedif52cf662021-03-02 22:33:25 +0000436 self._target_test()
437 elif self.target == 'clean':
438 self._target_clean()
439 elif self.target == 'all':
440 self._target_all()
441
442
443if __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-Subedid801b122021-04-10 00:41:07 +0000447 parser.add_argument('--clang', help='Use clang compiler.', default=False, action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000448 parser.add_argument('--use', help='Set a specific use flag.')
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000449 parser.add_argument('--notest', help="Don't compile test code.", default=False, action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000450 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-Subedid801b122021-04-10 00:41:07 +0000455 parser.add_argument('--vendored-rust', help='Use vendored rust crates', default=False, action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000456 parser.add_argument('--verbose', help='Verbose logs for build.')
457
458 args = parser.parse_args()
459 build = HostBuild(args)
460 build.build()