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', # Build and run the unit tests
65 'clean', # Clean up output directory
66 'all', # All targets except test and clean
72 def __init__(self, use_flags):
73 """ Construct the use flags.
76 use_flags: List of use flags parsed from the command.
80 # Import use flags required by common-mk
81 for use in COMMON_MK_USES:
82 self.set_flag(use, False)
85 for use, value in USE_DEFAULTS.items():
86 self.set_flag(use, value)
88 # Set use flags - value is set to True unless the use starts with -
89 # All given use flags always override the defaults
91 value = not use.startswith('-')
92 self.set_flag(use, value)
94 def set_flag(self, key, value=True):
95 setattr(self, key, value)
96 self.flags[key] = value
101 def __init__(self, args):
102 """ Construct the builder.
105 args: Parsed arguments from ArgumentParser
109 # Set jobs to number of cpus unless explicitly set
110 self.jobs = self.args.jobs
112 self.jobs = multiprocessing.cpu_count()
114 # Normalize all directories
115 self.output_dir = os.path.abspath(self.args.output)
116 self.platform_dir = os.path.abspath(self.args.platform_dir)
117 self.sysroot = self.args.sysroot
118 self.use_board = os.path.abspath(self.args.use_board) if self.args.use_board else None
119 self.libdir = self.args.libdir
121 # If default target isn't set, build everything
123 if hasattr(self.args, 'target') and self.args.target:
124 self.target = self.args.target
126 self.use = UseFlags(self.args.use if self.args.use else [])
128 # Validate platform directory
129 assert os.path.isdir(self.platform_dir), 'Platform dir does not exist'
130 assert os.path.isfile(os.path.join(self.platform_dir, '.gn')), 'Platform dir does not have .gn at root'
132 # Make sure output directory exists (or create it)
133 os.makedirs(self.output_dir, exist_ok=True)
135 # Set some default attributes
136 self.libbase_ver = None
138 self.configure_environ()
140 def configure_environ(self):
141 """ Configure environment variables for GN and Cargo.
143 self.env = os.environ.copy()
145 # Make sure cargo home dir exists and has a bin directory
146 cargo_home = os.path.join(self.output_dir, 'cargo_home')
147 os.makedirs(cargo_home, exist_ok=True)
148 os.makedirs(os.path.join(cargo_home, 'bin'), exist_ok=True)
150 # Configure Rust env variables
151 self.env['CARGO_TARGET_DIR'] = self.output_dir
152 self.env['CARGO_HOME'] = os.path.join(self.output_dir, 'cargo_home')
154 # Configure some GN variables
156 self.env['PKG_CONFIG_PATH'] = os.path.join(self.use_board, self.libdir, 'pkgconfig')
157 libdir = os.path.join(self.use_board, self.libdir)
158 if self.env.get('LIBRARY_PATH'):
159 libpath = self.env['LIBRARY_PATH']
160 self.env['LIBRARY_PATH'] = '{}:{}'.format(libdir, libpath)
162 self.env['LIBRARY_PATH'] = libdir
164 def run_command(self, target, args, cwd=None, env=None):
165 """ Run command and stream the output.
169 cwd = self.platform_dir
173 log_file = os.path.join(self.output_dir, '{}.log'.format(target))
174 with open(log_file, 'wb') as lf:
176 process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
178 line = process.stdout.readline()
179 print(line.decode('utf-8'), end="")
189 raise Exception("Return code is {}".format(rc))
191 def _get_basever(self):
193 return self.libbase_ver
195 self.libbase_ver = os.environ.get('BASE_VER', '')
196 if not self.libbase_ver:
197 base_file = os.path.join(self.sysroot, 'usr/share/libchrome/BASE_VER')
199 with open(base_file, 'r') as f:
200 self.libbase_ver = f.read().strip('\n')
202 self.libbase_ver = 'NOT-INSTALLED'
204 return self.libbase_ver
206 def _gn_default_output(self):
207 return os.path.join(self.output_dir, 'out/Default')
209 def _gn_configure(self):
210 """ Configure all required parameters for platform2.
212 Mostly copied from //common-mk/platform2.py
214 clang = self.args.clang
217 return '"%s"' % s.replace('"', '\\"')
219 def to_gn_list(strs):
220 return '[%s]' % ','.join([to_gn_string(s) for s in strs])
222 def to_gn_args_args(gn_args):
223 for k, v in gn_args.items():
224 if isinstance(v, bool):
226 elif isinstance(v, list):
228 elif isinstance(v, six.string_types):
231 raise AssertionError('Unexpected %s, %r=%r' % (type(v), k, v))
232 yield '%s=%s' % (k.replace('-', '_'), v)
235 'platform_subdir': 'bt',
236 'cc': 'clang' if clang else 'gcc',
237 'cxx': 'clang++' if clang else 'g++',
238 'ar': 'llvm-ar' if clang else 'ar',
239 'pkg-config': 'pkg-config',
243 'sysroot': self.sysroot,
244 'libdir': os.path.join(self.sysroot, self.libdir),
245 'build_root': self.output_dir,
246 'platform2_root': self.platform_dir,
247 'libbase_ver': self._get_basever(),
248 'enable_exceptions': os.environ.get('CXXEXCEPTIONS', 0) == '1',
249 'external_cflags': [],
250 'external_cxxflags': [],
251 'enable_werror': False,
255 # Make sure to mark the clang use flag as true
256 self.use.set_flag('clang', True)
257 gn_args['external_cxxflags'] += ['-I/usr/include/']
261 # In my laziness, I am supporting building against an already built
262 # sysroot path (i.e. chromeos board) so that I don't have to build
263 # libchrome or modp_b64 locally.
265 includedir = os.path.join(self.use_board, 'usr/include')
266 gn_args['external_cxxflags'] += [
267 '-I{}'.format(includedir),
268 '-I{}/libchrome'.format(includedir),
269 '-I{}/gtest'.format(includedir),
270 '-I{}/gmock'.format(includedir),
271 '-I{}/modp_b64'.format(includedir),
273 gn_args_args = list(to_gn_args_args(gn_args))
274 use_args = ['%s=%s' % (k, str(v).lower()) for k, v in self.use.flags.items()]
275 gn_args_args += ['use={%s}' % (' '.join(use_args))]
282 if self.args.verbose:
286 '--root=%s' % self.platform_dir,
287 '--args=%s' % ' '.join(gn_args_args),
288 self._gn_default_output(),
291 print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH'])
293 self.run_command('configure', gn_args)
295 def _gn_build(self, target):
296 """ Generate the ninja command for the target and run it.
298 args = ['%s:%s' % ('bt', target)]
299 ninja_args = ['ninja', '-C', self._gn_default_output()]
301 ninja_args += ['-j', str(self.jobs)]
304 if self.args.verbose:
305 ninja_args.append('-v')
307 self.run_command('build', ninja_args)
309 def _rust_configure(self):
310 """ Generate config file at cargo_home so we use vendored crates.
314 directory = "{}/external/rust/vendor"
317 replace-with = "systembt"
318 local-registry = "/nonexistent"
320 contents = template.format(self.platform_dir)
321 with open(os.path.join(self.env['CARGO_HOME'], 'config'), 'w') as f:
324 def _rust_build(self):
325 """ Run `cargo build` from platform2/bt directory.
327 self.run_command('rust', ['cargo', 'build'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
329 def _target_prepare(self):
330 """ Target to prepare the output directory for building.
332 This runs gn gen to generate all rquired files and set up the Rust
333 config properly. This will be run
336 self._rust_configure()
338 def _target_tools(self):
339 """ Build the tools target in an already prepared environment.
341 self._gn_build('tools')
343 # Also copy bluetooth_packetgen to CARGO_HOME so it's available
345 os.path.join(self._gn_default_output(), 'bluetooth_packetgen'), os.path.join(self.env['CARGO_HOME'], 'bin'))
347 def _target_rust(self):
348 """ Build rust artifacts in an already prepared environment.
352 def _target_main(self):
353 """ Build the main GN artifacts in an already prepared environment.
355 self._gn_build('all')
357 def _target_test(self):
358 """ Runs the host tests.
360 raise Exception('Not yet implemented')
362 def _target_clean(self):
363 """ Delete the output directory entirely.
365 shutil.rmtree(self.output_dir)
367 def _target_all(self):
368 """ Build all common targets (skipping test and clean).
370 self._target_prepare()
376 """ Builds according to self.target
378 print('Building target ', self.target)
380 if self.target == 'prepare':
381 self._target_prepare()
382 elif self.target == 'tools':
384 elif self.target == 'rust':
386 elif self.target == 'main':
388 elif self.target == 'test':
389 self.use.set_flag('test')
392 elif self.target == 'clean':
394 elif self.target == 'all':
398 if __name__ == '__main__':
399 parser = argparse.ArgumentParser(description='Simple build for host.')
400 parser.add_argument('--output', help='Output directory for the build.', required=True)
401 parser.add_argument('--platform-dir', help='Directory where platform2 is staged.', required=True)
402 parser.add_argument('--clang', help='Use clang compiler.', default=False, action="store_true")
403 parser.add_argument('--use', help='Set a specific use flag.')
404 parser.add_argument('--target', help='Run specific build target')
405 parser.add_argument('--sysroot', help='Set a specific sysroot path', default='/')
406 parser.add_argument('--libdir', help='Libdir - default = usr/lib64', default='usr/lib64')
407 parser.add_argument('--use-board', help='Use a built x86 board for dependencies. Provide path.')
408 parser.add_argument('--jobs', help='Number of jobs to run', default=0, type=int)
409 parser.add_argument('--verbose', help='Verbose logs for build.')
411 args = parser.parse_args()
412 build = HostBuild(args)