Fix for x86_64 build fail
[platform/upstream/connectedhomeip.git] / third_party / pigweed / repo / pw_doctor / py / pw_doctor / doctor.py
1 #!/usr/bin/env python3
2 # Copyright 2019 The Pigweed Authors
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License"); you may not
5 # use this file except in compliance with the License. You may obtain a copy of
6 # the License at
7 #
8 #     https://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations under
14 # the License.
15 """Checks if the environment is set up correctly for Pigweed."""
16
17 import argparse
18 from concurrent import futures
19 import logging
20 import json
21 import os
22 import pathlib
23 import shutil
24 import subprocess
25 import sys
26 import tempfile
27 from typing import Callable, Iterable, List, Set
28
29 import pw_cli.plugins
30
31
32 def call_stdout(*args, **kwargs):
33     kwargs.update(stdout=subprocess.PIPE)
34     proc = subprocess.run(*args, **kwargs)
35     return proc.stdout.decode('utf-8')
36
37
38 class _Fatal(Exception):
39     pass
40
41
42 class Doctor:
43     def __init__(self, *, log: logging.Logger = None, strict: bool = False):
44         self.strict = strict
45         self.log = log or logging.getLogger(__name__)
46         self.failures: Set[str] = set()
47
48     def run(self, checks: Iterable[Callable]):
49         with futures.ThreadPoolExecutor() as executor:
50             futures.wait([
51                 executor.submit(self._run_check, c, executor) for c in checks
52             ])
53
54     def _run_check(self, check, executor):
55         ctx = DoctorContext(self, check.__name__, executor)
56         try:
57             self.log.debug('Running check %s', ctx.check)
58             check(ctx)
59             ctx.wait()
60         except _Fatal:
61             pass
62         except:  # pylint: disable=bare-except
63             self.failures.add(ctx.check)
64             self.log.exception('%s failed with an unexpected exception',
65                                check.__name__)
66
67         self.log.debug('Completed check %s', ctx.check)
68
69
70 class DoctorContext:
71     """The context object provided to each context function."""
72     def __init__(self, doctor: Doctor, check: str, executor: futures.Executor):
73         self._doctor = doctor
74         self.check = check
75         self._executor = executor
76         self._futures: List[futures.Future] = []
77
78     def submit(self, function, *args, **kwargs):
79         """Starts running the provided function in parallel."""
80         self._futures.append(
81             self._executor.submit(self._run_job, function, *args, **kwargs))
82
83     def wait(self):
84         """Waits for all parallel tasks started with submit() to complete."""
85         futures.wait(self._futures)
86         self._futures.clear()
87
88     def _run_job(self, function, *args, **kwargs):
89         try:
90             function(*args, **kwargs)
91         except _Fatal:
92             pass
93         except:  # pylint: disable=bare-except
94             self._doctor.failures.add(self.check)
95             self._doctor.log.exception(
96                 '%s failed with an unexpected exception', self.check)
97
98     def fatal(self, fmt, *args, **kwargs):
99         """Same as error() but terminates the check early."""
100         self.error(fmt, *args, **kwargs)
101         raise _Fatal()
102
103     def error(self, fmt, *args, **kwargs):
104         self._doctor.log.error(fmt, *args, **kwargs)
105         self._doctor.failures.add(self.check)
106
107     def warning(self, fmt, *args, **kwargs):
108         if self._doctor.strict:
109             self.error(fmt, *args, **kwargs)
110         else:
111             self._doctor.log.warning(fmt, *args, **kwargs)
112
113     def info(self, fmt, *args, **kwargs):
114         self._doctor.log.info(fmt, *args, **kwargs)
115
116     def debug(self, fmt, *args, **kwargs):
117         self._doctor.log.debug(fmt, *args, **kwargs)
118
119
120 def register_into(dest):
121     def decorate(func):
122         dest.append(func)
123         return func
124
125     return decorate
126
127
128 CHECKS: List[Callable] = []
129
130
131 @register_into(CHECKS)
132 def pw_plugins(ctx: DoctorContext):
133     if pw_cli.plugins.errors():
134         ctx.error('Not all pw plugins loaded successfully')
135
136
137 @register_into(CHECKS)
138 def env_os(ctx: DoctorContext):
139     """Check that the environment matches this machine."""
140     if '_PW_ACTUAL_ENVIRONMENT_ROOT' not in os.environ:
141         return
142     env_root = pathlib.Path(os.environ['_PW_ACTUAL_ENVIRONMENT_ROOT'])
143     config = env_root / 'config.json'
144     if not config.is_file():
145         return
146
147     with open(config, 'r') as ins:
148         data = json.load(ins)
149     if data['os'] != os.name:
150         ctx.error('Current OS (%s) does not match bootstrapped OS (%s)',
151                   os.name, data['os'])
152
153     # Skipping sysname and nodename in os.uname(). nodename could change
154     # based on the current network. sysname won't change, but is
155     # redundant because it's contained in release or version, and
156     # skipping it here simplifies logic.
157     uname = ' '.join(getattr(os, 'uname', lambda: ())()[2:])
158     if data['uname'] != uname:
159         ctx.warning(
160             'Current uname (%s) does not match Bootstrap uname (%s), '
161             'you may need to rerun bootstrap on this system', uname,
162             data['uname'])
163
164
165 @register_into(CHECKS)
166 def pw_root(ctx: DoctorContext):
167     """Check that environment variable PW_ROOT is set and makes sense."""
168     try:
169         root = pathlib.Path(os.environ['PW_ROOT']).resolve()
170     except KeyError:
171         ctx.fatal('PW_ROOT not set')
172
173     git_root = pathlib.Path(
174         call_stdout(['git', 'rev-parse', '--show-toplevel'], cwd=root).strip())
175     git_root = git_root.resolve()
176     if root != git_root:
177         ctx.error('PW_ROOT (%s) != `git rev-parse --show-toplevel` (%s)', root,
178                   git_root)
179
180
181 @register_into(CHECKS)
182 def git_hook(ctx: DoctorContext):
183     """Check that presubmit git hook is installed."""
184     if not os.environ.get('PW_ENABLE_PRESUBMIT_HOOK_WARNING'):
185         return
186
187     try:
188         root = pathlib.Path(os.environ['PW_ROOT'])
189     except KeyError:
190         return  # This case is handled elsewhere.
191
192     hook = root / '.git' / 'hooks' / 'pre-push'
193     if not os.path.isfile(hook):
194         ctx.info('Presubmit hook not installed, please run '
195                  "'pw presubmit --install' before pushing changes.")
196
197
198 @register_into(CHECKS)
199 def python_version(ctx: DoctorContext):
200     """Check the Python version is correct."""
201     actual = sys.version_info
202     expected = (3, 8)
203     if (actual[0:2] < expected or actual[0] != expected[0]
204             or actual[0:2] > expected):
205         # If we get the wrong version but it still came from CIPD print a
206         # warning but give it a pass.
207         if 'chromium' in sys.version:
208             ctx.warning('Python %d.%d.x expected, got Python %d.%d.%d',
209                         *expected, *actual[0:3])
210         else:
211             ctx.error('Python %d.%d.x required, got Python %d.%d.%d',
212                       *expected, *actual[0:3])
213
214
215 @register_into(CHECKS)
216 def virtualenv(ctx: DoctorContext):
217     """Check that we're in the correct virtualenv."""
218     try:
219         venv_path = pathlib.Path(os.environ['VIRTUAL_ENV']).resolve()
220     except KeyError:
221         ctx.error('VIRTUAL_ENV not set')
222         return
223
224     # When running in LUCI we might not have gone through the normal environment
225     # setup process, so we need to skip the rest of this step.
226     if 'LUCI_CONTEXT' in os.environ:
227         return
228
229     var = 'PW_ROOT'
230     if '_PW_ACTUAL_ENVIRONMENT_ROOT' in os.environ:
231         var = '_PW_ACTUAL_ENVIRONMENT_ROOT'
232     root = pathlib.Path(os.environ[var]).resolve()
233
234     if root not in venv_path.parents:
235         ctx.error('VIRTUAL_ENV (%s) not inside %s (%s)', venv_path, var, root)
236         ctx.error('\n'.join(os.environ.keys()))
237
238
239 @register_into(CHECKS)
240 def cipd(ctx: DoctorContext):
241     """Check cipd is set up correctly and in use."""
242     if os.environ.get('PW_DOCTOR_SKIP_CIPD_CHECKS'):
243         return
244
245     cipd_path = 'pigweed'
246
247     cipd_exe = shutil.which('cipd')
248     if not cipd_exe:
249         ctx.fatal('cipd not in PATH (%s)', os.environ['PATH'])
250
251     temp = tempfile.NamedTemporaryFile(prefix='cipd', delete=False)
252     subprocess.run(['cipd', 'acl-check', '-json-output', temp.name, cipd_path],
253                    stdout=subprocess.PIPE)
254     if not json.load(temp)['result']:
255         ctx.fatal(
256             "can't access %s CIPD directory, have you run "
257             "'cipd auth-login'?", cipd_path)
258
259     commands_expected_from_cipd = [
260         'arm-none-eabi-gcc',
261         'gn',
262         'ninja',
263         'protoc',
264     ]
265
266     # TODO(mohrr) get these tools in CIPD for Windows.
267     if os.name == 'posix':
268         commands_expected_from_cipd += [
269             'bazel',
270             'bloaty',
271             'clang++',
272         ]
273
274     for command in commands_expected_from_cipd:
275         path = shutil.which(command)
276         if path is None:
277             ctx.error('could not find %s in PATH (%s)', command,
278                       os.environ['PATH'])
279         elif 'cipd' not in path:
280             ctx.warning('not using %s from cipd, got %s (path is %s)', command,
281                         path, os.environ['PATH'])
282
283
284 @register_into(CHECKS)
285 def cipd_versions(ctx: DoctorContext):
286     """Check cipd tool versions are current."""
287
288     if os.environ.get('PW_DOCTOR_SKIP_CIPD_CHECKS'):
289         return
290
291     try:
292         root = pathlib.Path(os.environ['PW_ROOT']).resolve()
293     except KeyError:
294         return  # This case is handled elsewhere.
295
296     if 'PW_PIGWEED_CIPD_INSTALL_DIR' not in os.environ:
297         ctx.error('PW_PIGWEED_CIPD_INSTALL_DIR not set')
298     cipd_dir = pathlib.Path(os.environ['PW_PIGWEED_CIPD_INSTALL_DIR'])
299
300     versions_path = cipd_dir / '.versions'
301     # Deliberately not checking luci.json--it's not required to be up-to-date.
302     json_path = root.joinpath('pw_env_setup', 'py', 'pw_env_setup',
303                               'cipd_setup', 'pigweed.json')
304
305     def check_cipd(package):
306         ctx.debug('checking version of %s', package['path'])
307         name = [
308             part for part in package['path'].split('/') if '{' not in part
309         ][-1]
310         path = versions_path.joinpath(f'{name}.cipd_version')
311         if not path.is_file():
312             ctx.debug('no version file')
313             return
314
315         with path.open() as ins:
316             installed = json.load(ins)
317
318         describe = (
319             'cipd',
320             'describe',
321             installed['package_name'],
322             '-version',
323             installed['instance_id'],
324         )
325         ctx.debug('%s', ' '.join(describe))
326         output_raw = subprocess.check_output(describe).decode()
327         ctx.debug('output: %r', output_raw)
328         output = output_raw.split()
329
330         for tag in package['tags']:
331             if tag not in output:
332                 ctx.error(
333                     'CIPD package %s is out of date, please rerun bootstrap',
334                     installed['package_name'])
335
336     for package in json.loads(json_path.read_text()):
337         ctx.submit(check_cipd, package)
338
339
340 def run_doctor(strict=False, checks=None):
341     """Run all the Check subclasses defined in this file."""
342
343     if checks is None:
344         checks = tuple(CHECKS)
345
346     doctor = Doctor(strict=strict)
347     doctor.log.debug('Doctor running %d checks...', len(checks))
348
349     doctor.run(checks)
350
351     if doctor.failures:
352         doctor.log.info('Failed checks: %s', ', '.join(doctor.failures))
353     else:
354         doctor.log.info('Environment passes all checks!')
355     return len(doctor.failures)
356
357
358 def main() -> int:
359     """Check that the environment is set up correctly for Pigweed."""
360     parser = argparse.ArgumentParser(description=__doc__)
361     parser.add_argument(
362         '--strict',
363         action='store_true',
364         help='Run additional checks.',
365     )
366
367     return run_doctor(**vars(parser.parse_args()))
368
369
370 if __name__ == '__main__':
371     # By default, display log messages like a simple print statement.
372     logging.basicConfig(format='%(message)s', level=logging.INFO)
373     sys.exit(main())