blob: e6fa77d93a34890c26fae0c8147cc7ecf20e3eb2 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2020 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.
"""Command line interface for arduino_builder."""
import argparse
import json
import logging
import os
import pprint
import shlex
import subprocess
import sys
from collections import OrderedDict
from pathlib import Path
from typing import List
from pw_arduino_build import core_installer, log
from pw_arduino_build.builder import ArduinoBuilder
from pw_arduino_build.file_operations import decode_file_json
_LOG = logging.getLogger(__name__)
_pretty_print = pprint.PrettyPrinter(indent=1, width=120).pprint
_pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
class MissingArduinoCore(Exception):
"""Exception raised when an Arduino core can not be found."""
def list_boards_command(unused_args, builder):
# list-boards subcommand
# (does not need a selected board or default menu options)
# TODO(tonymd): Print this sorted with auto-ljust columns
longest_name_length = 0
for board_name, board_dict in builder.board.items():
if len(board_name) > longest_name_length:
longest_name_length = len(board_name)
longest_name_length += 2
print("Board Name".ljust(longest_name_length), "Description")
for board_name, board_dict in builder.board.items():
print(board_name.ljust(longest_name_length), board_dict['name'])
sys.exit(0)
def list_menu_options_command(args, builder):
# List all menu options for the selected board.
builder.select_board(args.board)
print("All Options")
all_options, all_column_widths = builder.get_menu_options()
separator = "-" * (all_column_widths[0] + all_column_widths[1] + 2)
print(separator)
for name, description in all_options:
print(name.ljust(all_column_widths[0] + 1), description)
print("\nDefault Options")
print(separator)
menu_options, unused_col_widths = builder.get_default_menu_options()
for name, description in menu_options:
print(name.ljust(all_column_widths[0] + 1), description)
def show_command_print_string_list(args, string_list: List[str]):
if string_list:
join_token = "\n" if args.delimit_with_newlines else " "
print(join_token.join(string_list))
def show_command_print_flag_string(args, flag_string):
if args.delimit_with_newlines:
flag_string_with_newlines = shlex.split(flag_string)
print("\n".join(flag_string_with_newlines))
else:
print(flag_string)
def subtract_flags(flag_list_a: List[str],
flag_list_b: List[str]) -> List[str]:
"""Given two sets of flags return flags in a that are not in b."""
flag_counts = OrderedDict() # type: OrderedDict[str, int]
for flag in flag_list_a + flag_list_b:
flag_counts[flag] = flag_counts.get(flag, 0) + 1
return [flag for flag in flag_list_a if flag_counts.get(flag, 0) == 1]
def run_command_lines(args, command_lines: List[str]):
for command_line in command_lines:
if not args.quiet:
print(command_line)
# TODO(tonymd): Exit with sub command exit code.
command_line_args = shlex.split(command_line)
process = subprocess.run(command_line_args,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
if process.returncode != 0:
_LOG.error('Command failed with exit code %d.', process.returncode)
_LOG.error('Full command:')
_LOG.error('')
_LOG.error(' %s', command_line)
_LOG.error('')
_LOG.error('Process output:')
print(flush=True)
sys.stdout.buffer.write(process.stdout)
print(flush=True)
_LOG.error('')
def run_command(args, builder):
"""Run sub command function.
Runs Arduino recipes.
"""
if args.run_prebuilds:
run_command_lines(args, builder.get_prebuild_steps())
if args.run_link:
line = builder.get_link_line()
archive_file_path = args.run_link[0] # pylint: disable=unused-variable
object_files = args.run_link[1:]
line = line.replace("{object_files}", " ".join(object_files), 1)
run_command_lines(args, [line])
if args.run_objcopy:
run_command_lines(args, builder.get_objcopy_steps())
if args.run_postbuilds:
run_command_lines(args, builder.get_postbuild_steps())
if args.run_upload_command:
command = builder.get_upload_line(args.run_upload_command,
args.serial_port)
run_command_lines(args, [command])
# pylint: disable=too-many-branches
def show_command(args, builder):
"""Show sub command function.
Prints compiler info and flags.
"""
if args.cc_binary:
print(builder.get_cc_binary())
elif args.cxx_binary:
print(builder.get_cxx_binary())
elif args.objcopy_binary:
print(builder.get_objcopy_binary())
elif args.ar_binary:
print(builder.get_ar_binary())
elif args.size_binary:
print(builder.get_size_binary())
elif args.c_compile:
print(builder.get_c_compile_line())
elif args.cpp_compile:
print(builder.get_cpp_compile_line())
elif args.link:
print(builder.get_link_line())
elif args.objcopy:
print(builder.get_objcopy(args.objcopy))
elif args.objcopy_flags:
objcopy_flags = builder.get_objcopy_flags(args.objcopy_flags)
show_command_print_flag_string(args, objcopy_flags)
elif args.c_flags:
cflags = builder.get_c_flags()
show_command_print_flag_string(args, cflags)
elif args.s_flags:
sflags = builder.get_s_flags()
show_command_print_flag_string(args, sflags)
elif args.s_only_flags:
s_only_flags = subtract_flags(shlex.split(builder.get_s_flags()),
shlex.split(builder.get_c_flags()))
show_command_print_flag_string(args, " ".join(s_only_flags))
elif args.cpp_flags:
cppflags = builder.get_cpp_flags()
show_command_print_flag_string(args, cppflags)
elif args.cpp_only_flags:
cpp_only_flags = subtract_flags(shlex.split(builder.get_cpp_flags()),
shlex.split(builder.get_c_flags()))
show_command_print_flag_string(args, " ".join(cpp_only_flags))
elif args.ld_flags:
ldflags = builder.get_ld_flags()
show_command_print_flag_string(args, ldflags)
elif args.ld_libs:
show_command_print_flag_string(args, builder.get_ld_libs())
elif args.ld_lib_names:
show_command_print_flag_string(args,
builder.get_ld_libs(name_only=True))
elif args.ar_flags:
ar_flags = builder.get_ar_flags()
show_command_print_flag_string(args, ar_flags)
elif args.core_path:
print(builder.get_core_path())
elif args.prebuilds:
show_command_print_string_list(args, builder.get_prebuild_steps())
elif args.postbuilds:
show_command_print_string_list(args, builder.get_postbuild_steps())
elif args.upload_command:
print(builder.get_upload_line(args.upload_command, args.serial_port))
elif args.upload_tools:
tools = builder.get_upload_tool_names()
for tool_name in tools:
print(tool_name)
elif args.library_include_dirs:
show_command_print_string_list(args, builder.library_include_dirs())
elif args.library_includes:
show_command_print_string_list(args, builder.library_includes())
elif args.library_c_files:
show_command_print_string_list(args, builder.library_c_files())
elif args.library_s_files:
show_command_print_string_list(args, builder.library_s_files())
elif args.library_cpp_files:
show_command_print_string_list(args, builder.library_cpp_files())
elif args.core_c_files:
show_command_print_string_list(args, builder.core_c_files())
elif args.core_s_files:
show_command_print_string_list(args, builder.core_s_files())
elif args.core_cpp_files:
show_command_print_string_list(args, builder.core_cpp_files())
elif args.variant_c_files:
vfiles = builder.variant_c_files()
if vfiles:
show_command_print_string_list(args, vfiles)
elif args.variant_s_files:
vfiles = builder.variant_s_files()
if vfiles:
show_command_print_string_list(args, vfiles)
elif args.variant_cpp_files:
vfiles = builder.variant_cpp_files()
if vfiles:
show_command_print_string_list(args, vfiles)
def add_common_parser_args(parser, serial_port, build_path, build_project_name,
project_path, project_source_path):
"""Add command line options common to the run and show commands."""
parser.add_argument(
"--serial-port",
default=serial_port,
help="Serial port for flashing. Default: '{}'".format(serial_port))
parser.add_argument(
"--build-path",
default=build_path,
help="Build directory. Default: '{}'".format(build_path))
parser.add_argument(
"--project-path",
default=project_path,
help="Project directory. Default: '{}'".format(project_path))
parser.add_argument(
"--project-source-path",
default=project_source_path,
help="Project directory. Default: '{}'".format(project_source_path))
parser.add_argument("--library-path",
default=[],
nargs="+",
type=str,
help="Path to Arduino Library directory.")
parser.add_argument(
"--build-project-name",
default=build_project_name,
help="Project name. Default: '{}'".format(build_project_name))
parser.add_argument("--board",
required=True,
help="Name of the board to use.")
# nargs="+" is one or more args, e.g:
# --menu-options menu.usb.serialhid menu.speed.150
parser.add_argument(
"--menu-options",
nargs="+",
type=str,
metavar="menu.usb.serial",
help="Desired Arduino menu options. See the "
"'list-menu-options' subcommand for available options.")
parser.add_argument("--set-variable",
action="append",
metavar='some.variable=NEW_VALUE',
help="Override an Arduino recipe variable. May be "
"specified multiple times. For example: "
"--set-variable 'serial.port.label=/dev/ttyACM0' "
"--set-variable 'serial.port.protocol=Teensy'")
def check_for_missing_args(args):
if args.arduino_package_path is None:
raise MissingArduinoCore(
"Please specify the location of an Arduino core using "
"'--arduino-package-path' and '--arduino-package-name'.")
# TODO(tonymd): These defaults don't make sense anymore and should be removed.
def get_default_options():
defaults = {}
defaults["build_path"] = os.path.realpath(
os.path.expanduser(
os.path.expandvars(os.path.join(os.getcwd(), "build"))))
defaults["project_path"] = os.path.realpath(
os.path.expanduser(os.path.expandvars(os.getcwd())))
defaults["project_source_path"] = os.path.join(defaults["project_path"],
"src")
defaults["build_project_name"] = os.path.basename(defaults["project_path"])
defaults["serial_port"] = "UNKNOWN"
return defaults
def load_config_file(args):
"""Load a config file and merge with command line options.
Command line takes precedence over values loaded from a config file."""
if args.save_config and not args.config_file:
raise FileNotFoundError(
"'--save-config' requires the '--config-file' option")
if not args.config_file:
return
default_options = get_default_options()
commandline_options = {
# Global option
"arduino_package_path": args.arduino_package_path,
"arduino_package_name": args.arduino_package_name,
"compiler_path_override": args.compiler_path_override,
# These options may not exist unless show or run command
"build_path": getattr(args, "build_path", None),
"project_path": getattr(args, "project_path", None),
"project_source_path": getattr(args, "project_source_path", None),
"build_project_name": getattr(args, "build_project_name", None),
"board": getattr(args, "board", None),
"menu_options": getattr(args, "menu_options", None),
}
# Decode JSON config file.
json_file_options, config_file_path = decode_file_json(args.config_file)
# Merge config file with command line options.
merged_options = {}
for key, value in commandline_options.items():
# Use the command line specified option by default
merged_options[key] = value
# Is this option in the config file?
if json_file_options.get(key, None) is not None:
# Use the json defined option if it's not set on the command
# line (or is a default value).
if value is None or value == default_options.get(key, None):
merged_options[key] = json_file_options[key]
# Update args namespace to matched merged_options.
for key, value in merged_options.items():
setattr(args, key, value)
# Write merged_options if --save-config.
if args.save_config:
encoded_json = json.dumps(merged_options, indent=4)
# Create parent directories
os.makedirs(os.path.dirname(config_file_path), exist_ok=True)
# Save json file.
with open(config_file_path, "w") as jfile:
jfile.write(encoded_json)
def _parse_args() -> argparse.Namespace:
"""Setup argparse and parse command line args."""
def log_level(arg: str) -> int:
try:
return getattr(logging, arg.upper())
except AttributeError:
raise argparse.ArgumentTypeError(
f'{arg.upper()} is not a valid log level')
def existing_directory(input_string: str):
"""Argparse type that resolves to an absolute path."""
input_path = Path(os.path.expandvars(input_string)).absolute()
if not input_path.exists():
raise argparse.ArgumentTypeError(
"'{}' is not a valid directory.".format(str(input_path)))
return input_path.as_posix()
parser = argparse.ArgumentParser()
parser.add_argument("-q",
"--quiet",
help="hide run command output",
action="store_true")
parser.add_argument('-l',
'--loglevel',
type=log_level,
default=logging.INFO,
help='Set the log level '
'(debug, info, warning, error, critical)')
default_options = get_default_options()
# Global command line options
parser.add_argument("--arduino-package-path",
type=existing_directory,
help="Path to the arduino IDE install location.")
parser.add_argument("--arduino-package-name",
help="Name of the Arduino board package to use.")
parser.add_argument("--compiler-path-override",
type=existing_directory,
help="Path to arm-none-eabi-gcc bin folder. "
"Default: Arduino core specified gcc")
parser.add_argument("-c", "--config-file", help="Path to a config file.")
parser.add_argument("--save-config",
action="store_true",
help="Save command line arguments to the config file.")
# Subcommands
subparsers = parser.add_subparsers(title="subcommand",
description="valid subcommands",
help="sub-command help",
dest="subcommand",
required=True)
# install-core command
install_core_parser = subparsers.add_parser(
"install-core", help="Download and install arduino cores")
install_core_parser.set_defaults(func=core_installer.install_core_command)
install_core_parser.add_argument("--prefix",
required=True,
help="Path to install core files.")
install_core_parser.add_argument(
"--core-name",
required=True,
choices=core_installer.supported_cores(),
help="Name of the arduino core to install.")
# list-boards command
list_boards_parser = subparsers.add_parser("list-boards",
help="show supported boards")
list_boards_parser.set_defaults(func=list_boards_command)
# list-menu-options command
list_menu_options_parser = subparsers.add_parser(
"list-menu-options",
help="show available menu options for selected board")
list_menu_options_parser.set_defaults(func=list_menu_options_command)
list_menu_options_parser.add_argument("--board",
required=True,
help="Name of the board to use.")
# show command
show_parser = subparsers.add_parser("show",
help="Return compiler information.")
add_common_parser_args(show_parser, default_options["serial_port"],
default_options["build_path"],
default_options["build_project_name"],
default_options["project_path"],
default_options["project_source_path"])
show_parser.add_argument("--delimit-with-newlines",
help="Separate flag output with newlines.",
action="store_true")
show_parser.add_argument("--library-names", nargs="+", type=str)
output_group = show_parser.add_mutually_exclusive_group(required=True)
output_group.add_argument("--c-compile", action="store_true")
output_group.add_argument("--cpp-compile", action="store_true")
output_group.add_argument("--link", action="store_true")
output_group.add_argument("--c-flags", action="store_true")
output_group.add_argument("--s-flags", action="store_true")
output_group.add_argument("--s-only-flags", action="store_true")
output_group.add_argument("--cpp-flags", action="store_true")
output_group.add_argument("--cpp-only-flags", action="store_true")
output_group.add_argument("--ld-flags", action="store_true")
output_group.add_argument("--ar-flags", action="store_true")
output_group.add_argument("--ld-libs", action="store_true")
output_group.add_argument("--ld-lib-names", action="store_true")
output_group.add_argument("--objcopy", help="objcopy step for SUFFIX")
output_group.add_argument("--objcopy-flags",
help="objcopy flags for SUFFIX")
output_group.add_argument("--core-path", action="store_true")
output_group.add_argument("--cc-binary", action="store_true")
output_group.add_argument("--cxx-binary", action="store_true")
output_group.add_argument("--ar-binary", action="store_true")
output_group.add_argument("--objcopy-binary", action="store_true")
output_group.add_argument("--size-binary", action="store_true")
output_group.add_argument("--prebuilds",
action="store_true",
help="Show prebuild step commands.")
output_group.add_argument("--postbuilds",
action="store_true",
help="Show postbuild step commands.")
output_group.add_argument("--upload-tools", action="store_true")
output_group.add_argument("--upload-command")
output_group.add_argument("--library-includes", action="store_true")
output_group.add_argument("--library-include-dirs", action="store_true")
output_group.add_argument("--library-c-files", action="store_true")
output_group.add_argument("--library-s-files", action="store_true")
output_group.add_argument("--library-cpp-files", action="store_true")
output_group.add_argument("--core-c-files", action="store_true")
output_group.add_argument("--core-s-files", action="store_true")
output_group.add_argument("--core-cpp-files", action="store_true")
output_group.add_argument("--variant-c-files", action="store_true")
output_group.add_argument("--variant-s-files", action="store_true")
output_group.add_argument("--variant-cpp-files", action="store_true")
show_parser.set_defaults(func=show_command)
# run command
run_parser = subparsers.add_parser("run", help="Run Arduino recipes.")
add_common_parser_args(run_parser, default_options["serial_port"],
default_options["build_path"],
default_options["build_project_name"],
default_options["project_path"],
default_options["project_source_path"])
run_parser.add_argument("--run-link",
nargs="+",
type=str,
help="Run the link command. Expected arguments: "
"the archive file followed by all obj files.")
run_parser.add_argument("--run-objcopy", action="store_true")
run_parser.add_argument("--run-prebuilds", action="store_true")
run_parser.add_argument("--run-postbuilds", action="store_true")
run_parser.add_argument("--run-upload-command")
run_parser.set_defaults(func=run_command)
return parser.parse_args()
def main():
"""Main command line function.
Dispatches command line invocations to sub `*_command()` functions.
"""
# Parse command line arguments.
args = _parse_args()
_LOG.debug(_pretty_format(args))
log.install(args.loglevel)
# Check for and set alternate compiler path.
if args.compiler_path_override:
# Get absolute path
compiler_path_override = os.path.realpath(
os.path.expanduser(os.path.expandvars(
args.compiler_path_override)))
args.compiler_path_override = compiler_path_override
load_config_file(args)
if args.subcommand == "install-core":
args.func(args)
elif args.subcommand in ["list-boards", "list-menu-options"]:
check_for_missing_args(args)
builder = ArduinoBuilder(args.arduino_package_path,
args.arduino_package_name)
builder.load_board_definitions()
args.func(args, builder)
else: # args.subcommand in ["run", "show"]
check_for_missing_args(args)
builder = ArduinoBuilder(
args.arduino_package_path,
args.arduino_package_name,
build_path=args.build_path,
build_project_name=args.build_project_name,
project_path=args.project_path,
project_source_path=args.project_source_path,
library_path=getattr(args, 'library_path', None),
library_names=getattr(args, 'library_names', None),
compiler_path_override=args.compiler_path_override)
builder.load_board_definitions()
builder.select_board(args.board, args.menu_options)
if args.set_variable:
builder.set_variables(args.set_variable)
args.func(args, builder)
sys.exit(0)
if __name__ == '__main__':
main()