blob: d7b94d1b3d826f9fd303c15064b905df3b992b11 [file] [log] [blame]
# Copyright 2022 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Build a Pigweed Project.
Usage examples:
# Build pw_run_tests.modules in the out/cmake directory
pw build -C out/cmake pw_run_tests.modules
# Build the default target in out/ and pw_apps in out/cmake
pw build -C out -C out/cmake pw_apps
# Find a directory and build python.tests, and build pw_apps in out/cmake
pw build python.tests -C out/cmake pw_apps
"""
import argparse
import os
import logging
import shlex
import sys
import subprocess
from typing import (
Callable,
Dict,
Generator,
List,
NoReturn,
Optional,
Sequence,
NamedTuple,
)
import pw_cli.log
import pw_cli.env
from pw_build.build_recipe import BuildRecipe, create_build_recipes
from pw_build.project_builder_prefs import ProjectBuilderPrefs
from pw_build.project_builder_argparse import add_project_builder_arguments
_COLOR = pw_cli.color.colors()
_LOG = logging.getLogger('pw_build')
PASS_MESSAGE = """
██████╗ █████╗ ███████╗███████╗██╗
██╔══██╗██╔══██╗██╔════╝██╔════╝██║
██████╔╝███████║███████╗███████╗██║
██╔═══╝ ██╔══██║╚════██║╚════██║╚═╝
██║ ██║ ██║███████║███████║██╗
╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝
"""
# Pick a visually-distinct font from "PASS" to ensure that readers can't
# possibly mistake the difference between the two states.
FAIL_MESSAGE = """
▄██████▒░▄▄▄ ██▓ ░██▓
▓█▓ ░▒████▄ ▓██▒ ░▓██▒
▒████▒ ░▒█▀ ▀█▄ ▒██▒ ▒██░
░▓█▒ ░░██▄▄▄▄██ ░██░ ▒██░
░▒█░ ▓█ ▓██▒░██░░ ████████▒
▒█░ ▒▒ ▓▒█░░▓ ░ ▒░▓ ░
░▒ ▒ ▒▒ ░ ▒ ░░ ░ ▒ ░
░ ░ ░ ▒ ▒ ░ ░ ░
░ ░ ░ ░ ░
"""
class ProjectBuilderCharset(NamedTuple):
slug_ok: str
slug_fail: str
ASCII_CHARSET = ProjectBuilderCharset(_COLOR.green('OK '), _COLOR.red('FAIL'))
EMOJI_CHARSET = ProjectBuilderCharset('✔️ ', '💥')
def _exit(*args) -> NoReturn:
_LOG.critical(*args)
sys.exit(1)
def _exit_due_to_interrupt() -> NoReturn:
# To keep the log lines aligned with each other in the presence of
# a '^C' from the keyboard interrupt, add a newline before the log.
print()
_LOG.info('Got Ctrl-C; exiting...')
sys.exit(1)
def _execute_command(command: list, env: dict) -> bool:
print()
current_build = subprocess.run(command, env=env, errors='replace')
print()
return current_build.returncode == 0
class ProjectBuilder:
"""Pigweed Project Builder"""
def __init__(
self,
build_recipes: Sequence[BuildRecipe],
jobs: Optional[int] = None,
banners: bool = True,
keep_going: bool = False,
abort_callback: Callable = _exit,
execute_command: Callable[[List, Dict], bool] = _execute_command,
charset: ProjectBuilderCharset = ASCII_CHARSET,
colors: bool = True,
):
self.colors = colors
self.charset: ProjectBuilderCharset = charset
self.abort_callback = abort_callback
self.execute_command = execute_command
self.banners = banners
self.build_recipes = build_recipes
self.extra_ninja_args = [] if jobs is None else ['-j', f'{jobs}']
if keep_going:
self.extra_ninja_args.extend(['-k', '0'])
def __len__(self) -> int:
return len(self.build_recipes)
def __getitem__(self, index: int) -> BuildRecipe:
return self.build_recipes[index]
def __iter__(self) -> Generator[BuildRecipe, None, None]:
return (build_recipe for build_recipe in self.build_recipes)
def run_build(self,
cfg: BuildRecipe,
env: Dict,
index_message: Optional[str] = '') -> bool:
"""Run a single build config."""
if self.colors:
# Force colors in Pigweed subcommands run through the watcher.
env['PW_USE_COLOR'] = '1'
# Force Ninja to output ANSI colors
env['CLICOLOR_FORCE'] = '1'
build_succeded = False
for command_step in cfg.steps:
command_args = command_step.get_args(
additional_build_args=self.extra_ninja_args)
_LOG.info('%s Running ==> %s', index_message,
' '.join(shlex.quote(arg) for arg in command_args))
# Verify that the build output directories exist.
if command_step.build_system_command is not None and (
not cfg.build_dir.is_dir()):
self.abort_callback('Build directory does not exist: %s',
cfg.build_dir)
build_succeded = self.execute_command(command_args, env)
# Don't run further steps if a command fails.
if not build_succeded:
break
return build_succeded
def print_build_summary(self,
builds_succeeded: List[bool],
cancelled: bool = False) -> None:
"""Print build results summary table."""
if not cancelled:
_LOG.info('')
_LOG.info(' .------------------------------------')
_LOG.info(' |')
for (succeeded, cmd) in zip(builds_succeeded,
[str(cfg) for cfg in self]):
slug = (self.charset.slug_ok
if succeeded else self.charset.slug_fail)
_LOG.info(' | %s %s', slug, cmd)
_LOG.info(' |')
_LOG.info(" '------------------------------------")
else:
# Build was interrupted.
_LOG.info('')
_LOG.info(' .------------------------------------')
_LOG.info(' |')
_LOG.info(' | %s- interrupted', self.charset.slug_fail)
_LOG.info(' |')
_LOG.info(" '------------------------------------")
# Show a large color banner for the overall result.
if self.banners:
if all(builds_succeeded) and not cancelled:
for line in PASS_MESSAGE.splitlines():
_LOG.info(_COLOR.green(line))
else:
for line in FAIL_MESSAGE.splitlines():
_LOG.info(_COLOR.red(line))
def main() -> None:
"""Build a Pigweed Project."""
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser = add_project_builder_arguments(parser)
args = parser.parse_args()
pw_env = pw_cli.env.pigweed_environment()
if pw_env.PW_EMOJI:
charset = EMOJI_CHARSET
else:
charset = ASCII_CHARSET
prefs = ProjectBuilderPrefs(
load_argparse_arguments=add_project_builder_arguments)
prefs.apply_command_line_args(args)
build_recipes = create_build_recipes(prefs)
project_builder = ProjectBuilder(
build_recipes=build_recipes,
jobs=args.jobs,
banners=args.banners,
keep_going=args.keep_going,
charset=charset,
)
pw_cli.log.install(
level=logging.DEBUG if args.debug_logging else logging.INFO,
use_color=args.colors,
hide_timestamp=False,
)
builds_succeeded = []
num_builds = len(project_builder)
_LOG.info('Starting build with %d directories', num_builds)
env = os.environ.copy()
for i, cfg in enumerate(project_builder, 1):
index = f'[{i}/{num_builds}]'
build_start_msg = '{} Starting {}'.format(index, cfg)
_LOG.info(build_start_msg)
try:
builds_succeeded.append(
project_builder.run_build(cfg, env, index_message=index))
# Ctrl-C on Unix generates KeyboardInterrupt
# Ctrl-Z on Windows generates EOFError
except (KeyboardInterrupt, EOFError):
_exit_due_to_interrupt()
if builds_succeeded[-1]:
level = logging.INFO
tag = '(OK)'
else:
level = logging.ERROR
tag = '(FAIL)'
_LOG.log(level, '%s Finished %s %s', index, cfg, tag)
project_builder.print_build_summary(builds_succeeded)
if __name__ == '__main__':
main()