3 # Copyright 2021 Google, Inc.
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:
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
18 For building, you will first have to stage a platform directory that has the
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.
31 import multiprocessing
38 # Use flags required by common-mk (find -type f | grep -nE 'use[.]' {})
55 'bt_nonstandard_codecs': False,
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
64 'test', # Run the unit tests
65 'clean', # Clean up output directory
66 'all', # All targets except test and clean
70 'bluetooth_test_common',
82 def __init__(self, use_flags):
83 """ Construct the use flags.
86 use_flags: List of use flags parsed from the command.
90 # Import use flags required by common-mk
91 for use in COMMON_MK_USES:
92 self.set_flag(use, False)
95 for use, value in USE_DEFAULTS.items():
96 self.set_flag(use, value)
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)
104 def set_flag(self, key, value=True):
105 setattr(self, key, value)
106 self.flags[key] = value
111 def __init__(self, args):
112 """ Construct the builder.
115 args: Parsed arguments from ArgumentParser
119 # Set jobs to number of cpus unless explicitly set
120 self.jobs = self.args.jobs
122 self.jobs = multiprocessing.cpu_count()
123 print("Number of jobs = {}".format(self.jobs))
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
132 # If default target isn't set, build everything
134 if hasattr(self.args, 'target') and self.args.target:
135 self.target = self.args.target
137 target_use = self.args.use if self.args.use else []
139 # Unless set, always build test code
140 if not self.args.notest:
141 target_use.append('test')
143 self.use = UseFlags(target_use)
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'
149 # Make sure output directory exists (or create it)
150 os.makedirs(self.output_dir, exist_ok=True)
152 # Set some default attributes
153 self.libbase_ver = None
155 self.configure_environ()
157 def _generate_rustflags(self):
158 """ Rustflags to include for the build.
162 '{}/out/Default/'.format(self.output_dir),
164 'link-arg=-Wl,--allow-multiple-definition',
167 return ' '.join(rust_flags)
169 def configure_environ(self):
170 """ Configure environment variables for GN and Cargo.
172 self.env = os.environ.copy()
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)
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')
182 self.env['RUSTFLAGS'] = self._generate_rustflags()
184 # Configure some GN variables
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)
192 self.env['LIBRARY_PATH'] = libdir
194 def run_command(self, target, args, cwd=None, env=None):
195 """ Run command and stream the output.
199 cwd = self.platform_dir
203 log_file = os.path.join(self.output_dir, '{}.log'.format(target))
204 with open(log_file, 'wb') as lf:
206 process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
208 line = process.stdout.readline()
209 print(line.decode('utf-8'), end="")
219 raise Exception("Return code is {}".format(rc))
221 def _get_basever(self):
223 return self.libbase_ver
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')
229 with open(base_file, 'r') as f:
230 self.libbase_ver = f.read().strip('\n')
232 self.libbase_ver = 'NOT-INSTALLED'
234 return self.libbase_ver
236 def _gn_default_output(self):
237 return os.path.join(self.output_dir, 'out/Default')
239 def _gn_configure(self):
240 """ Configure all required parameters for platform2.
242 Mostly copied from //common-mk/platform2.py
244 clang = self.args.clang
247 return '"%s"' % s.replace('"', '\\"')
249 def to_gn_list(strs):
250 return '[%s]' % ','.join([to_gn_string(s) for s in strs])
252 def to_gn_args_args(gn_args):
253 for k, v in gn_args.items():
254 if isinstance(v, bool):
256 elif isinstance(v, list):
258 elif isinstance(v, six.string_types):
261 raise AssertionError('Unexpected %s, %r=%r' % (type(v), k, v))
262 yield '%s=%s' % (k.replace('-', '_'), v)
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',
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,
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/']
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.
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),
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))]
312 if self.args.verbose:
316 '--root=%s' % self.platform_dir,
317 '--args=%s' % ' '.join(gn_args_args),
318 self._gn_default_output(),
321 if 'PKG_CONFIG_PATH' in self.env:
322 print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH'])
324 self.run_command('configure', gn_args)
326 def _gn_build(self, target):
327 """ Generate the ninja command for the target and run it.
329 args = ['%s:%s' % ('bt', target)]
330 ninja_args = ['ninja', '-C', self._gn_default_output()]
332 ninja_args += ['-j', str(self.jobs)]
335 if self.args.verbose:
336 ninja_args.append('-v')
338 self.run_command('build', ninja_args)
340 def _rust_configure(self):
341 """ Generate config file at cargo_home so we use vendored crates.
345 directory = "{}/external/rust/vendor"
348 replace-with = "systembt"
349 local-registry = "/nonexistent"
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:
357 def _rust_build(self):
358 """ Run `cargo build` from platform2/bt directory.
360 self.run_command('rust', ['cargo', 'build'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
362 def _target_prepare(self):
363 """ Target to prepare the output directory for building.
365 This runs gn gen to generate all rquired files and set up the Rust
366 config properly. This will be run
369 self._rust_configure()
371 def _target_tools(self):
372 """ Build the tools target in an already prepared environment.
374 self._gn_build('tools')
376 # Also copy bluetooth_packetgen to CARGO_HOME so it's available
378 os.path.join(self._gn_default_output(), 'bluetooth_packetgen'), os.path.join(self.env['CARGO_HOME'], 'bin'))
380 def _target_rust(self):
381 """ Build rust artifacts in an already prepared environment.
384 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)
389 def _target_main(self):
390 """ Build the main GN artifacts in an already prepared environment.
392 self._gn_build('all')
394 def _target_test(self):
395 """ Runs the host tests.
398 self.run_command('test', ['cargo', 'test'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
400 # Host tests second based on host test list
403 'test', [os.path.join(self.output_dir, 'out/Default', t)],
404 cwd=os.path.join(self.output_dir),
407 def _target_clean(self):
408 """ Delete the output directory entirely.
410 shutil.rmtree(self.output_dir)
412 def _target_all(self):
413 """ Build all common targets (skipping test and clean).
415 self._target_prepare()
421 """ Builds according to self.target
423 print('Building target ', self.target)
425 if self.target == 'prepare':
426 self._target_prepare()
427 elif self.target == 'tools':
429 elif self.target == 'rust':
431 elif self.target == 'main':
433 elif self.target == 'test':
435 elif self.target == 'clean':
437 elif self.target == 'all':
441 if __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)
445 parser.add_argument('--clang', help='Use clang compiler.', default=False, action='store_true')
446 parser.add_argument('--use', help='Set a specific use flag.')
447 parser.add_argument('--notest', help="Don't compile test code.", default=False, action='store_true')
448 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)
453 parser.add_argument('--vendored-rust', help='Use vendored rust crates', default=False, action='store_true')
454 parser.add_argument('--verbose', help='Verbose logs for build.')
456 args = parser.parse_args()
457 build = HostBuild(args)