OSDN Git Service

Merge "Use proper UUID for PBAP-PCE profile descriptor list"
[android-x86/system-bt.git] / build.py
1 #!/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
18 For building, you will first have to stage a platform directory that has the
19 following structure:
20 |-common-mk
21 |-bt
22 |-external
23 |-|-rust
24 |-|-|-vendor
25
26 The simplest way to do this is to check out platform2 to another directory (that
27 is not a subdir of this bt directory), symlink bt there and symlink the rust
28 vendor repository as well.
29 """
30 import argparse
31 import multiprocessing
32 import os
33 import shutil
34 import six
35 import subprocess
36 import sys
37
38 # Use flags required by common-mk (find -type f | grep -nE 'use[.]' {})
39 COMMON_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.
53 USE_DEFAULTS = {
54     'android': False,
55     'bt_nonstandard_codecs': False,
56     'test': False,
57 }
58
59 VALID_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
70 class 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
99 class 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
398 if __name__ == '__main__':
399     parser = argparse.ArgumentParser(description='Simple build for host.')
400     parser.add_argument('--output', help='Output directory for the build.', required=True)
401     parser.add_argument('--platform-dir', help='Directory where platform2 is staged.', required=True)
402     parser.add_argument('--clang', help='Use clang compiler.', default=False, action="store_true")
403     parser.add_argument('--use', help='Set a specific use flag.')
404     parser.add_argument('--target', help='Run specific build target')
405     parser.add_argument('--sysroot', help='Set a specific sysroot path', default='/')
406     parser.add_argument('--libdir', help='Libdir - default = usr/lib64', default='usr/lib64')
407     parser.add_argument('--use-board', help='Use a built x86 board for dependencies. Provide path.')
408     parser.add_argument('--jobs', help='Number of jobs to run', default=0, type=int)
409     parser.add_argument('--verbose', help='Verbose logs for build.')
410
411     args = parser.parse_args()
412     build = HostBuild(args)
413     build.build()