| #!/usr/bin/env -S python3 -B |
| |
| # Copyright (c) 2021 Project CHIP 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 |
| # |
| # http://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. |
| |
| import json |
| import logging |
| import os |
| import shlex |
| import sys |
| |
| import click |
| import coloredlogs |
| from builders.builder import BuilderOptions, BuildProfile |
| from runner import PrintOnlyRunner, ShellRunner |
| from runner.shell import SubcommandException |
| |
| import build |
| |
| sys.path.append(os.path.abspath(os.path.dirname(__file__))) |
| |
| log = logging.getLogger(__name__) |
| |
| # Supported log levels, mapping string values required for argument |
| # parsing into logging constants |
| __LOG_LEVELS__ = { |
| 'debug': logging.DEBUG, |
| 'info': logging.INFO, |
| 'warn': logging.WARNING, |
| 'fatal': logging.FATAL, |
| } |
| |
| |
| def ValidateRepoPath(context, parameter, value): |
| """ |
| Validates that the given path looks like a valid chip repository checkout. |
| """ |
| if value.startswith('/TEST/'): |
| # Hackish command to allow for unit testing |
| return value |
| |
| for name in ['BUILD.gn', '.gn', os.path.join('scripts', 'bootstrap.sh')]: |
| expected_file = os.path.join(value, name) |
| if not os.path.exists(expected_file): |
| raise click.BadParameter( |
| ("'%s' does not look like a valid repository path: " |
| "%s not found.") % (value, expected_file)) |
| return value |
| |
| |
| def ValidateTargetNames(context, parameter, values): |
| """ |
| Validates that the given target name is valid. |
| """ |
| for value in values: |
| if not any(target.StringIntoTargetParts(value.lower()) |
| for target in build.targets.BUILD_TARGETS): |
| raise click.BadParameter( |
| "'%s' is not a valid target name." % value) |
| return values |
| |
| |
| @click.group(chain=True) |
| @click.option( |
| '--log-level', |
| default='INFO', |
| type=click.Choice(__LOG_LEVELS__.keys(), case_sensitive=False), |
| help='Determines the verbosity of script output.') |
| @click.option( |
| '--verbose', |
| default=False, |
| is_flag=True, |
| help='Pass verbose flag to ninja.') |
| @click.option( |
| '--quiet', |
| default=False, |
| is_flag=True, |
| help='Pass quiet flag to ninja.') |
| @click.option( |
| '--target', |
| default=[], |
| multiple=True, |
| callback=ValidateTargetNames, |
| help='Build target(s)') |
| @click.option( |
| "--build-profile", |
| default=BuildProfile.DEFAULT, |
| type=click.Choice(BuildProfile, case_sensitive=False), |
| help="The build profile to use.") |
| @click.option( |
| '--enable-link-map-file', |
| default=False, |
| is_flag=True, |
| help='Enable generation of link map files.') |
| @click.option( |
| '--enable-flashbundle', |
| default=False, |
| is_flag=True, |
| help='Also generate the flashbundles for the app.') |
| @click.option( |
| '--repo', |
| default='.', |
| callback=ValidateRepoPath, |
| help='Path to the root of the CHIP SDK repository checkout.') |
| @click.option( |
| '--out-prefix', |
| default='./out', |
| type=click.Path(file_okay=False, resolve_path=True), |
| help='Prefix for the generated file output.') |
| @click.option( |
| '--ninja-jobs', |
| type=int, |
| is_flag=False, |
| flag_value=0, |
| default=None, |
| help='Number of ninja jobs') |
| @click.option( |
| '--concurrent-generation', |
| type=click.IntRange(min=0), |
| default=0, |
| help='Number of concurrent generation jobs. If 0, use number of CPU cores.') |
| @click.option( |
| '--concurrent-builders', |
| type=click.IntRange(min=1), |
| default=1, |
| help='Number of concurrent builders. If greater than 1, count of Ninja jobs is scaled down accordingly') |
| @click.option( |
| '--pregen-dir', |
| default=None, |
| type=click.Path(file_okay=False, resolve_path=True), |
| help='Directory where generated files have been pre-generated.') |
| @click.option( |
| '--clean', |
| default=False, |
| is_flag=True, |
| help='Clean output directory before running the command') |
| @click.option( |
| '--dry-run', |
| default=False, |
| is_flag=True, |
| help='Only print out shell commands that would be executed') |
| @click.option( |
| '--dry-run-output', |
| default="-", |
| type=click.File("wt"), |
| help='Where to write the dry run output') |
| @click.option( |
| '--log-timestamps/--no-log-timestamps', |
| default=True, |
| help='Show timestamps in log output') |
| @click.option( |
| '--pw-command-launcher', |
| default=None, |
| help=( |
| 'Set pigweed command launcher. E.g.: "--pw-command-launcher=ccache" ' |
| 'for using ccache when building examples.')) |
| @click.pass_context |
| def main(context, log_level, verbose, quiet, target, build_profile, enable_link_map_file, repo, out_prefix, ninja_jobs, |
| concurrent_generation: int, concurrent_builders: int, pregen_dir, clean, dry_run, dry_run_output, enable_flashbundle, |
| log_timestamps, pw_command_launcher): |
| # Ensures somewhat pretty logging of what is going on |
| if log_timestamps: |
| log_fmt = '%(asctime)s.%(msecs)03d %(levelname)-7s %(threadName)s: %(message)s' |
| else: |
| log_fmt = '%(levelname)-7s %(threadName)s: %(message)s' |
| coloredlogs.install(level=__LOG_LEVELS__[log_level], fmt=log_fmt) |
| |
| if 'PW_PROJECT_ROOT' not in os.environ: |
| raise click.UsageError(""" |
| PW_PROJECT_ROOT not set in the current environment. |
| |
| Please make sure you `source scripts/bootstrap.sh` or `source scripts/activate.sh` |
| before running this script. |
| """.strip()) |
| |
| if dry_run: |
| runner = PrintOnlyRunner(dry_run_output, root=repo) |
| |
| if concurrent_generation > 1: |
| log.warning( |
| "Concurrent generation is set to %d, but dry-run mode is enabled. " |
| "Ignoring concurrent generation and running in single-threaded mode.", |
| concurrent_generation) |
| concurrent_generation = 1 |
| |
| if concurrent_builders > 1: |
| log.warning( |
| "Concurrent builders is set to %d, but dry-run mode is enabled. " |
| "Ignoring concurrent builders and running in single-threaded mode.", |
| concurrent_builders) |
| concurrent_builders = 1 |
| else: |
| runner = ShellRunner(root=repo) |
| |
| context.obj = build.Context( |
| repository_path=repo, output_prefix=out_prefix, verbose=verbose, quiet=quiet, ninja_jobs=ninja_jobs, |
| concurrent_generation=concurrent_generation, concurrent_builders=concurrent_builders, runner=runner |
| ) |
| |
| requested_targets = {t.lower() for t in target} |
| context.obj.SetupBuilders(targets=requested_targets, options=BuilderOptions( |
| build_profile=build_profile, |
| enable_link_map_file=enable_link_map_file, |
| enable_flashbundle=enable_flashbundle, |
| pw_command_launcher=pw_command_launcher, |
| pregen_dir=pregen_dir, |
| )) |
| |
| if clean: |
| context.obj.CleanOutputDirectories() |
| |
| |
| @main.command( |
| 'gen', help='Generate ninja/makefiles (but does not run the compilation)') |
| @click.pass_context |
| def cmd_generate(context): |
| context.obj.Generate() |
| |
| |
| @main.command( |
| 'targets', |
| help=('Lists the targets that can be used with the build and gen commands')) |
| @click.option( |
| '--format', 'format_type', |
| default='summary', |
| type=click.Choice(['summary', 'expanded', 'json', 'completion'], case_sensitive=False), |
| help=""" |
| summary - list of shorthand strings summarizing the available targets; |
| |
| expanded - list all possible targets rather than the shorthand string; |
| |
| json - a JSON representation of the available targets; |
| |
| completion - a list of strings suitable for shell completion; |
| """) |
| @click.argument('COMPLETION-PREFIX', default='') |
| @click.pass_context |
| def cmd_targets(context, format_type, completion_prefix): |
| if format_type == 'expanded': |
| build.target.report_rejected_parts = False |
| for target in build.targets.BUILD_TARGETS: |
| for s in target.AllVariants(): |
| print(s) |
| elif format_type == 'json': |
| print(json.dumps([target.ToDict() for target in build.targets.BUILD_TARGETS], indent=4)) |
| elif format_type == 'completion': |
| build.target.report_rejected_parts = False |
| for target in build.targets.BUILD_TARGETS: |
| for s in target.CompletionStrings(completion_prefix): |
| print(s) |
| else: |
| for target in build.targets.BUILD_TARGETS: |
| print(target.HumanString()) |
| |
| |
| @main.command('build', help='generate and run ninja/make as needed to compile') |
| @click.option( |
| '--copy-artifacts-to', |
| default=None, |
| type=click.Path(file_okay=False, resolve_path=True), |
| help='Prefix for the generated file output.') |
| @click.option( |
| '--create-archives', |
| default=None, |
| type=click.Path(file_okay=False, resolve_path=True), |
| help='Prefix of compressed archives of the generated files.') |
| @click.pass_context |
| def cmd_build(context, copy_artifacts_to, create_archives): |
| try: |
| context.obj.Build() |
| except SubcommandException as e: |
| log.error("Command '%s' failed with error code %d", shlex.join(e.command), e.returncode) |
| sys.exit(1) |
| |
| if copy_artifacts_to: |
| context.obj.CopyArtifactsTo(copy_artifacts_to) |
| |
| if create_archives: |
| context.obj.CreateArtifactArchives(create_archives) |
| |
| |
| if __name__ == '__main__': |
| main(auto_envvar_prefix='CHIP') |