blob: 1ece6ceef5251cc276bbee4f8e0d879df6082c98 [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
291 print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH'])
292
293 self.run_command('configure', gn_args)
294
295 def _gn_build(self, target):
296 """ Generate the ninja command for the target and run it.
297 """
298 args = ['%s:%s' % ('bt', target)]
299 ninja_args = ['ninja', '-C', self._gn_default_output()]
300 if self.jobs:
301 ninja_args += ['-j', str(self.jobs)]
302 ninja_args += args
303
304 if self.args.verbose:
305 ninja_args.append('-v')
306
307 self.run_command('build', ninja_args)
308
309 def _rust_configure(self):
310 """ Generate config file at cargo_home so we use vendored crates.
311 """
312 template = """
313 [source.systembt]
314 directory = "{}/external/rust/vendor"
315
316 [source.crates-io]
317 replace-with = "systembt"
318 local-registry = "/nonexistent"
319 """
320 contents = template.format(self.platform_dir)
321 with open(os.path.join(self.env['CARGO_HOME'], 'config'), 'w') as f:
322 f.write(contents)
323
324 def _rust_build(self):
325 """ Run `cargo build` from platform2/bt directory.
326 """
327 self.run_command('rust', ['cargo', 'build'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
328
329 def _target_prepare(self):
330 """ Target to prepare the output directory for building.
331
332 This runs gn gen to generate all rquired files and set up the Rust
333 config properly. This will be run
334 """
335 self._gn_configure()
336 self._rust_configure()
337
338 def _target_tools(self):
339 """ Build the tools target in an already prepared environment.
340 """
341 self._gn_build('tools')
342
343 # Also copy bluetooth_packetgen to CARGO_HOME so it's available
344 shutil.copy(
345 os.path.join(self._gn_default_output(), 'bluetooth_packetgen'), os.path.join(self.env['CARGO_HOME'], 'bin'))
346
347 def _target_rust(self):
348 """ Build rust artifacts in an already prepared environment.
349 """
350 self._rust_build()
351
352 def _target_main(self):
353 """ Build the main GN artifacts in an already prepared environment.
354 """
355 self._gn_build('all')
356
357 def _target_test(self):
358 """ Runs the host tests.
359 """
360 raise Exception('Not yet implemented')
361
362 def _target_clean(self):
363 """ Delete the output directory entirely.
364 """
365 shutil.rmtree(self.output_dir)
366
367 def _target_all(self):
368 """ Build all common targets (skipping test and clean).
369 """
370 self._target_prepare()
371 self._target_tools()
372 self._target_rust()
373 self._target_main()
374
375 def build(self):
376 """ Builds according to self.target
377 """
378 print('Building target ', self.target)
379
380 if self.target == 'prepare':
381 self._target_prepare()
382 elif self.target == 'tools':
383 self._target_tools()
384 elif self.target == 'rust':
385 self._target_rust()
386 elif self.target == 'main':
387 self._target_main()
388 elif self.target == 'test':
389 self.use.set_flag('test')
390 self._target_all()
391 self._target_test()
392 elif self.target == 'clean':
393 self._target_clean()
394 elif self.target == 'all':
395 self._target_all()
396
397
398if __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.')
410
411 args = parser.parse_args()
412 build = HostBuild(args)
413 build.build()