blob: ac8d265868ca0895fdb182e08022e4eb26bf0707 [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)
73 'rust', # Build only the rust components + copy artifacts to output dir
74 'test', # Run the unit tests
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -080075 'utils', # Build Floss utils
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +000076]
77
Abhishek Pandit-Subedi70e43042021-06-10 21:16:52 +000078# TODO(b/190750167) - Host tests are disabled until we are full bazel build
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +000079HOST_TESTS = [
Abhishek Pandit-Subedi70e43042021-06-10 21:16:52 +000080 # 'bluetooth_test_common',
81 # 'bluetoothtbd_test',
82 # 'net_test_avrcp',
83 # 'net_test_btcore',
84 # 'net_test_types',
85 # 'net_test_btm_iso',
86 # 'net_test_btpackets',
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +000087]
88
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -080089# Map of git repos to bootstrap and what commit to check them out at. None
90# values will just checkout to HEAD.
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -070091BOOTSTRAP_GIT_REPOS = {
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -080092 'platform2': ('https://chromium.googlesource.com/chromiumos/platform2', COMMON_MK_COMMIT),
93 'rust_crates': ('https://chromium.googlesource.com/chromiumos/third_party/rust_crates', None),
94 'proto_logging': ('https://android.googlesource.com/platform/frameworks/proto_logging', None),
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -070095}
96
97# List of packages required for linux build
98REQUIRED_APT_PACKAGES = [
99 'bison',
100 'build-essential',
101 'curl',
102 'debmake',
103 'flatbuffers-compiler',
104 'flex',
105 'g++-multilib',
106 'gcc-multilib',
107 'generate-ninja',
108 'gnupg',
109 'gperf',
Martin Brabhamba22adf2022-02-04 19:51:21 +0000110 'libc++abi-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700111 'libc++-dev',
112 'libdbus-1-dev',
Martin Brabham996f1502022-02-14 17:39:23 +0000113 'libdouble-conversion-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700114 'libevent-dev',
115 'libevent-dev',
116 'libflatbuffers-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700117 'libgl1-mesa-dev',
118 'libglib2.0-dev',
Martin Brabham996f1502022-02-14 17:39:23 +0000119 'libgtest-dev',
120 'libgmock-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700121 'liblz4-tool',
122 'libncurses5',
123 'libnss3-dev',
124 'libprotobuf-dev',
125 'libre2-9',
Martin Brabham996f1502022-02-14 17:39:23 +0000126 'libre2-dev',
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700127 'libssl-dev',
128 'libtinyxml2-dev',
129 'libx11-dev',
130 'libxml2-utils',
131 'ninja-build',
132 'openssl',
133 'protobuf-compiler',
134 'unzip',
135 'x11proto-core-dev',
136 'xsltproc',
137 'zip',
138 'zlib1g-dev',
139]
140
141# List of cargo packages required for linux build
142REQUIRED_CARGO_PACKAGES = ['cxxbridge-cmd']
143
144APT_PKG_LIST = ['apt', '-qq', 'list']
145CARGO_PKG_LIST = ['cargo', 'install', '--list']
146
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000147
148class UseFlags():
149
150 def __init__(self, use_flags):
151 """ Construct the use flags.
152
153 Args:
154 use_flags: List of use flags parsed from the command.
155 """
156 self.flags = {}
157
158 # Import use flags required by common-mk
159 for use in COMMON_MK_USES:
160 self.set_flag(use, False)
161
162 # Set our defaults
163 for use, value in USE_DEFAULTS.items():
164 self.set_flag(use, value)
165
166 # Set use flags - value is set to True unless the use starts with -
167 # All given use flags always override the defaults
168 for use in use_flags:
169 value = not use.startswith('-')
170 self.set_flag(use, value)
171
172 def set_flag(self, key, value=True):
173 setattr(self, key, value)
174 self.flags[key] = value
175
176
177class HostBuild():
178
179 def __init__(self, args):
180 """ Construct the builder.
181
182 Args:
183 args: Parsed arguments from ArgumentParser
184 """
185 self.args = args
186
187 # Set jobs to number of cpus unless explicitly set
188 self.jobs = self.args.jobs
189 if not self.jobs:
190 self.jobs = multiprocessing.cpu_count()
Sonny Sasaka5b7e6f22022-09-06 11:31:53 -0700191 sys.stderr.write("Number of jobs = {}\n".format(self.jobs))
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000192
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700193 # Normalize bootstrap dir and make sure it exists
194 self.bootstrap_dir = os.path.abspath(self.args.bootstrap_dir)
195 os.makedirs(self.bootstrap_dir, exist_ok=True)
196
197 # Output and platform directories are based on bootstrap
198 self.output_dir = os.path.join(self.bootstrap_dir, 'output')
199 self.platform_dir = os.path.join(self.bootstrap_dir, 'staging')
Michael Sun4940f2b2022-09-15 16:08:24 -0700200 self.bt_dir = os.path.join(self.platform_dir, 'bt')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000201 self.sysroot = self.args.sysroot
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000202 self.libdir = self.args.libdir
Abhishek Pandit-Subedia1a94d12021-11-18 13:14:04 -0800203 self.install_dir = os.path.join(self.output_dir, 'install')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000204
Michael Sun4940f2b2022-09-15 16:08:24 -0700205 assert os.path.samefile(self.bt_dir,
206 os.path.dirname(__file__)), "Please rerun bootstrap for the current project!"
207
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000208 # If default target isn't set, build everything
209 self.target = 'all'
210 if hasattr(self.args, 'target') and self.args.target:
211 self.target = self.args.target
212
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000213 target_use = self.args.use if self.args.use else []
214
215 # Unless set, always build test code
216 if not self.args.notest:
217 target_use.append('test')
218
219 self.use = UseFlags(target_use)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000220
221 # Validate platform directory
222 assert os.path.isdir(self.platform_dir), 'Platform dir does not exist'
223 assert os.path.isfile(os.path.join(self.platform_dir, '.gn')), 'Platform dir does not have .gn at root'
224
225 # Make sure output directory exists (or create it)
226 os.makedirs(self.output_dir, exist_ok=True)
227
228 # Set some default attributes
229 self.libbase_ver = None
230
231 self.configure_environ()
232
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000233 def _generate_rustflags(self):
234 """ Rustflags to include for the build.
235 """
236 rust_flags = [
237 '-L',
Martin Brabham1c24fda2021-09-16 11:19:46 -0700238 '{}/out/Default'.format(self.output_dir),
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000239 '-C',
240 'link-arg=-Wl,--allow-multiple-definition',
Sonny Sasaka87bacb62022-04-29 10:34:29 -0700241 # exclude uninteresting warnings
242 '-A improper_ctypes_definitions -A improper_ctypes -A unknown_lints',
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000243 ]
244
245 return ' '.join(rust_flags)
246
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000247 def configure_environ(self):
248 """ Configure environment variables for GN and Cargo.
249 """
250 self.env = os.environ.copy()
251
252 # Make sure cargo home dir exists and has a bin directory
253 cargo_home = os.path.join(self.output_dir, 'cargo_home')
254 os.makedirs(cargo_home, exist_ok=True)
255 os.makedirs(os.path.join(cargo_home, 'bin'), exist_ok=True)
256
257 # Configure Rust env variables
Sonny Sasaka5b7e6f22022-09-06 11:31:53 -0700258 self.custom_env = {}
259 self.custom_env['CARGO_TARGET_DIR'] = self.output_dir
260 self.custom_env['CARGO_HOME'] = os.path.join(self.output_dir, 'cargo_home')
261 self.custom_env['RUSTFLAGS'] = self._generate_rustflags()
262 self.custom_env['CXX_ROOT_PATH'] = os.path.join(self.platform_dir, 'bt')
263 self.custom_env['CROS_SYSTEM_API_ROOT'] = os.path.join(self.platform_dir, 'system_api')
264 self.custom_env['CXX_OUTDIR'] = self._gn_default_output()
265 self.env.update(self.custom_env)
266
267 def print_env(self):
268 """ Print the custom environment variables that are used in build.
269
270 Useful so that external tools can mimic the environment to be the same
271 as build.py, e.g. rust-analyzer.
272 """
273 for k, v in self.custom_env.items():
274 print("export {}='{}'".format(k, v))
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000275
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000276 def run_command(self, target, args, cwd=None, env=None):
277 """ Run command and stream the output.
278 """
279 # Set some defaults
280 if not cwd:
281 cwd = self.platform_dir
282 if not env:
283 env = self.env
284
285 log_file = os.path.join(self.output_dir, '{}.log'.format(target))
286 with open(log_file, 'wb') as lf:
287 rc = 0
288 process = subprocess.Popen(args, cwd=cwd, env=env, stdout=subprocess.PIPE)
289 while True:
290 line = process.stdout.readline()
291 print(line.decode('utf-8'), end="")
292 lf.write(line)
293 if not line:
294 rc = process.poll()
295 if rc is not None:
296 break
297
298 time.sleep(0.1)
299
300 if rc != 0:
301 raise Exception("Return code is {}".format(rc))
302
303 def _get_basever(self):
304 if self.libbase_ver:
305 return self.libbase_ver
306
307 self.libbase_ver = os.environ.get('BASE_VER', '')
308 if not self.libbase_ver:
309 base_file = os.path.join(self.sysroot, 'usr/share/libchrome/BASE_VER')
310 try:
311 with open(base_file, 'r') as f:
312 self.libbase_ver = f.read().strip('\n')
313 except:
314 self.libbase_ver = 'NOT-INSTALLED'
315
316 return self.libbase_ver
317
318 def _gn_default_output(self):
319 return os.path.join(self.output_dir, 'out/Default')
320
321 def _gn_configure(self):
322 """ Configure all required parameters for platform2.
323
324 Mostly copied from //common-mk/platform2.py
325 """
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700326 clang = not self.args.no_clang
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000327
328 def to_gn_string(s):
329 return '"%s"' % s.replace('"', '\\"')
330
331 def to_gn_list(strs):
332 return '[%s]' % ','.join([to_gn_string(s) for s in strs])
333
334 def to_gn_args_args(gn_args):
335 for k, v in gn_args.items():
336 if isinstance(v, bool):
337 v = str(v).lower()
338 elif isinstance(v, list):
339 v = to_gn_list(v)
340 elif isinstance(v, six.string_types):
341 v = to_gn_string(v)
342 else:
343 raise AssertionError('Unexpected %s, %r=%r' % (type(v), k, v))
344 yield '%s=%s' % (k.replace('-', '_'), v)
345
346 gn_args = {
347 'platform_subdir': 'bt',
348 'cc': 'clang' if clang else 'gcc',
349 'cxx': 'clang++' if clang else 'g++',
350 'ar': 'llvm-ar' if clang else 'ar',
351 'pkg-config': 'pkg-config',
352 'clang_cc': clang,
353 'clang_cxx': clang,
354 'OS': 'linux',
355 'sysroot': self.sysroot,
356 'libdir': os.path.join(self.sysroot, self.libdir),
357 'build_root': self.output_dir,
358 'platform2_root': self.platform_dir,
359 'libbase_ver': self._get_basever(),
360 'enable_exceptions': os.environ.get('CXXEXCEPTIONS', 0) == '1',
361 'external_cflags': [],
Abhishek Pandit-Subedi852dc3a2022-02-14 15:12:36 -0800362 'external_cxxflags': ["-DNDEBUG"],
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000363 'enable_werror': False,
364 }
365
366 if clang:
367 # Make sure to mark the clang use flag as true
368 self.use.set_flag('clang', True)
369 gn_args['external_cxxflags'] += ['-I/usr/include/']
370
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000371 gn_args_args = list(to_gn_args_args(gn_args))
372 use_args = ['%s=%s' % (k, str(v).lower()) for k, v in self.use.flags.items()]
373 gn_args_args += ['use={%s}' % (' '.join(use_args))]
374
375 gn_args = [
376 'gn',
377 'gen',
378 ]
379
380 if self.args.verbose:
381 gn_args.append('-v')
382
383 gn_args += [
384 '--root=%s' % self.platform_dir,
385 '--args=%s' % ' '.join(gn_args_args),
386 self._gn_default_output(),
387 ]
388
Sonny Sasaka706ec3b2021-03-25 05:39:20 -0700389 if 'PKG_CONFIG_PATH' in self.env:
390 print('DEBUG: PKG_CONFIG_PATH is', self.env['PKG_CONFIG_PATH'])
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000391
392 self.run_command('configure', gn_args)
393
394 def _gn_build(self, target):
395 """ Generate the ninja command for the target and run it.
396 """
397 args = ['%s:%s' % ('bt', target)]
398 ninja_args = ['ninja', '-C', self._gn_default_output()]
399 if self.jobs:
400 ninja_args += ['-j', str(self.jobs)]
401 ninja_args += args
402
403 if self.args.verbose:
404 ninja_args.append('-v')
405
406 self.run_command('build', ninja_args)
407
408 def _rust_configure(self):
409 """ Generate config file at cargo_home so we use vendored crates.
410 """
411 template = """
412 [source.systembt]
413 directory = "{}/external/rust/vendor"
414
415 [source.crates-io]
416 replace-with = "systembt"
417 local-registry = "/nonexistent"
418 """
Sonny Sasakac1335a22021-03-25 07:10:47 -0700419
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700420 if not self.args.no_vendored_rust:
Sonny Sasakac1335a22021-03-25 07:10:47 -0700421 contents = template.format(self.platform_dir)
422 with open(os.path.join(self.env['CARGO_HOME'], 'config'), 'w') as f:
423 f.write(contents)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000424
425 def _rust_build(self):
426 """ Run `cargo build` from platform2/bt directory.
427 """
Abhishek Pandit-Subedi30e57302022-10-17 13:41:09 -0700428 cmd = ['cargo', 'build']
429 if not self.args.rust_debug:
430 cmd.append('--release')
431
432 self.run_command('rust', cmd, cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000433
434 def _target_prepare(self):
435 """ Target to prepare the output directory for building.
436
437 This runs gn gen to generate all rquired files and set up the Rust
438 config properly. This will be run
439 """
440 self._gn_configure()
441 self._rust_configure()
442
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800443 def _target_hosttools(self):
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000444 """ Build the tools target in an already prepared environment.
445 """
446 self._gn_build('tools')
447
448 # Also copy bluetooth_packetgen to CARGO_HOME so it's available
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800449 shutil.copy(os.path.join(self._gn_default_output(), 'bluetooth_packetgen'),
450 os.path.join(self.env['CARGO_HOME'], 'bin'))
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000451
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800452 def _target_docs(self):
453 """Build the Rust docs."""
454 self.run_command('docs', ['cargo', 'doc'], cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
455
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000456 def _target_rust(self):
457 """ Build rust artifacts in an already prepared environment.
458 """
459 self._rust_build()
460
461 def _target_main(self):
462 """ Build the main GN artifacts in an already prepared environment.
463 """
464 self._gn_build('all')
465
466 def _target_test(self):
467 """ Runs the host tests.
468 """
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000469 # Rust tests first
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800470 rust_test_cmd = ['cargo', 'test']
Abhishek Pandit-Subedi30e57302022-10-17 13:41:09 -0700471 if not self.args.rust_debug:
472 rust_test_cmd.append('--release')
473
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800474 if self.args.test_name:
Abhishek Pandit-Subedib04e6a92022-09-08 19:19:40 -0700475 rust_test_cmd = rust_test_cmd + [self.args.test_name, "--", "--test-threads=1", "--nocapture"]
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800476
477 self.run_command('test', rust_test_cmd, cwd=os.path.join(self.platform_dir, 'bt'), env=self.env)
Abhishek Pandit-Subedid801b122021-04-10 00:41:07 +0000478
479 # Host tests second based on host test list
480 for t in HOST_TESTS:
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800481 self.run_command('test', [os.path.join(self.output_dir, 'out/Default', t)],
482 cwd=os.path.join(self.output_dir),
483 env=self.env)
484
485 def _target_utils(self):
486 """ Builds the utility applications.
487 """
488 rust_targets = ['hcidoc']
489
490 # Build targets
491 for target in rust_targets:
492 self.run_command('utils', ['cargo', 'build', '-p', target],
493 cwd=os.path.join(self.platform_dir, 'bt'),
494 env=self.env)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000495
Abhishek Pandit-Subedia1a94d12021-11-18 13:14:04 -0800496 def _target_install(self):
497 """ Installs files required to run Floss to install directory.
498 """
499 # First make the install directory
500 prefix = self.install_dir
501 os.makedirs(prefix, exist_ok=True)
502
503 # Next save the cwd and change to install directory
504 last_cwd = os.getcwd()
505 os.chdir(prefix)
506
507 bindir = os.path.join(self.output_dir, 'debug')
508 srcdir = os.path.dirname(__file__)
509
510 install_map = [
511 {
512 'src': os.path.join(bindir, 'btadapterd'),
513 'dst': 'usr/libexec/bluetooth/btadapterd',
514 'strip': True
515 },
516 {
517 'src': os.path.join(bindir, 'btmanagerd'),
518 'dst': 'usr/libexec/bluetooth/btmanagerd',
519 'strip': True
520 },
521 {
522 'src': os.path.join(bindir, 'btclient'),
523 'dst': 'usr/local/bin/btclient',
524 'strip': True
525 },
526 ]
527
528 for v in install_map:
529 src, partial_dst, strip = (v['src'], v['dst'], v['strip'])
530 dst = os.path.join(prefix, partial_dst)
531
532 # Create dst directory first and copy file there
533 os.makedirs(os.path.dirname(dst), exist_ok=True)
534 print('Installing {}'.format(dst))
535 shutil.copy(src, dst)
536
537 # Binary should be marked for strip and no-strip option shouldn't be
538 # set. No-strip is useful while debugging.
539 if strip and not self.args.no_strip:
540 self.run_command('install', ['llvm-strip', dst])
541
542 # Put all files into a tar.gz for easier installation
543 tar_location = os.path.join(prefix, 'floss.tar.gz')
544 with tarfile.open(tar_location, 'w:gz') as tar:
545 for v in install_map:
546 tar.add(v['dst'])
547
548 print('Tarball created at {}'.format(tar_location))
549
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000550 def _target_clean(self):
551 """ Delete the output directory entirely.
552 """
553 shutil.rmtree(self.output_dir)
Abhishek Pandit-Subedia1a94d12021-11-18 13:14:04 -0800554
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700555 # Remove Cargo.lock that may have become generated
Abhishek Pandit-Subedi16617282022-09-28 10:11:10 -0700556 cargo_lock_files = [
557 os.path.join(self.platform_dir, 'bt', 'Cargo.lock'),
Abhishek Pandit-Subedi16617282022-09-28 10:11:10 -0700558 ]
559 for lock_file in cargo_lock_files:
560 try:
561 os.remove(lock_file)
562 print('Removed {}'.format(lock_file))
563 except FileNotFoundError:
564 pass
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000565
566 def _target_all(self):
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800567 """ Build all common targets (skipping doc, test, and clean).
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000568 """
569 self._target_prepare()
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800570 self._target_hosttools()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000571 self._target_main()
Abhishek Pandit-Subedia7b57b72021-04-01 15:33:05 -0700572 self._target_rust()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000573
574 def build(self):
575 """ Builds according to self.target
576 """
577 print('Building target ', self.target)
578
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800579 # Validate that the target is valid
580 if self.target not in VALID_TARGETS:
howardchung63187b62022-08-16 17:06:17 +0800581 print('Target {} is not valid. Must be in {}'.format(self.target, VALID_TARGETS))
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800582 return
583
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000584 if self.target == 'prepare':
585 self._target_prepare()
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800586 elif self.target == 'hosttools':
587 self._target_hosttools()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000588 elif self.target == 'rust':
589 self._target_rust()
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800590 elif self.target == 'docs':
591 self._target_docs()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000592 elif self.target == 'main':
593 self._target_main()
594 elif self.target == 'test':
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000595 self._target_test()
596 elif self.target == 'clean':
597 self._target_clean()
Abhishek Pandit-Subedia1a94d12021-11-18 13:14:04 -0800598 elif self.target == 'install':
599 self._target_install()
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800600 elif self.target == 'utils':
601 self._target_utils()
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000602 elif self.target == 'all':
603 self._target_all()
604
605
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700606class Bootstrap():
607
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800608 def __init__(self, base_dir, bt_dir, partial_staging):
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700609 """ Construct bootstrapper.
610
611 Args:
612 base_dir: Where to stage everything.
613 bt_dir: Where bluetooth source is kept (will be symlinked)
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800614 partial_staging: Whether to do a partial clone for staging.
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700615 """
616 self.base_dir = os.path.abspath(base_dir)
617 self.bt_dir = os.path.abspath(bt_dir)
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800618 self.partial_staging = partial_staging
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700619
620 # Create base directory if it doesn't already exist
621 os.makedirs(self.base_dir, exist_ok=True)
622
623 if not os.path.isdir(self.bt_dir):
624 raise Exception('{} is not a valid directory'.format(self.bt_dir))
625
626 self.git_dir = os.path.join(self.base_dir, 'repos')
627 self.staging_dir = os.path.join(self.base_dir, 'staging')
628 self.output_dir = os.path.join(self.base_dir, 'output')
629 self.external_dir = os.path.join(self.base_dir, 'staging', 'external')
630
631 self.dir_setup_complete = os.path.join(self.base_dir, '.setup-complete')
632
633 def _update_platform2(self):
634 """Updates repositories used for build."""
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -0800635 for project in BOOTSTRAP_GIT_REPOS.keys():
636 cwd = os.path.join(self.git_dir, project)
637 (repo, commit) = BOOTSTRAP_GIT_REPOS[project]
638
639 # Update to required commit when necessary or pull the latest code.
640 if commit:
641 head = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=cwd).strip()
642 if head != commit:
643 subprocess.check_call(['git', 'fetch'], cwd=cwd)
644 subprocess.check_call(['git', 'checkout', commit], cwd=cwd)
645 else:
646 subprocess.check_call(['git', 'pull'], cwd=cwd)
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700647
648 def _setup_platform2(self):
649 """ Set up platform2.
650
651 This will check out all the git repos and symlink everything correctly.
652 """
653
Abhishek Pandit-Subedib987eb62021-11-17 17:27:31 -0800654 # Create all directories we will need to use
655 for dirpath in [self.git_dir, self.staging_dir, self.output_dir, self.external_dir]:
656 os.makedirs(dirpath, exist_ok=True)
657
658 # If already set up, only update platform2
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700659 if os.path.isfile(self.dir_setup_complete):
660 print('{} already set-up. Updating instead.'.format(self.base_dir))
661 self._update_platform2()
Abhishek Pandit-Subedib987eb62021-11-17 17:27:31 -0800662 else:
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800663 clone_options = []
664 # When doing a partial staging, we use a treeless clone which allows
665 # us to access all commits but downloads things on demand. This
666 # helps speed up the initial git clone during builds but isn't good
667 # for long-term development.
668 if self.partial_staging:
669 clone_options = ['--filter=tree:0']
Abhishek Pandit-Subedib987eb62021-11-17 17:27:31 -0800670 # Check out all repos in git directory
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -0800671 for project in BOOTSTRAP_GIT_REPOS.keys():
672 (repo, commit) = BOOTSTRAP_GIT_REPOS[project]
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800673 subprocess.check_call(['git', 'clone', repo, project] + clone_options, cwd=self.git_dir)
Abhishek Pandit-Subedi461eeab2023-01-09 10:20:17 -0800674 # Pin to commit.
675 if commit:
676 subprocess.check_call(['git', 'checkout', commit], cwd=os.path.join(self.git_dir, project))
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700677
678 # Symlink things
679 symlinks = [
680 (os.path.join(self.git_dir, 'platform2', 'common-mk'), os.path.join(self.staging_dir, 'common-mk')),
Sonny Sasakaae9f6522022-03-28 10:31:34 -0700681 (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 -0700682 (os.path.join(self.git_dir, 'platform2', '.gn'), os.path.join(self.staging_dir, '.gn')),
683 (os.path.join(self.bt_dir), os.path.join(self.staging_dir, 'bt')),
684 (os.path.join(self.git_dir, 'rust_crates'), os.path.join(self.external_dir, 'rust')),
685 (os.path.join(self.git_dir, 'proto_logging'), os.path.join(self.external_dir, 'proto_logging')),
686 ]
687
688 # Create symlinks
689 for pairs in symlinks:
690 (src, dst) = pairs
Martin Brabham247d80b2022-02-04 19:42:49 +0000691 try:
692 os.unlink(dst)
693 except Exception as e:
694 print(e)
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700695 os.symlink(src, dst)
696
697 # Write to setup complete file so we don't repeat this step
698 with open(self.dir_setup_complete, 'w') as f:
699 f.write('Setup complete.')
700
701 def _pretty_print_install(self, install_cmd, packages, line_limit=80):
702 """ Pretty print an install command.
703
704 Args:
705 install_cmd: Prefixed install command.
706 packages: Enumerate packages and append them to install command.
707 line_limit: Number of characters per line.
708
709 Return:
710 Array of lines to join and print.
711 """
712 install = [install_cmd]
713 line = ' '
714 # Remainder needed = space + len(pkg) + space + \
715 # Assuming 80 character lines, that's 80 - 3 = 77
716 line_limit = line_limit - 3
717 for pkg in packages:
718 if len(line) + len(pkg) < line_limit:
719 line = '{}{} '.format(line, pkg)
720 else:
721 install.append(line)
722 line = ' {} '.format(pkg)
723
724 if len(line) > 0:
725 install.append(line)
726
727 return install
728
729 def _check_package_installed(self, package, cmd, predicate):
730 """Check that the given package is installed.
731
732 Args:
733 package: Check that this package is installed.
734 cmd: Command prefix to check if installed (package appended to end)
735 predicate: Function/lambda to check if package is installed based
736 on output. Takes string output and returns boolean.
737
738 Return:
739 True if package is installed.
740 """
741 try:
742 output = subprocess.check_output(cmd + [package], stderr=subprocess.STDOUT)
743 is_installed = predicate(output.decode('utf-8'))
744 print(' {} is {}'.format(package, 'installed' if is_installed else 'missing'))
745
746 return is_installed
747 except Exception as e:
748 print(e)
749 return False
750
751 def _get_command_output(self, cmd):
752 """Runs the command and gets the output.
753
754 Args:
755 cmd: Command to run.
756
757 Return:
758 Tuple (Success, Output). Success represents if the command ran ok.
759 """
760 try:
761 output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
762 return (True, output.decode('utf-8').split('\n'))
763 except Exception as e:
764 print(e)
765 return (False, "")
766
767 def _print_missing_packages(self):
768 """Print any missing packages found via apt.
769
770 This will find any missing packages necessary for build using apt and
771 print it out as an apt-get install printf.
772 """
773 print('Checking for any missing packages...')
774
775 (success, output) = self._get_command_output(APT_PKG_LIST)
776 if not success:
777 raise Exception("Could not query apt for packages.")
778
779 packages_installed = {}
780 for line in output:
781 if 'installed' in line:
782 split = line.split('/', 2)
783 packages_installed[split[0]] = True
784
785 need_packages = []
786 for pkg in REQUIRED_APT_PACKAGES:
787 if pkg not in packages_installed:
788 need_packages.append(pkg)
789
790 # No packages need to be installed
791 if len(need_packages) == 0:
792 print('+ All required packages are installed')
793 return
794
795 install = self._pretty_print_install('sudo apt-get install', need_packages)
796
797 # Print all lines so they can be run in cmdline
798 print('Missing system packages. Run the following command: ')
799 print(' \\\n'.join(install))
800
801 def _print_missing_rust_packages(self):
802 """Print any missing packages found via cargo.
803
804 This will find any missing packages necessary for build using cargo and
805 print it out as a cargo-install printf.
806 """
807 print('Checking for any missing cargo packages...')
808
809 (success, output) = self._get_command_output(CARGO_PKG_LIST)
810 if not success:
811 raise Exception("Could not query cargo for packages.")
812
813 packages_installed = {}
814 for line in output:
815 # Cargo installed packages have this format
816 # :
817 #
818 # We only care about the crates themselves
819 if ':' not in line:
820 continue
821
822 split = line.split(' ', 2)
823 packages_installed[split[0]] = True
824
825 need_packages = []
826 for pkg in REQUIRED_CARGO_PACKAGES:
827 if pkg not in packages_installed:
828 need_packages.append(pkg)
829
830 # No packages to be installed
831 if len(need_packages) == 0:
832 print('+ All required cargo packages are installed')
833 return
834
835 install = self._pretty_print_install('cargo install', need_packages)
836 print('Missing cargo packages. Run the following command: ')
837 print(' \\\n'.join(install))
838
839 def bootstrap(self):
840 """ Bootstrap the Linux build."""
841 self._setup_platform2()
842 self._print_missing_packages()
843 self._print_missing_rust_packages()
844
845
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000846if __name__ == '__main__':
847 parser = argparse.ArgumentParser(description='Simple build for host.')
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800848 parser.add_argument('--bootstrap-dir',
849 help='Directory to run bootstrap on (or was previously run on).',
850 default="~/.floss")
851 parser.add_argument('--run-bootstrap',
852 help='Run bootstrap code to verify build env is ok to build.',
853 default=False,
854 action='store_true')
855 parser.add_argument('--print-env',
856 help='Print environment variables used for build.',
857 default=False,
858 action='store_true')
Andre Braga17ff7bc2022-06-24 22:43:18 +0000859 parser.add_argument('--no-clang', help='Don\'t use clang compiler.', default=False, action='store_true')
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800860 parser.add_argument('--no-strip',
861 help='Skip stripping binaries during install.',
862 default=False,
863 action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000864 parser.add_argument('--use', help='Set a specific use flag.')
Abhishek Pandit-Subedi94764142022-02-07 17:18:00 -0800865 parser.add_argument('--notest', help='Don\'t compile test code.', default=False, action='store_true')
866 parser.add_argument('--test-name', help='Run test with this string in the name.', default=None)
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000867 parser.add_argument('--target', help='Run specific build target')
868 parser.add_argument('--sysroot', help='Set a specific sysroot path', default='/')
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700869 parser.add_argument('--libdir', help='Libdir - default = usr/lib', default='usr/lib')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000870 parser.add_argument('--jobs', help='Number of jobs to run', default=0, type=int)
Abhishek Pandit-Subedi2a9a9162022-12-16 16:09:01 -0800871 parser.add_argument('--no-vendored-rust',
872 help='Do not use vendored rust crates',
873 default=False,
874 action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000875 parser.add_argument('--verbose', help='Verbose logs for build.')
Abhishek Pandit-Subedi30e57302022-10-17 13:41:09 -0700876 parser.add_argument('--rust-debug', help='Build Rust code as debug.', default=False, action='store_true')
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800877 parser.add_argument(
878 '--partial-staging',
879 help='Bootstrap git repositories with partial clones. Use to speed up initial git clone for automated builds.',
880 default=False,
881 action='store_true')
Abhishek Pandit-Subedif52cf662021-03-02 22:33:25 +0000882 args = parser.parse_args()
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700883
884 # Make sure we get absolute path + expanded path for bootstrap directory
885 args.bootstrap_dir = os.path.abspath(os.path.expanduser(args.bootstrap_dir))
886
Andre Bragaaa11e7d2022-08-10 21:46:44 +0000887 # Possible values for machine() come from 'uname -m'
888 # Since this script only runs on Linux, x86_64 machines must have this value
889 if platform.machine() != 'x86_64':
890 raise Exception("Only x86_64 machines are currently supported by this build script.")
891
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700892 if args.run_bootstrap:
Abhishek Pandit-Subedi4a56c042023-01-23 13:52:52 -0800893 bootstrap = Bootstrap(args.bootstrap_dir, os.path.dirname(__file__), args.partial_staging)
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700894 bootstrap.bootstrap()
Sonny Sasaka5b7e6f22022-09-06 11:31:53 -0700895 elif args.print_env:
896 build = HostBuild(args)
897 build.print_env()
Abhishek Pandit-Subedi0db77012021-09-20 17:54:26 -0700898 else:
899 build = HostBuild(args)
900 build.build()