blob: 131cbb299e82101ac158f7097ff0e7c54ad03c3a [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
64 'test', # Build and run the unit tests
65 'clean', # Clean up output directory
66 'all', # All targets except test and clean
67]
68
69
70class UseFlags():
71
72 def __init__(self, use_flags):
73 """ Construct the use flags.
74
75 Args:
76 use_flags: List of use flags parsed from the command.
77 """
78 self.flags = {}
79
80 # Import use flags required by common-mk
81 for use in COMMON_MK_USES:
82 self.set_flag(use, False)
83
84 # Set our defaults
85 for use, value in USE_DEFAULTS.items():
86 self.set_flag(use, value)
87
88 # Set use flags - value is set to True unless the use starts with -
89 # All given use flags always override the defaults
90 for use in use_flags:
91 value = not use.startswith('-')
92 self.set_flag(use, value)
93
94 def set_flag(self, key, value=True):
95 setattr(self, key, value)
96 self.flags[key] = value
97
98
99class HostBuild():
100
101 def __init__(self, args):
102 """ Construct the builder.
103
104 Args:
105 args: Parsed arguments from ArgumentParser
106 """
107 self.args = args
108
109 # Set jobs to number of cpus unless explicitly set
110 self.jobs = self.args.jobs
111 if not self.jobs:
112 self.jobs = multiprocessing.cpu_count()
113
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
120
121 # If default target isn't set, build everything
122 self.target = 'all'
123 if hasattr(self.args, 'target') and self.args.target:
124 self.target = self.args.target
125
126 self.use = UseFlags(self.args.use if self.args.use else [])
127
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'
131
132 # Make sure output directory exists (or create it)
133 os.makedirs(self.output_dir, exist_ok=True)
134
135 # Set some default attributes
136 self.libbase_ver = None
137
138 self.configure_environ()
139
140 def configure_environ(self):
141 """ Configure environment variables for GN and Cargo.
142 """
143 self.env = os.environ.copy()
144
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)
149
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')
153
154 # Configure some GN variables
155 if self.use_board:
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)
161 else:
162 self.env['LIBRARY_PATH'] = libdir
163
164 def run_command(self, target, args, cwd=None, env=None):
165 """ Run command and stream the output.
166 """
167 # Set some defaults
168 if not cwd:
169 cwd = self.platform_dir
170 if not env:
171 env = self.env
172
173 log_file = os.path.join(self.output_dir, '{}.log'.format(target))
174 with open(log_file, 'wb') as lf:
175 rc = 0
176 process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
177 while True:
178 line = process.stdout.readline()
179 print(line.decode('utf-8'), end="")
180 lf.write(line)
181 if not line:
182 rc = process.poll()
183 if rc is not None:
184 break
185
186 time.sleep(0.1)
187
188 if rc != 0:
189 raise Exception("Return code is {}".format(rc))
190
191 def _get_basever(self):
192 if self.libbase_ver:
193 return self.libbase_ver
194
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')
198 try:
199 with open(base_file, 'r') as f:
200 self.libbase_ver = f.read().strip('\n')
201 except:
202 self.libbase_ver = 'NOT-INSTALLED'
203
204 return self.libbase_ver
205
206 def _gn_default_output(self):
207 return os.path.join(self.output_dir, 'out/Default')
208
209 def _gn_configure(self):
210 """ Configure all required parameters for platform2.
211
212 Mostly copied from //common-mk/platform2.py
213 """
214 clang = self.args.clang
215
216 def to_gn_string(s):
217 return '"%s"' % s.replace('"', '\\"')
218
219 def to_gn_list(strs):
220 return '[%s]' % ','.join([to_gn_string(s) for s in strs])
221
222 def to_gn_args_args(gn_args):
223 for k, v in gn_args.items():
224 if isinstance(v, bool):
225 v = str(v).lower()
226 elif isinstance(v, list):
227 v = to_gn_list(v)
228 elif isinstance(v, six.string_types):
229 v = to_gn_string(v)
230 else:
231 raise AssertionError('Unexpected %s, %r=%r' % (type(v), k, v))
232 yield '%s=%s' % (k.replace('-', '_'), v)
233
234 gn_args = {
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',
240 'clang_cc': clang,
241 'clang_cxx': clang,
242 'OS': 'linux',
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,
252 }
253
254 if clang:
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/']
258
259 # EXTREME HACK ALERT
260 #
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.
264 if self.use_board:
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),
272 ]
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))]
276
277 gn_args = [
278 'gn',
279 'gen',
280 ]
281
282 if self.args.verbose:
283 gn_args.append('-v')
284
285 gn_args += [
286 '--root=%s' % self.platform_dir,
287 '--args=%s' % ' '.join(gn_args_args),
288 self._gn_default_output(),
289 ]
290
Sonny Sasaka706ec3b2021-03-25 05:39:20 -0700291 if 'PKG_CONFIG_PATH' in self.env:
292 print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH'])
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000293
294 self.run_command('configure', gn_args)
295
296 def _gn_build(self, target):
297 """ Generate the ninja command for the target and run it.
298 """
299 args = ['%s:%s' % ('bt', target)]
300 ninja_args = ['ninja', '-C', self._gn_default_output()]
301 if self.jobs:
302 ninja_args += ['-j', str(self.jobs)]
303 ninja_args += args
304
305 if self.args.verbose:
306 ninja_args.append('-v')
307
308 self.run_command('build', ninja_args)
309
310 def _rust_configure(self):
311 """ Generate config file at cargo_home so we use vendored crates.
312 """
313 template = """
314 [source.systembt]
315 directory = "{}/external/rust/vendor"
316
317 [source.crates-io]
318 replace-with = "systembt"
319 local-registry = "/nonexistent"
320 """
Sonny Sasakac1335a22021-03-25 07:10:47 -0700321
322 if self.args.vendored_rust:
323 contents = template.format(self.platform_dir)
324 with open(os.path.join(self.env['CARGO_HOME'], 'config'), 'w') as f:
325 f.write(contents)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000326
327 def _rust_build(self):
328 """ Run `cargo build` from platform2/bt directory.
329 """
330 self.run_command('rust', ['cargo', 'build'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
331
332 def _target_prepare(self):
333 """ Target to prepare the output directory for building.
334
335 This runs gn gen to generate all rquired files and set up the Rust
336 config properly. This will be run
337 """
338 self._gn_configure()
339 self._rust_configure()
340
341 def _target_tools(self):
342 """ Build the tools target in an already prepared environment.
343 """
344 self._gn_build('tools')
345
346 # Also copy bluetooth_packetgen to CARGO_HOME so it's available
347 shutil.copy(
348 os.path.join(self._gn_default_output(), 'bluetooth_packetgen'), os.path.join(self.env['CARGO_HOME'], 'bin'))
349
350 def _target_rust(self):
351 """ Build rust artifacts in an already prepared environment.
352 """
353 self._rust_build()
Sonny Sasakac1335a22021-03-25 07:10:47 -0700354 rust_dir = os.path.join(self._gn_default_output(), 'rust')
355 if os.path.exists(rust_dir):
356 shutil.rmtree(rust_dir)
357 shutil.copytree(os.path.join(self.output_dir, 'debug'), rust_dir)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000358
359 def _target_main(self):
360 """ Build the main GN artifacts in an already prepared environment.
361 """
362 self._gn_build('all')
363
364 def _target_test(self):
365 """ Runs the host tests.
366 """
367 raise Exception('Not yet implemented')
368
369 def _target_clean(self):
370 """ Delete the output directory entirely.
371 """
372 shutil.rmtree(self.output_dir)
373
374 def _target_all(self):
375 """ Build all common targets (skipping test and clean).
376 """
377 self._target_prepare()
378 self._target_tools()
379 self._target_rust()
380 self._target_main()
381
382 def build(self):
383 """ Builds according to self.target
384 """
385 print('Building target ', self.target)
386
387 if self.target == 'prepare':
388 self._target_prepare()
389 elif self.target == 'tools':
390 self._target_tools()
391 elif self.target == 'rust':
392 self._target_rust()
393 elif self.target == 'main':
394 self._target_main()
395 elif self.target == 'test':
396 self.use.set_flag('test')
397 self._target_all()
398 self._target_test()
399 elif self.target == 'clean':
400 self._target_clean()
401 elif self.target == 'all':
402 self._target_all()
403
404
405if __name__ == '__main__':
406 parser = argparse.ArgumentParser(description='Simple build for host.')
407 parser.add_argument('--output', help='Output directory for the build.', required=True)
408 parser.add_argument('--platform-dir', help='Directory where platform2 is staged.', required=True)
409 parser.add_argument('--clang', help='Use clang compiler.', default=False, action="store_true")
410 parser.add_argument('--use', help='Set a specific use flag.')
411 parser.add_argument('--target', help='Run specific build target')
412 parser.add_argument('--sysroot', help='Set a specific sysroot path', default='/')
413 parser.add_argument('--libdir', help='Libdir - default = usr/lib64', default='usr/lib64')
414 parser.add_argument('--use-board', help='Use a built x86 board for dependencies. Provide path.')
415 parser.add_argument('--jobs', help='Number of jobs to run', default=0, type=int)
Sonny Sasakac1335a22021-03-25 07:10:47 -0700416 parser.add_argument('--vendored-rust', help='Use vendored rust crates', default=False, action="store_true")
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000417 parser.add_argument('--verbose', help='Verbose logs for build.')
418
419 args = parser.parse_args()
420 build = HostBuild(args)
421 build.build()