blob: c6cbabd3692889633d7a3418e956e836294b9bd9 [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-Subedif52cf662021-03-02 22:33:25 +0000183
184 # Configure some GN variables
185 if self.use_board:
186 self.env['PKG_CONFIG_PATH'] = os.path.join(self.use_board, self.libdir, 'pkgconfig')
187 libdir = os.path.join(self.use_board, self.libdir)
188 if self.env.get('LIBRARY_PATH'):
189 libpath = self.env['LIBRARY_PATH']
190 self.env['LIBRARY_PATH'] = '{}:{}'.format(libdir, libpath)
191 else:
192 self.env['LIBRARY_PATH'] = libdir
193
194 def run_command(self, target, args, cwd=None, env=None):
195 """ Run command and stream the output.
196 """
197 # Set some defaults
198 if not cwd:
199 cwd = self.platform_dir
200 if not env:
201 env = self.env
202
203 log_file = os.path.join(self.output_dir, '{}.log'.format(target))
204 with open(log_file, 'wb') as lf:
205 rc = 0
206 process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
207 while True:
208 line = process.stdout.readline()
209 print(line.decode('utf-8'), end="")
210 lf.write(line)
211 if not line:
212 rc = process.poll()
213 if rc is not None:
214 break
215
216 time.sleep(0.1)
217
218 if rc != 0:
219 raise Exception("Return code is {}".format(rc))
220
221 def _get_basever(self):
222 if self.libbase_ver:
223 return self.libbase_ver
224
225 self.libbase_ver = os.environ.get('BASE_VER', '')
226 if not self.libbase_ver:
227 base_file = os.path.join(self.sysroot, 'usr/share/libchrome/BASE_VER')
228 try:
229 with open(base_file, 'r') as f:
230 self.libbase_ver = f.read().strip('\n')
231 except:
232 self.libbase_ver = 'NOT-INSTALLED'
233
234 return self.libbase_ver
235
236 def _gn_default_output(self):
237 return os.path.join(self.output_dir, 'out/Default')
238
239 def _gn_configure(self):
240 """ Configure all required parameters for platform2.
241
242 Mostly copied from //common-mk/platform2.py
243 """
244 clang = self.args.clang
245
246 def to_gn_string(s):
247 return '"%s"' % s.replace('"', '\\"')
248
249 def to_gn_list(strs):
250 return '[%s]' % ','.join([to_gn_string(s) for s in strs])
251
252 def to_gn_args_args(gn_args):
253 for k, v in gn_args.items():
254 if isinstance(v, bool):
255 v = str(v).lower()
256 elif isinstance(v, list):
257 v = to_gn_list(v)
258 elif isinstance(v, six.string_types):
259 v = to_gn_string(v)
260 else:
261 raise AssertionError('Unexpected %s, %r=%r' % (type(v), k, v))
262 yield '%s=%s' % (k.replace('-', '_'), v)
263
264 gn_args = {
265 'platform_subdir': 'bt',
266 'cc': 'clang' if clang else 'gcc',
267 'cxx': 'clang++' if clang else 'g++',
268 'ar': 'llvm-ar' if clang else 'ar',
269 'pkg-config': 'pkg-config',
270 'clang_cc': clang,
271 'clang_cxx': clang,
272 'OS': 'linux',
273 'sysroot': self.sysroot,
274 'libdir': os.path.join(self.sysroot, self.libdir),
275 'build_root': self.output_dir,
276 'platform2_root': self.platform_dir,
277 'libbase_ver': self._get_basever(),
278 'enable_exceptions': os.environ.get('CXXEXCEPTIONS', 0) == '1',
279 'external_cflags': [],
280 'external_cxxflags': [],
281 'enable_werror': False,
282 }
283
284 if clang:
285 # Make sure to mark the clang use flag as true
286 self.use.set_flag('clang', True)
287 gn_args['external_cxxflags'] += ['-I/usr/include/']
288
289 # EXTREME HACK ALERT
290 #
291 # In my laziness, I am supporting building against an already built
292 # sysroot path (i.e. chromeos board) so that I don't have to build
293 # libchrome or modp_b64 locally.
294 if self.use_board:
295 includedir = os.path.join(self.use_board, 'usr/include')
296 gn_args['external_cxxflags'] += [
297 '-I{}'.format(includedir),
298 '-I{}/libchrome'.format(includedir),
299 '-I{}/gtest'.format(includedir),
300 '-I{}/gmock'.format(includedir),
301 '-I{}/modp_b64'.format(includedir),
302 ]
303 gn_args_args = list(to_gn_args_args(gn_args))
304 use_args = ['%s=%s' % (k, str(v).lower()) for k, v in self.use.flags.items()]
305 gn_args_args += ['use={%s}' % (' '.join(use_args))]
306
307 gn_args = [
308 'gn',
309 'gen',
310 ]
311
312 if self.args.verbose:
313 gn_args.append('-v')
314
315 gn_args += [
316 '--root=%s' % self.platform_dir,
317 '--args=%s' % ' '.join(gn_args_args),
318 self._gn_default_output(),
319 ]
320
Sonny Sasaka706ec3b2021-03-25 05:39:20 -0700321 if 'PKG_CONFIG_PATH' in self.env:
322 print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH'])
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000323
324 self.run_command('configure', gn_args)
325
326 def _gn_build(self, target):
327 """ Generate the ninja command for the target and run it.
328 """
329 args = ['%s:%s' % ('bt', target)]
330 ninja_args = ['ninja', '-C', self._gn_default_output()]
331 if self.jobs:
332 ninja_args += ['-j', str(self.jobs)]
333 ninja_args += args
334
335 if self.args.verbose:
336 ninja_args.append('-v')
337
338 self.run_command('build', ninja_args)
339
340 def _rust_configure(self):
341 """ Generate config file at cargo_home so we use vendored crates.
342 """
343 template = """
344 [source.systembt]
345 directory = "{}/external/rust/vendor"
346
347 [source.crates-io]
348 replace-with = "systembt"
349 local-registry = "/nonexistent"
350 """
Sonny Sasakac1335a22021-03-25 07:10:47 -0700351
352 if self.args.vendored_rust:
353 contents = template.format(self.platform_dir)
354 with open(os.path.join(self.env['CARGO_HOME'], 'config'), 'w') as f:
355 f.write(contents)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000356
357 def _rust_build(self):
358 """ Run `cargo build` from platform2/bt directory.
359 """
360 self.run_command('rust', ['cargo', 'build'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
361
362 def _target_prepare(self):
363 """ Target to prepare the output directory for building.
364
365 This runs gn gen to generate all rquired files and set up the Rust
366 config properly. This will be run
367 """
368 self._gn_configure()
369 self._rust_configure()
370
371 def _target_tools(self):
372 """ Build the tools target in an already prepared environment.
373 """
374 self._gn_build('tools')
375
376 # Also copy bluetooth_packetgen to CARGO_HOME so it's available
377 shutil.copy(
378 os.path.join(self._gn_default_output(), 'bluetooth_packetgen'), os.path.join(self.env['CARGO_HOME'], 'bin'))
379
380 def _target_rust(self):
381 """ Build rust artifacts in an already prepared environment.
382 """
383 self._rust_build()
Sonny Sasakac1335a22021-03-25 07:10:47 -0700384 rust_dir = os.path.join(self._gn_default_output(), 'rust')
385 if os.path.exists(rust_dir):
386 shutil.rmtree(rust_dir)
387 shutil.copytree(os.path.join(self.output_dir, 'debug'), rust_dir)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000388
389 def _target_main(self):
390 """ Build the main GN artifacts in an already prepared environment.
391 """
392 self._gn_build('all')
393
394 def _target_test(self):
395 """ Runs the host tests.
396 """
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000397 # Rust tests first
398 self.run_command('test', ['cargo', 'test'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
399
400 # Host tests second based on host test list
401 for t in HOST_TESTS:
402 self.run_command(
403 'test', [os.path.join(self.output_dir, 'out/Default', t)],
404 cwd=os.path.join(self.output_dir),
405 env=self.env)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000406
407 def _target_clean(self):
408 """ Delete the output directory entirely.
409 """
410 shutil.rmtree(self.output_dir)
411
412 def _target_all(self):
413 """ Build all common targets (skipping test and clean).
414 """
415 self._target_prepare()
416 self._target_tools()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000417 self._target_main()
Abhishek Pandit-Subedia7b57b72021-04-01 15:33:05 -0700418 self._target_rust()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000419
420 def build(self):
421 """ Builds according to self.target
422 """
423 print('Building target ', self.target)
424
425 if self.target == 'prepare':
426 self._target_prepare()
427 elif self.target == 'tools':
428 self._target_tools()
429 elif self.target == 'rust':
430 self._target_rust()
431 elif self.target == 'main':
432 self._target_main()
433 elif self.target == 'test':
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000434 self._target_test()
435 elif self.target == 'clean':
436 self._target_clean()
437 elif self.target == 'all':
438 self._target_all()
439
440
441if __name__ == '__main__':
442 parser = argparse.ArgumentParser(description='Simple build for host.')
443 parser.add_argument('--output', help='Output directory for the build.', required=True)
444 parser.add_argument('--platform-dir', help='Directory where platform2 is staged.', required=True)
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000445 parser.add_argument('--clang', help='Use clang compiler.', default=False, action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000446 parser.add_argument('--use', help='Set a specific use flag.')
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000447 parser.add_argument('--notest', help="Don't compile test code.", default=False, action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000448 parser.add_argument('--target', help='Run specific build target')
449 parser.add_argument('--sysroot', help='Set a specific sysroot path', default='/')
450 parser.add_argument('--libdir', help='Libdir - default = usr/lib64', default='usr/lib64')
451 parser.add_argument('--use-board', help='Use a built x86 board for dependencies. Provide path.')
452 parser.add_argument('--jobs', help='Number of jobs to run', default=0, type=int)
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000453 parser.add_argument('--vendored-rust', help='Use vendored rust crates', default=False, action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000454 parser.add_argument('--verbose', help='Verbose logs for build.')
455
456 args = parser.parse_args()
457 build = HostBuild(args)
458 build.build()