blob: 808c80891d5b0715d54d3a2e1330a4a230051b4d [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-Subedid801b122021-04-10 00:41:07 +000069HOST_TESTS = [
70 'bluetooth_test_common',
71 'bluetoothtbd_test',
72 'net_test_avrcp',
73 'net_test_btcore',
74 'net_test_types',
75 'net_test_btm_iso',
76 'net_test_btpackets',
77]
78
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000079
80class UseFlags():
81
82 def __init__(self, use_flags):
83 """ Construct the use flags.
84
85 Args:
86 use_flags: List of use flags parsed from the command.
87 """
88 self.flags = {}
89
90 # Import use flags required by common-mk
91 for use in COMMON_MK_USES:
92 self.set_flag(use, False)
93
94 # Set our defaults
95 for use, value in USE_DEFAULTS.items():
96 self.set_flag(use, value)
97
98 # Set use flags - value is set to True unless the use starts with -
99 # All given use flags always override the defaults
100 for use in use_flags:
101 value = not use.startswith('-')
102 self.set_flag(use, value)
103
104 def set_flag(self, key, value=True):
105 setattr(self, key, value)
106 self.flags[key] = value
107
108
109class HostBuild():
110
111 def __init__(self, args):
112 """ Construct the builder.
113
114 Args:
115 args: Parsed arguments from ArgumentParser
116 """
117 self.args = args
118
119 # Set jobs to number of cpus unless explicitly set
120 self.jobs = self.args.jobs
121 if not self.jobs:
122 self.jobs = multiprocessing.cpu_count()
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000123 print("Number of jobs = {}".format(self.jobs))
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000124
125 # Normalize all directories
126 self.output_dir = os.path.abspath(self.args.output)
127 self.platform_dir = os.path.abspath(self.args.platform_dir)
128 self.sysroot = self.args.sysroot
129 self.use_board = os.path.abspath(self.args.use_board) if self.args.use_board else None
130 self.libdir = self.args.libdir
131
132 # If default target isn't set, build everything
133 self.target = 'all'
134 if hasattr(self.args, 'target') and self.args.target:
135 self.target = self.args.target
136
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000137 target_use = self.args.use if self.args.use else []
138
139 # Unless set, always build test code
140 if not self.args.notest:
141 target_use.append('test')
142
143 self.use = UseFlags(target_use)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000144
145 # Validate platform directory
146 assert os.path.isdir(self.platform_dir), 'Platform dir does not exist'
147 assert os.path.isfile(os.path.join(self.platform_dir, '.gn')), 'Platform dir does not have .gn at root'
148
149 # Make sure output directory exists (or create it)
150 os.makedirs(self.output_dir, exist_ok=True)
151
152 # Set some default attributes
153 self.libbase_ver = None
154
155 self.configure_environ()
156
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000157 def _generate_rustflags(self):
158 """ Rustflags to include for the build.
159 """
160 rust_flags = [
161 '-L',
162 '{}/out/Default/'.format(self.output_dir),
163 '-C',
164 'link-arg=-Wl,--allow-multiple-definition',
165 ]
166
167 return ' '.join(rust_flags)
168
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000169 def configure_environ(self):
170 """ Configure environment variables for GN and Cargo.
171 """
172 self.env = os.environ.copy()
173
174 # Make sure cargo home dir exists and has a bin directory
175 cargo_home = os.path.join(self.output_dir, 'cargo_home')
176 os.makedirs(cargo_home, exist_ok=True)
177 os.makedirs(os.path.join(cargo_home, 'bin'), exist_ok=True)
178
179 # Configure Rust env variables
180 self.env['CARGO_TARGET_DIR'] = self.output_dir
181 self.env['CARGO_HOME'] = os.path.join(self.output_dir, 'cargo_home')
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000182 self.env['RUSTFLAGS'] = self._generate_rustflags()
Abhishek Pandit-Subedi1927afa2021-04-28 21:16:18 -0700183 self.env['CXX_ROOT_PATH'] = os.path.join(self.platform_dir, 'bt')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000184
185 # Configure some GN variables
186 if self.use_board:
187 self.env['PKG_CONFIG_PATH'] = os.path.join(self.use_board, self.libdir, 'pkgconfig')
188 libdir = os.path.join(self.use_board, self.libdir)
189 if self.env.get('LIBRARY_PATH'):
190 libpath = self.env['LIBRARY_PATH']
191 self.env['LIBRARY_PATH'] = '{}:{}'.format(libdir, libpath)
192 else:
193 self.env['LIBRARY_PATH'] = libdir
194
195 def run_command(self, target, args, cwd=None, env=None):
196 """ Run command and stream the output.
197 """
198 # Set some defaults
199 if not cwd:
200 cwd = self.platform_dir
201 if not env:
202 env = self.env
203
204 log_file = os.path.join(self.output_dir, '{}.log'.format(target))
205 with open(log_file, 'wb') as lf:
206 rc = 0
207 process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
208 while True:
209 line = process.stdout.readline()
210 print(line.decode('utf-8'), end="")
211 lf.write(line)
212 if not line:
213 rc = process.poll()
214 if rc is not None:
215 break
216
217 time.sleep(0.1)
218
219 if rc != 0:
220 raise Exception("Return code is {}".format(rc))
221
222 def _get_basever(self):
223 if self.libbase_ver:
224 return self.libbase_ver
225
226 self.libbase_ver = os.environ.get('BASE_VER', '')
227 if not self.libbase_ver:
228 base_file = os.path.join(self.sysroot, 'usr/share/libchrome/BASE_VER')
229 try:
230 with open(base_file, 'r') as f:
231 self.libbase_ver = f.read().strip('\n')
232 except:
233 self.libbase_ver = 'NOT-INSTALLED'
234
235 return self.libbase_ver
236
237 def _gn_default_output(self):
238 return os.path.join(self.output_dir, 'out/Default')
239
240 def _gn_configure(self):
241 """ Configure all required parameters for platform2.
242
243 Mostly copied from //common-mk/platform2.py
244 """
245 clang = self.args.clang
246
247 def to_gn_string(s):
248 return '"%s"' % s.replace('"', '\\"')
249
250 def to_gn_list(strs):
251 return '[%s]' % ','.join([to_gn_string(s) for s in strs])
252
253 def to_gn_args_args(gn_args):
254 for k, v in gn_args.items():
255 if isinstance(v, bool):
256 v = str(v).lower()
257 elif isinstance(v, list):
258 v = to_gn_list(v)
259 elif isinstance(v, six.string_types):
260 v = to_gn_string(v)
261 else:
262 raise AssertionError('Unexpected %s, %r=%r' % (type(v), k, v))
263 yield '%s=%s' % (k.replace('-', '_'), v)
264
265 gn_args = {
266 'platform_subdir': 'bt',
267 'cc': 'clang' if clang else 'gcc',
268 'cxx': 'clang++' if clang else 'g++',
269 'ar': 'llvm-ar' if clang else 'ar',
270 'pkg-config': 'pkg-config',
271 'clang_cc': clang,
272 'clang_cxx': clang,
273 'OS': 'linux',
274 'sysroot': self.sysroot,
275 'libdir': os.path.join(self.sysroot, self.libdir),
276 'build_root': self.output_dir,
277 'platform2_root': self.platform_dir,
278 'libbase_ver': self._get_basever(),
279 'enable_exceptions': os.environ.get('CXXEXCEPTIONS', 0) == '1',
280 'external_cflags': [],
281 'external_cxxflags': [],
282 'enable_werror': False,
283 }
284
285 if clang:
286 # Make sure to mark the clang use flag as true
287 self.use.set_flag('clang', True)
288 gn_args['external_cxxflags'] += ['-I/usr/include/']
289
290 # EXTREME HACK ALERT
291 #
292 # In my laziness, I am supporting building against an already built
293 # sysroot path (i.e. chromeos board) so that I don't have to build
294 # libchrome or modp_b64 locally.
295 if self.use_board:
296 includedir = os.path.join(self.use_board, 'usr/include')
297 gn_args['external_cxxflags'] += [
298 '-I{}'.format(includedir),
299 '-I{}/libchrome'.format(includedir),
300 '-I{}/gtest'.format(includedir),
301 '-I{}/gmock'.format(includedir),
302 '-I{}/modp_b64'.format(includedir),
303 ]
304 gn_args_args = list(to_gn_args_args(gn_args))
305 use_args = ['%s=%s' % (k, str(v).lower()) for k, v in self.use.flags.items()]
306 gn_args_args += ['use={%s}' % (' '.join(use_args))]
307
308 gn_args = [
309 'gn',
310 'gen',
311 ]
312
313 if self.args.verbose:
314 gn_args.append('-v')
315
316 gn_args += [
317 '--root=%s' % self.platform_dir,
318 '--args=%s' % ' '.join(gn_args_args),
319 self._gn_default_output(),
320 ]
321
Sonny Sasaka706ec3b2021-03-25 05:39:20 -0700322 if 'PKG_CONFIG_PATH' in self.env:
323 print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH'])
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000324
325 self.run_command('configure', gn_args)
326
327 def _gn_build(self, target):
328 """ Generate the ninja command for the target and run it.
329 """
330 args = ['%s:%s' % ('bt', target)]
331 ninja_args = ['ninja', '-C', self._gn_default_output()]
332 if self.jobs:
333 ninja_args += ['-j', str(self.jobs)]
334 ninja_args += args
335
336 if self.args.verbose:
337 ninja_args.append('-v')
338
339 self.run_command('build', ninja_args)
340
341 def _rust_configure(self):
342 """ Generate config file at cargo_home so we use vendored crates.
343 """
344 template = """
345 [source.systembt]
346 directory = "{}/external/rust/vendor"
347
348 [source.crates-io]
349 replace-with = "systembt"
350 local-registry = "/nonexistent"
351 """
Sonny Sasakac1335a22021-03-25 07:10:47 -0700352
353 if self.args.vendored_rust:
354 contents = template.format(self.platform_dir)
355 with open(os.path.join(self.env['CARGO_HOME'], 'config'), 'w') as f:
356 f.write(contents)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000357
358 def _rust_build(self):
359 """ Run `cargo build` from platform2/bt directory.
360 """
361 self.run_command('rust', ['cargo', 'build'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
362
363 def _target_prepare(self):
364 """ Target to prepare the output directory for building.
365
366 This runs gn gen to generate all rquired files and set up the Rust
367 config properly. This will be run
368 """
369 self._gn_configure()
370 self._rust_configure()
371
372 def _target_tools(self):
373 """ Build the tools target in an already prepared environment.
374 """
375 self._gn_build('tools')
376
377 # Also copy bluetooth_packetgen to CARGO_HOME so it's available
378 shutil.copy(
379 os.path.join(self._gn_default_output(), 'bluetooth_packetgen'), os.path.join(self.env['CARGO_HOME'], 'bin'))
380
381 def _target_rust(self):
382 """ Build rust artifacts in an already prepared environment.
383 """
384 self._rust_build()
Sonny Sasakac1335a22021-03-25 07:10:47 -0700385 rust_dir = os.path.join(self._gn_default_output(), 'rust')
386 if os.path.exists(rust_dir):
387 shutil.rmtree(rust_dir)
388 shutil.copytree(os.path.join(self.output_dir, 'debug'), rust_dir)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000389
390 def _target_main(self):
391 """ Build the main GN artifacts in an already prepared environment.
392 """
393 self._gn_build('all')
394
395 def _target_test(self):
396 """ Runs the host tests.
397 """
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000398 # Rust tests first
399 self.run_command('test', ['cargo', 'test'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
400
401 # Host tests second based on host test list
402 for t in HOST_TESTS:
403 self.run_command(
404 'test', [os.path.join(self.output_dir, 'out/Default', t)],
405 cwd=os.path.join(self.output_dir),
406 env=self.env)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000407
408 def _target_clean(self):
409 """ Delete the output directory entirely.
410 """
411 shutil.rmtree(self.output_dir)
412
413 def _target_all(self):
414 """ Build all common targets (skipping test and clean).
415 """
416 self._target_prepare()
417 self._target_tools()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000418 self._target_main()
Abhishek Pandit-Subedia7b57b72021-04-01 15:33:05 -0700419 self._target_rust()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000420
421 def build(self):
422 """ Builds according to self.target
423 """
424 print('Building target ', self.target)
425
426 if self.target == 'prepare':
427 self._target_prepare()
428 elif self.target == 'tools':
429 self._target_tools()
430 elif self.target == 'rust':
431 self._target_rust()
432 elif self.target == 'main':
433 self._target_main()
434 elif self.target == 'test':
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000435 self._target_test()
436 elif self.target == 'clean':
437 self._target_clean()
438 elif self.target == 'all':
439 self._target_all()
440
441
442if __name__ == '__main__':
443 parser = argparse.ArgumentParser(description='Simple build for host.')
444 parser.add_argument('--output', help='Output directory for the build.', required=True)
445 parser.add_argument('--platform-dir', help='Directory where platform2 is staged.', required=True)
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000446 parser.add_argument('--clang', help='Use clang compiler.', default=False, action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000447 parser.add_argument('--use', help='Set a specific use flag.')
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000448 parser.add_argument('--notest', help="Don't compile test code.", default=False, action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000449 parser.add_argument('--target', help='Run specific build target')
450 parser.add_argument('--sysroot', help='Set a specific sysroot path', default='/')
451 parser.add_argument('--libdir', help='Libdir - default = usr/lib64', default='usr/lib64')
452 parser.add_argument('--use-board', help='Use a built x86 board for dependencies. Provide path.')
453 parser.add_argument('--jobs', help='Number of jobs to run', default=0, type=int)
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000454 parser.add_argument('--vendored-rust', help='Use vendored rust crates', default=False, action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000455 parser.add_argument('--verbose', help='Verbose logs for build.')
456
457 args = parser.parse_args()
458 build = HostBuild(args)
459 build.build()