blob: b73349382e59ff986b394625b22632fc1a5258d5 [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
Andre Bragaaa11e7d2022-08-10 21:46:44 +000033import platform
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000034import shutil
35import six
36import subprocess
37import sys
Abhishek Pandit-Subedia1a94d12021-11-18 13:14:04 -080038import tarfile
Chris Mantone7ad6332021-09-30 22:55:39 -070039import time
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000040
41# Use flags required by common-mk (find -type f | grep -nE 'use[.]' {})
42COMMON_MK_USES = [
43 'asan',
44 'coverage',
45 'cros_host',
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -080046 'cros_debug',
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000047 'fuzzer',
48 'fuzzer',
49 'msan',
50 'profiling',
51 'tcmalloc',
52 'test',
53 'ubsan',
54]
55
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -080056# Use a specific commit version for common-mk to avoid build surprises.
57COMMON_MK_COMMIT = "136c3e114b65f2c6c5f026376c2e75c73c2478a3"
58
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000059# Default use flags.
60USE_DEFAULTS = {
61 'android': False,
62 'bt_nonstandard_codecs': False,
63 'test': False,
64}
65
66VALID_TARGETS = [
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000067 'all', # All targets except test and clean
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -080068 'clean', # Clean up output directory
69 'docs', # Build Rust docs
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -080070 'hosttools', # Build the host tools (i.e. packetgen)
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -080071 'main', # Build the main C++ codebase
72 'prepare', # Prepare the output directory (gn gen + rust setup)
Ivan Podogovd7b6b6d2022-08-10 11:07:12 +010073 'rootcanal', # Build Rust targets for RootCanal
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -080074 'rust', # Build only the rust components + copy artifacts to output dir
75 'test', # Run the unit tests
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -080076 'utils', # Build Floss utils
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000077]
78
Abhishek Pandit-Subedi70e43042021-06-10 21:16:52 +000079# TODO(b/190750167) - Host tests are disabled until we are full bazel build
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +000080HOST_TESTS = [
Abhishek Pandit-Subedi70e43042021-06-10 21:16:52 +000081 # 'bluetooth_test_common',
82 # 'bluetoothtbd_test',
83 # 'net_test_avrcp',
84 # 'net_test_btcore',
85 # 'net_test_types',
86 # 'net_test_btm_iso',
87 # 'net_test_btpackets',
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +000088]
89
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -080090# Map of git repos to bootstrap and what commit to check them out at. None
91# values will just checkout to HEAD.
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -070092BOOTSTRAP_GIT_REPOS = {
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -080093 'platform2': ('https://chromium.googlesource.com/chromiumos/platform2', COMMON_MK_COMMIT),
94 'rust_crates': ('https://chromium.googlesource.com/chromiumos/third_party/rust_crates', None),
95 'proto_logging': ('https://android.googlesource.com/platform/frameworks/proto_logging', None),
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -070096}
97
98# List of packages required for linux build
99REQUIRED_APT_PACKAGES = [
100 'bison',
101 'build-essential',
102 'curl',
103 'debmake',
104 'flatbuffers-compiler',
105 'flex',
106 'g++-multilib',
107 'gcc-multilib',
108 'generate-ninja',
109 'gnupg',
110 'gperf',
Martin Brabhamba22adf2022-02-04 19:51:21 +0000111 'libc++abi-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700112 'libc++-dev',
113 'libdbus-1-dev',
Martin Brabham996f1502022-02-14 17:39:23 +0000114 'libdouble-conversion-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700115 'libevent-dev',
116 'libevent-dev',
117 'libflatbuffers-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700118 'libgl1-mesa-dev',
119 'libglib2.0-dev',
Martin Brabham996f1502022-02-14 17:39:23 +0000120 'libgtest-dev',
121 'libgmock-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700122 'liblz4-tool',
123 'libncurses5',
124 'libnss3-dev',
125 'libprotobuf-dev',
126 'libre2-9',
Martin Brabham996f1502022-02-14 17:39:23 +0000127 'libre2-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700128 'libssl-dev',
129 'libtinyxml2-dev',
130 'libx11-dev',
131 'libxml2-utils',
132 'ninja-build',
133 'openssl',
134 'protobuf-compiler',
135 'unzip',
136 'x11proto-core-dev',
137 'xsltproc',
138 'zip',
139 'zlib1g-dev',
140]
141
142# List of cargo packages required for linux build
143REQUIRED_CARGO_PACKAGES = ['cxxbridge-cmd']
144
145APT_PKG_LIST = ['apt', '-qq', 'list']
146CARGO_PKG_LIST = ['cargo', 'install', '--list']
147
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000148
149class UseFlags():
150
151 def __init__(self, use_flags):
152 """ Construct the use flags.
153
154 Args:
155 use_flags: List of use flags parsed from the command.
156 """
157 self.flags = {}
158
159 # Import use flags required by common-mk
160 for use in COMMON_MK_USES:
161 self.set_flag(use, False)
162
163 # Set our defaults
164 for use, value in USE_DEFAULTS.items():
165 self.set_flag(use, value)
166
167 # Set use flags - value is set to True unless the use starts with -
168 # All given use flags always override the defaults
169 for use in use_flags:
170 value = not use.startswith('-')
171 self.set_flag(use, value)
172
173 def set_flag(self, key, value=True):
174 setattr(self, key, value)
175 self.flags[key] = value
176
177
178class HostBuild():
179
180 def __init__(self, args):
181 """ Construct the builder.
182
183 Args:
184 args: Parsed arguments from ArgumentParser
185 """
186 self.args = args
187
188 # Set jobs to number of cpus unless explicitly set
189 self.jobs = self.args.jobs
190 if not self.jobs:
191 self.jobs = multiprocessing.cpu_count()
Sonny Sasaka5b7e6f22022-09-06 11:31:53 -0700192 sys.stderr.write("Number of jobs = {}\n".format(self.jobs))
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000193
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700194 # Normalize bootstrap dir and make sure it exists
195 self.bootstrap_dir = os.path.abspath(self.args.bootstrap_dir)
196 os.makedirs(self.bootstrap_dir, exist_ok=True)
197
198 # Output and platform directories are based on bootstrap
199 self.output_dir = os.path.join(self.bootstrap_dir, 'output')
200 self.platform_dir = os.path.join(self.bootstrap_dir, 'staging')
Michael Sun4940f2b2022-09-15 16:08:24 -0700201 self.bt_dir = os.path.join(self.platform_dir, 'bt')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000202 self.sysroot = self.args.sysroot
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000203 self.libdir = self.args.libdir
Abhishek Pandit-Subedia1a94d12021-11-18 13:14:04 -0800204 self.install_dir = os.path.join(self.output_dir, 'install')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000205
Michael Sun4940f2b2022-09-15 16:08:24 -0700206 assert os.path.samefile(self.bt_dir,
207 os.path.dirname(__file__)), "Please rerun bootstrap for the current project!"
208
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000209 # If default target isn't set, build everything
210 self.target = 'all'
211 if hasattr(self.args, 'target') and self.args.target:
212 self.target = self.args.target
213
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000214 target_use = self.args.use if self.args.use else []
215
216 # Unless set, always build test code
217 if not self.args.notest:
218 target_use.append('test')
219
220 self.use = UseFlags(target_use)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000221
222 # Validate platform directory
223 assert os.path.isdir(self.platform_dir), 'Platform dir does not exist'
224 assert os.path.isfile(os.path.join(self.platform_dir, '.gn')), 'Platform dir does not have .gn at root'
225
226 # Make sure output directory exists (or create it)
227 os.makedirs(self.output_dir, exist_ok=True)
228
229 # Set some default attributes
230 self.libbase_ver = None
231
232 self.configure_environ()
233
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000234 def _generate_rustflags(self):
235 """ Rustflags to include for the build.
236 """
237 rust_flags = [
238 '-L',
Martin Brabham1c24fda2021-09-16 11:19:46 -0700239 '{}/out/Default'.format(self.output_dir),
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000240 '-C',
241 'link-arg=-Wl,--allow-multiple-definition',
Sonny Sasaka87bacb62022-04-29 10:34:29 -0700242 # exclude uninteresting warnings
243 '-A improper_ctypes_definitions -A improper_ctypes -A unknown_lints',
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000244 ]
245
246 return ' '.join(rust_flags)
247
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000248 def configure_environ(self):
249 """ Configure environment variables for GN and Cargo.
250 """
251 self.env = os.environ.copy()
252
253 # Make sure cargo home dir exists and has a bin directory
254 cargo_home = os.path.join(self.output_dir, 'cargo_home')
255 os.makedirs(cargo_home, exist_ok=True)
256 os.makedirs(os.path.join(cargo_home, 'bin'), exist_ok=True)
257
258 # Configure Rust env variables
Sonny Sasaka5b7e6f22022-09-06 11:31:53 -0700259 self.custom_env = {}
260 self.custom_env['CARGO_TARGET_DIR'] = self.output_dir
261 self.custom_env['CARGO_HOME'] = os.path.join(self.output_dir, 'cargo_home')
262 self.custom_env['RUSTFLAGS'] = self._generate_rustflags()
263 self.custom_env['CXX_ROOT_PATH'] = os.path.join(self.platform_dir, 'bt')
264 self.custom_env['CROS_SYSTEM_API_ROOT'] = os.path.join(self.platform_dir, 'system_api')
265 self.custom_env['CXX_OUTDIR'] = self._gn_default_output()
266 self.env.update(self.custom_env)
267
268 def print_env(self):
269 """ Print the custom environment variables that are used in build.
270
271 Useful so that external tools can mimic the environment to be the same
272 as build.py, e.g. rust-analyzer.
273 """
274 for k, v in self.custom_env.items():
275 print("export {}='{}'".format(k, v))
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000276
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000277 def run_command(self, target, args, cwd=None, env=None):
278 """ Run command and stream the output.
279 """
280 # Set some defaults
281 if not cwd:
282 cwd = self.platform_dir
283 if not env:
284 env = self.env
285
286 log_file = os.path.join(self.output_dir, '{}.log'.format(target))
287 with open(log_file, 'wb') as lf:
288 rc = 0
289 process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
290 while True:
291 line = process.stdout.readline()
292 print(line.decode('utf-8'), end="")
293 lf.write(line)
294 if not line:
295 rc = process.poll()
296 if rc is not None:
297 break
298
299 time.sleep(0.1)
300
301 if rc != 0:
302 raise Exception("Return code is {}".format(rc))
303
304 def _get_basever(self):
305 if self.libbase_ver:
306 return self.libbase_ver
307
308 self.libbase_ver = os.environ.get('BASE_VER', '')
309 if not self.libbase_ver:
310 base_file = os.path.join(self.sysroot, 'usr/share/libchrome/BASE_VER')
311 try:
312 with open(base_file, 'r') as f:
313 self.libbase_ver = f.read().strip('\n')
314 except:
315 self.libbase_ver = 'NOT-INSTALLED'
316
317 return self.libbase_ver
318
319 def _gn_default_output(self):
320 return os.path.join(self.output_dir, 'out/Default')
321
322 def _gn_configure(self):
323 """ Configure all required parameters for platform2.
324
325 Mostly copied from //common-mk/platform2.py
326 """
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700327 clang = not self.args.no_clang
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000328
329 def to_gn_string(s):
330 return '"%s"' % s.replace('"', '\\"')
331
332 def to_gn_list(strs):
333 return '[%s]' % ','.join([to_gn_string(s) for s in strs])
334
335 def to_gn_args_args(gn_args):
336 for k, v in gn_args.items():
337 if isinstance(v, bool):
338 v = str(v).lower()
339 elif isinstance(v, list):
340 v = to_gn_list(v)
341 elif isinstance(v, six.string_types):
342 v = to_gn_string(v)
343 else:
344 raise AssertionError('Unexpected %s, %r=%r' % (type(v), k, v))
345 yield '%s=%s' % (k.replace('-', '_'), v)
346
347 gn_args = {
348 'platform_subdir': 'bt',
349 'cc': 'clang' if clang else 'gcc',
350 'cxx': 'clang++' if clang else 'g++',
351 'ar': 'llvm-ar' if clang else 'ar',
352 'pkg-config': 'pkg-config',
353 'clang_cc': clang,
354 'clang_cxx': clang,
355 'OS': 'linux',
356 'sysroot': self.sysroot,
357 'libdir': os.path.join(self.sysroot, self.libdir),
358 'build_root': self.output_dir,
359 'platform2_root': self.platform_dir,
360 'libbase_ver': self._get_basever(),
361 'enable_exceptions': os.environ.get('CXXEXCEPTIONS', 0) == '1',
362 'external_cflags': [],
Abhishek Pandit-Subedi852dc3a2022-02-14 15:12:36 -0800363 'external_cxxflags': ["-DNDEBUG"],
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000364 'enable_werror': False,
365 }
366
367 if clang:
368 # Make sure to mark the clang use flag as true
369 self.use.set_flag('clang', True)
370 gn_args['external_cxxflags'] += ['-I/usr/include/']
371
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000372 gn_args_args = list(to_gn_args_args(gn_args))
373 use_args = ['%s=%s' % (k, str(v).lower()) for k, v in self.use.flags.items()]
374 gn_args_args += ['use={%s}' % (' '.join(use_args))]
375
376 gn_args = [
377 'gn',
378 'gen',
379 ]
380
381 if self.args.verbose:
382 gn_args.append('-v')
383
384 gn_args += [
385 '--root=%s' % self.platform_dir,
386 '--args=%s' % ' '.join(gn_args_args),
387 self._gn_default_output(),
388 ]
389
Sonny Sasaka706ec3b2021-03-25 05:39:20 -0700390 if 'PKG_CONFIG_PATH' in self.env:
391 print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH'])
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000392
393 self.run_command('configure', gn_args)
394
395 def _gn_build(self, target):
396 """ Generate the ninja command for the target and run it.
397 """
398 args = ['%s:%s' % ('bt', target)]
399 ninja_args = ['ninja', '-C', self._gn_default_output()]
400 if self.jobs:
401 ninja_args += ['-j', str(self.jobs)]
402 ninja_args += args
403
404 if self.args.verbose:
405 ninja_args.append('-v')
406
407 self.run_command('build', ninja_args)
408
409 def _rust_configure(self):
410 """ Generate config file at cargo_home so we use vendored crates.
411 """
412 template = """
413 [source.systembt]
414 directory = "{}/external/rust/vendor"
415
416 [source.crates-io]
417 replace-with = "systembt"
418 local-registry = "/nonexistent"
419 """
Sonny Sasakac1335a22021-03-25 07:10:47 -0700420
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700421 if not self.args.no_vendored_rust:
Sonny Sasakac1335a22021-03-25 07:10:47 -0700422 contents = template.format(self.platform_dir)
423 with open(os.path.join(self.env['CARGO_HOME'], 'config'), 'w') as f:
424 f.write(contents)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000425
426 def _rust_build(self):
427 """ Run `cargo build` from platform2/bt directory.
428 """
Abhishek Pandit-Subedi30e57302022-10-17 13:41:09 -0700429 cmd = ['cargo', 'build']
430 if not self.args.rust_debug:
431 cmd.append('--release')
432
433 self.run_command('rust', cmd, cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000434
435 def _target_prepare(self):
436 """ Target to prepare the output directory for building.
437
438 This runs gn gen to generate all rquired files and set up the Rust
439 config properly. This will be run
440 """
441 self._gn_configure()
442 self._rust_configure()
443
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800444 def _target_hosttools(self):
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000445 """ Build the tools target in an already prepared environment.
446 """
447 self._gn_build('tools')
448
449 # Also copy bluetooth_packetgen to CARGO_HOME so it's available
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800450 shutil.copy(os.path.join(self._gn_default_output(), 'bluetooth_packetgen'),
451 os.path.join(self.env['CARGO_HOME'], 'bin'))
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000452
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800453 def _target_docs(self):
454 """Build the Rust docs."""
455 self.run_command('docs', ['cargo', 'doc'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
456
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000457 def _target_rust(self):
458 """ Build rust artifacts in an already prepared environment.
459 """
460 self._rust_build()
461
Ivan Podogovd7b6b6d2022-08-10 11:07:12 +0100462 def _target_rootcanal(self):
463 """ Build rust artifacts for RootCanal in an already prepared environment.
464 """
Abhishek Pandit-Subedi30e57302022-10-17 13:41:09 -0700465 cmd = ['cargo', 'build']
466 if not self.args.rust_debug:
467 cmd.append('--release')
468
469 self.run_command('rust', cmd, cwd=os.path.join(self.platform_dir, 'bt/tools/rootcanal'), env=self.env)
Ivan Podogovd7b6b6d2022-08-10 11:07:12 +0100470
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000471 def _target_main(self):
472 """ Build the main GN artifacts in an already prepared environment.
473 """
474 self._gn_build('all')
475
476 def _target_test(self):
477 """ Runs the host tests.
478 """
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000479 # Rust tests first
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800480 rust_test_cmd = ['cargo', 'test']
Abhishek Pandit-Subedi30e57302022-10-17 13:41:09 -0700481 if not self.args.rust_debug:
482 rust_test_cmd.append('--release')
483
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800484 if self.args.test_name:
Abhishek Pandit-Subedib04e6a92022-09-08 19:19:40 -0700485 rust_test_cmd = rust_test_cmd + [self.args.test_name, "--", "--test-threads=1", "--nocapture"]
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800486
487 self.run_command('test', rust_test_cmd, cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
Ivan Podogovd7b6b6d2022-08-10 11:07:12 +0100488 self.run_command('test', rust_test_cmd, cwd=os.path.join(self.platform_dir, 'bt/tools/rootcanal'), env=self.env)
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000489
490 # Host tests second based on host test list
491 for t in HOST_TESTS:
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800492 self.run_command('test', [os.path.join(self.output_dir, 'out/Default', t)],
493 cwd=os.path.join(self.output_dir),
494 env=self.env)
495
496 def _target_utils(self):
497 """ Builds the utility applications.
498 """
499 rust_targets = ['hcidoc']
500
501 # Build targets
502 for target in rust_targets:
503 self.run_command('utils', ['cargo', 'build', '-p', target],
504 cwd=os.path.join(self.platform_dir, 'bt'),
505 env=self.env)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000506
Abhishek Pandit-Subedia1a94d12021-11-18 13:14:04 -0800507 def _target_install(self):
508 """ Installs files required to run Floss to install directory.
509 """
510 # First make the install directory
511 prefix = self.install_dir
512 os.makedirs(prefix, exist_ok=True)
513
514 # Next save the cwd and change to install directory
515 last_cwd = os.getcwd()
516 os.chdir(prefix)
517
518 bindir = os.path.join(self.output_dir, 'debug')
519 srcdir = os.path.dirname(__file__)
520
521 install_map = [
522 {
523 'src': os.path.join(bindir, 'btadapterd'),
524 'dst': 'usr/libexec/bluetooth/btadapterd',
525 'strip': True
526 },
527 {
528 'src': os.path.join(bindir, 'btmanagerd'),
529 'dst': 'usr/libexec/bluetooth/btmanagerd',
530 'strip': True
531 },
532 {
533 'src': os.path.join(bindir, 'btclient'),
534 'dst': 'usr/local/bin/btclient',
535 'strip': True
536 },
537 ]
538
539 for v in install_map:
540 src, partial_dst, strip = (v['src'], v['dst'], v['strip'])
541 dst = os.path.join(prefix, partial_dst)
542
543 # Create dst directory first and copy file there
544 os.makedirs(os.path.dirname(dst), exist_ok=True)
545 print('Installing {}'.format(dst))
546 shutil.copy(src, dst)
547
548 # Binary should be marked for strip and no-strip option shouldn't be
549 # set. No-strip is useful while debugging.
550 if strip and not self.args.no_strip:
551 self.run_command('install', ['llvm-strip', dst])
552
553 # Put all files into a tar.gz for easier installation
554 tar_location = os.path.join(prefix, 'floss.tar.gz')
555 with tarfile.open(tar_location, 'w:gz') as tar:
556 for v in install_map:
557 tar.add(v['dst'])
558
559 print('Tarball created at {}'.format(tar_location))
560
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000561 def _target_clean(self):
562 """ Delete the output directory entirely.
563 """
564 shutil.rmtree(self.output_dir)
Abhishek Pandit-Subedia1a94d12021-11-18 13:14:04 -0800565
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700566 # Remove Cargo.lock that may have become generated
Abhishek Pandit-Subedi16617282022-09-28 10:11:10 -0700567 cargo_lock_files = [
568 os.path.join(self.platform_dir, 'bt', 'Cargo.lock'),
569 os.path.join(self.platform_dir, 'bt', 'tools', 'rootcanal', 'Cargo.lock'),
570 ]
571 for lock_file in cargo_lock_files:
572 try:
573 os.remove(lock_file)
574 print('Removed {}'.format(lock_file))
575 except FileNotFoundError:
576 pass
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000577
578 def _target_all(self):
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800579 """ Build all common targets (skipping doc, test, and clean).
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000580 """
581 self._target_prepare()
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800582 self._target_hosttools()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000583 self._target_main()
Abhishek Pandit-Subedia7b57b72021-04-01 15:33:05 -0700584 self._target_rust()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000585
586 def build(self):
587 """ Builds according to self.target
588 """
589 print('Building target ', self.target)
590
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800591 # Validate that the target is valid
592 if self.target not in VALID_TARGETS:
howardchung63187b62022-08-16 17:06:17 +0800593 print('Target {} is not valid. Must be in {}'.format(self.target, VALID_TARGETS))
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800594 return
595
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000596 if self.target == 'prepare':
597 self._target_prepare()
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800598 elif self.target == 'hosttools':
599 self._target_hosttools()
Ivan Podogovd7b6b6d2022-08-10 11:07:12 +0100600 elif self.target == 'rootcanal':
601 self._target_rootcanal()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000602 elif self.target == 'rust':
603 self._target_rust()
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800604 elif self.target == 'docs':
605 self._target_docs()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000606 elif self.target == 'main':
607 self._target_main()
608 elif self.target == 'test':
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000609 self._target_test()
610 elif self.target == 'clean':
611 self._target_clean()
Abhishek Pandit-Subedia1a94d12021-11-18 13:14:04 -0800612 elif self.target == 'install':
613 self._target_install()
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800614 elif self.target == 'utils':
615 self._target_utils()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000616 elif self.target == 'all':
617 self._target_all()
618
619
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700620class Bootstrap():
621
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800622 def __init__(self, base_dir, bt_dir, partial_staging):
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700623 """ Construct bootstrapper.
624
625 Args:
626 base_dir: Where to stage everything.
627 bt_dir: Where bluetooth source is kept (will be symlinked)
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800628 partial_staging: Whether to do a partial clone for staging.
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700629 """
630 self.base_dir = os.path.abspath(base_dir)
631 self.bt_dir = os.path.abspath(bt_dir)
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800632 self.partial_staging = partial_staging
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700633
634 # Create base directory if it doesn't already exist
635 os.makedirs(self.base_dir, exist_ok=True)
636
637 if not os.path.isdir(self.bt_dir):
638 raise Exception('{} is not a valid directory'.format(self.bt_dir))
639
640 self.git_dir = os.path.join(self.base_dir, 'repos')
641 self.staging_dir = os.path.join(self.base_dir, 'staging')
642 self.output_dir = os.path.join(self.base_dir, 'output')
643 self.external_dir = os.path.join(self.base_dir, 'staging', 'external')
644
645 self.dir_setup_complete = os.path.join(self.base_dir, '.setup-complete')
646
647 def _update_platform2(self):
648 """Updates repositories used for build."""
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -0800649 for project in BOOTSTRAP_GIT_REPOS.keys():
650 cwd = os.path.join(self.git_dir, project)
651 (repo, commit) = BOOTSTRAP_GIT_REPOS[project]
652
653 # Update to required commit when necessary or pull the latest code.
654 if commit:
655 head = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=cwd).strip()
656 if head != commit:
657 subprocess.check_call(['git', 'fetch'], cwd=cwd)
658 subprocess.check_call(['git', 'checkout', commit], cwd=cwd)
659 else:
660 subprocess.check_call(['git', 'pull'], cwd=cwd)
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700661
662 def _setup_platform2(self):
663 """ Set up platform2.
664
665 This will check out all the git repos and symlink everything correctly.
666 """
667
Abhishek Pandit-Subedib987eb62021-11-17 17:27:31 -0800668 # Create all directories we will need to use
669 for dirpath in [self.git_dir, self.staging_dir, self.output_dir, self.external_dir]:
670 os.makedirs(dirpath, exist_ok=True)
671
672 # If already set up, only update platform2
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700673 if os.path.isfile(self.dir_setup_complete):
674 print('{} already set-up. Updating instead.'.format(self.base_dir))
675 self._update_platform2()
Abhishek Pandit-Subedib987eb62021-11-17 17:27:31 -0800676 else:
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800677 clone_options = []
678 # When doing a partial staging, we use a treeless clone which allows
679 # us to access all commits but downloads things on demand. This
680 # helps speed up the initial git clone during builds but isn't good
681 # for long-term development.
682 if self.partial_staging:
683 clone_options = ['--filter=tree:0']
Abhishek Pandit-Subedib987eb62021-11-17 17:27:31 -0800684 # Check out all repos in git directory
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -0800685 for project in BOOTSTRAP_GIT_REPOS.keys():
686 (repo, commit) = BOOTSTRAP_GIT_REPOS[project]
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800687 subprocess.check_call(['git', 'clone', repo, project] + clone_options, cwd=self.git_dir)
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -0800688 # Pin to commit.
689 if commit:
690 subprocess.check_call(['git', 'checkout', commit], cwd=os.path.join(self.git_dir, project))
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700691
692 # Symlink things
693 symlinks = [
694 (os.path.join(self.git_dir, 'platform2', 'common-mk'), os.path.join(self.staging_dir, 'common-mk')),
Sonny Sasakaae9f6522022-03-28 10:31:34 -0700695 (os.path.join(self.git_dir, 'platform2', 'system_api'), os.path.join(self.staging_dir, 'system_api')),
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700696 (os.path.join(self.git_dir, 'platform2', '.gn'), os.path.join(self.staging_dir, '.gn')),
697 (os.path.join(self.bt_dir), os.path.join(self.staging_dir, 'bt')),
698 (os.path.join(self.git_dir, 'rust_crates'), os.path.join(self.external_dir, 'rust')),
699 (os.path.join(self.git_dir, 'proto_logging'), os.path.join(self.external_dir, 'proto_logging')),
700 ]
701
702 # Create symlinks
703 for pairs in symlinks:
704 (src, dst) = pairs
Martin Brabham247d80b2022-02-04 19:42:49 +0000705 try:
706 os.unlink(dst)
707 except Exception as e:
708 print(e)
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700709 os.symlink(src, dst)
710
711 # Write to setup complete file so we don't repeat this step
712 with open(self.dir_setup_complete, 'w') as f:
713 f.write('Setup complete.')
714
715 def _pretty_print_install(self, install_cmd, packages, line_limit=80):
716 """ Pretty print an install command.
717
718 Args:
719 install_cmd: Prefixed install command.
720 packages: Enumerate packages and append them to install command.
721 line_limit: Number of characters per line.
722
723 Return:
724 Array of lines to join and print.
725 """
726 install = [install_cmd]
727 line = ' '
728 # Remainder needed = space + len(pkg) + space + \
729 # Assuming 80 character lines, that's 80 - 3 = 77
730 line_limit = line_limit - 3
731 for pkg in packages:
732 if len(line) + len(pkg) < line_limit:
733 line = '{}{} '.format(line, pkg)
734 else:
735 install.append(line)
736 line = ' {} '.format(pkg)
737
738 if len(line) > 0:
739 install.append(line)
740
741 return install
742
743 def _check_package_installed(self, package, cmd, predicate):
744 """Check that the given package is installed.
745
746 Args:
747 package: Check that this package is installed.
748 cmd: Command prefix to check if installed (package appended to end)
749 predicate: Function/lambda to check if package is installed based
750 on output. Takes string output and returns boolean.
751
752 Return:
753 True if package is installed.
754 """
755 try:
756 output = subprocess.check_output(cmd + [package], stderr=subprocess.STDOUT)
757 is_installed = predicate(output.decode('utf-8'))
758 print(' {} is {}'.format(package, 'installed' if is_installed else 'missing'))
759
760 return is_installed
761 except Exception as e:
762 print(e)
763 return False
764
765 def _get_command_output(self, cmd):
766 """Runs the command and gets the output.
767
768 Args:
769 cmd: Command to run.
770
771 Return:
772 Tuple (Success, Output). Success represents if the command ran ok.
773 """
774 try:
775 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
776 return (True, output.decode('utf-8').split('\n'))
777 except Exception as e:
778 print(e)
779 return (False, "")
780
781 def _print_missing_packages(self):
782 """Print any missing packages found via apt.
783
784 This will find any missing packages necessary for build using apt and
785 print it out as an apt-get install printf.
786 """
787 print('Checking for any missing packages...')
788
789 (success, output) = self._get_command_output(APT_PKG_LIST)
790 if not success:
791 raise Exception("Could not query apt for packages.")
792
793 packages_installed = {}
794 for line in output:
795 if 'installed' in line:
796 split = line.split('/', 2)
797 packages_installed[split[0]] = True
798
799 need_packages = []
800 for pkg in REQUIRED_APT_PACKAGES:
801 if pkg not in packages_installed:
802 need_packages.append(pkg)
803
804 # No packages need to be installed
805 if len(need_packages) == 0:
806 print('+ All required packages are installed')
807 return
808
809 install = self._pretty_print_install('sudo apt-get install', need_packages)
810
811 # Print all lines so they can be run in cmdline
812 print('Missing system packages. Run the following command: ')
813 print(' \\\n'.join(install))
814
815 def _print_missing_rust_packages(self):
816 """Print any missing packages found via cargo.
817
818 This will find any missing packages necessary for build using cargo and
819 print it out as a cargo-install printf.
820 """
821 print('Checking for any missing cargo packages...')
822
823 (success, output) = self._get_command_output(CARGO_PKG_LIST)
824 if not success:
825 raise Exception("Could not query cargo for packages.")
826
827 packages_installed = {}
828 for line in output:
829 # Cargo installed packages have this format
830 # <crate name> <version>:
831 # <binary name>
832 # We only care about the crates themselves
833 if ':' not in line:
834 continue
835
836 split = line.split(' ', 2)
837 packages_installed[split[0]] = True
838
839 need_packages = []
840 for pkg in REQUIRED_CARGO_PACKAGES:
841 if pkg not in packages_installed:
842 need_packages.append(pkg)
843
844 # No packages to be installed
845 if len(need_packages) == 0:
846 print('+ All required cargo packages are installed')
847 return
848
849 install = self._pretty_print_install('cargo install', need_packages)
850 print('Missing cargo packages. Run the following command: ')
851 print(' \\\n'.join(install))
852
853 def bootstrap(self):
854 """ Bootstrap the Linux build."""
855 self._setup_platform2()
856 self._print_missing_packages()
857 self._print_missing_rust_packages()
858
859
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000860if __name__ == '__main__':
861 parser = argparse.ArgumentParser(description='Simple build for host.')
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800862 parser.add_argument('--bootstrap-dir',
863 help='Directory to run bootstrap on (or was previously run on).',
864 default="~/.floss")
865 parser.add_argument('--run-bootstrap',
866 help='Run bootstrap code to verify build env is ok to build.',
867 default=False,
868 action='store_true')
869 parser.add_argument('--print-env',
870 help='Print environment variables used for build.',
871 default=False,
872 action='store_true')
Andre Braga17ff7bc2022-06-24 22:43:18 +0000873 parser.add_argument('--no-clang', help='Don\'t use clang compiler.', default=False, action='store_true')
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800874 parser.add_argument('--no-strip',
875 help='Skip stripping binaries during install.',
876 default=False,
877 action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000878 parser.add_argument('--use', help='Set a specific use flag.')
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800879 parser.add_argument('--notest', help='Don\'t compile test code.', default=False, action='store_true')
880 parser.add_argument('--test-name', help='Run test with this string in the name.', default=None)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000881 parser.add_argument('--target', help='Run specific build target')
882 parser.add_argument('--sysroot', help='Set a specific sysroot path', default='/')
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700883 parser.add_argument('--libdir', help='Libdir - default = usr/lib', default='usr/lib')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000884 parser.add_argument('--jobs', help='Number of jobs to run', default=0, type=int)
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800885 parser.add_argument('--no-vendored-rust',
886 help='Do not use vendored rust crates',
887 default=False,
888 action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000889 parser.add_argument('--verbose', help='Verbose logs for build.')
Abhishek Pandit-Subedi30e57302022-10-17 13:41:09 -0700890 parser.add_argument('--rust-debug', help='Build Rust code as debug.', default=False, action='store_true')
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800891 parser.add_argument(
892 '--partial-staging',
893 help='Bootstrap git repositories with partial clones. Use to speed up initial git clone for automated builds.',
894 default=False,
895 action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000896 args = parser.parse_args()
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700897
898 # Make sure we get absolute path + expanded path for bootstrap directory
899 args.bootstrap_dir = os.path.abspath(os.path.expanduser(args.bootstrap_dir))
900
Andre Bragaaa11e7d2022-08-10 21:46:44 +0000901 # Possible values for machine() come from 'uname -m'
902 # Since this script only runs on Linux, x86_64 machines must have this value
903 if platform.machine() != 'x86_64':
904 raise Exception("Only x86_64 machines are currently supported by this build script.")
905
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700906 if args.run_bootstrap:
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800907 bootstrap = Bootstrap(args.bootstrap_dir, os.path.dirname(__file__), args.partial_staging)
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700908 bootstrap.bootstrap()
Sonny Sasaka5b7e6f22022-09-06 11:31:53 -0700909 elif args.print_env:
910 build = HostBuild(args)
911 build.print_env()
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700912 else:
913 build = HostBuild(args)
914 build.build()