2 # Copyright 2020 The Pigweed Authors
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
8 # https://www.apache.org/licenses/LICENSE-2.0
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
15 """Command line interface for arduino_builder."""
25 from collections import OrderedDict
26 from pathlib import Path
27 from typing import List
29 from pw_arduino_build import core_installer, log
30 from pw_arduino_build.builder import ArduinoBuilder
31 from pw_arduino_build.file_operations import decode_file_json
33 _LOG = logging.getLogger(__name__)
35 _pretty_print = pprint.PrettyPrinter(indent=1, width=120).pprint
36 _pretty_format = pprint.PrettyPrinter(indent=1, width=120).pformat
39 class MissingArduinoCore(Exception):
40 """Exception raised when an Arduino core can not be found."""
43 def list_boards_command(unused_args, builder):
44 # list-boards subcommand
45 # (does not need a selected board or default menu options)
47 # TODO(tonymd): Print this sorted with auto-ljust columns
48 longest_name_length = 0
49 for board_name, board_dict in builder.board.items():
50 if len(board_name) > longest_name_length:
51 longest_name_length = len(board_name)
52 longest_name_length += 2
54 print("Board Name".ljust(longest_name_length), "Description")
55 for board_name, board_dict in builder.board.items():
56 print(board_name.ljust(longest_name_length), board_dict['name'])
60 def list_menu_options_command(args, builder):
61 # List all menu options for the selected board.
62 builder.select_board(args.board)
65 all_options, all_column_widths = builder.get_menu_options()
66 separator = "-" * (all_column_widths[0] + all_column_widths[1] + 2)
69 for name, description in all_options:
70 print(name.ljust(all_column_widths[0] + 1), description)
72 print("\nDefault Options")
75 menu_options, unused_col_widths = builder.get_default_menu_options()
76 for name, description in menu_options:
77 print(name.ljust(all_column_widths[0] + 1), description)
80 def show_command_print_string_list(args, string_list: List[str]):
82 join_token = "\n" if args.delimit_with_newlines else " "
83 print(join_token.join(string_list))
86 def show_command_print_flag_string(args, flag_string):
87 if args.delimit_with_newlines:
88 flag_string_with_newlines = shlex.split(flag_string)
89 print("\n".join(flag_string_with_newlines))
94 def subtract_flags(flag_list_a: List[str],
95 flag_list_b: List[str]) -> List[str]:
96 """Given two sets of flags return flags in a that are not in b."""
97 flag_counts = OrderedDict() # type: OrderedDict[str, int]
98 for flag in flag_list_a + flag_list_b:
99 flag_counts[flag] = flag_counts.get(flag, 0) + 1
100 return [flag for flag in flag_list_a if flag_counts.get(flag, 0) == 1]
103 def run_command_lines(args, command_lines: List[str]):
104 for command_line in command_lines:
107 # TODO(tonymd): Exit with sub command exit code.
108 command_line_args = shlex.split(command_line)
109 process = subprocess.run(command_line_args,
110 stdout=subprocess.PIPE,
111 stderr=subprocess.STDOUT)
112 if process.returncode != 0:
113 _LOG.error('Command failed with exit code %d.', process.returncode)
114 _LOG.error('Full command:')
116 _LOG.error(' %s', command_line)
118 _LOG.error('Process output:')
120 sys.stdout.buffer.write(process.stdout)
125 def run_command(args, builder):
126 """Run sub command function.
128 Runs Arduino recipes.
131 if args.run_prebuilds:
132 run_command_lines(args, builder.get_prebuild_steps())
135 line = builder.get_link_line()
136 archive_file_path = args.run_link[0] # pylint: disable=unused-variable
137 object_files = args.run_link[1:]
138 line = line.replace("{object_files}", " ".join(object_files), 1)
139 run_command_lines(args, [line])
142 run_command_lines(args, builder.get_objcopy_steps())
144 if args.run_postbuilds:
145 run_command_lines(args, builder.get_postbuild_steps())
147 if args.run_upload_command:
148 command = builder.get_upload_line(args.run_upload_command,
150 run_command_lines(args, [command])
153 # pylint: disable=too-many-branches
154 def show_command(args, builder):
155 """Show sub command function.
157 Prints compiler info and flags.
160 print(builder.get_cc_binary())
162 elif args.cxx_binary:
163 print(builder.get_cxx_binary())
165 elif args.objcopy_binary:
166 print(builder.get_objcopy_binary())
169 print(builder.get_ar_binary())
171 elif args.size_binary:
172 print(builder.get_size_binary())
175 print(builder.get_c_compile_line())
177 elif args.cpp_compile:
178 print(builder.get_cpp_compile_line())
181 print(builder.get_link_line())
184 print(builder.get_objcopy(args.objcopy))
186 elif args.objcopy_flags:
187 objcopy_flags = builder.get_objcopy_flags(args.objcopy_flags)
188 show_command_print_flag_string(args, objcopy_flags)
191 cflags = builder.get_c_flags()
192 show_command_print_flag_string(args, cflags)
195 sflags = builder.get_s_flags()
196 show_command_print_flag_string(args, sflags)
198 elif args.s_only_flags:
199 s_only_flags = subtract_flags(shlex.split(builder.get_s_flags()),
200 shlex.split(builder.get_c_flags()))
201 show_command_print_flag_string(args, " ".join(s_only_flags))
204 cppflags = builder.get_cpp_flags()
205 show_command_print_flag_string(args, cppflags)
207 elif args.cpp_only_flags:
208 cpp_only_flags = subtract_flags(shlex.split(builder.get_cpp_flags()),
209 shlex.split(builder.get_c_flags()))
210 show_command_print_flag_string(args, " ".join(cpp_only_flags))
213 ldflags = builder.get_ld_flags()
214 show_command_print_flag_string(args, ldflags)
217 show_command_print_flag_string(args, builder.get_ld_libs())
219 elif args.ld_lib_names:
220 show_command_print_flag_string(args,
221 builder.get_ld_libs(name_only=True))
224 ar_flags = builder.get_ar_flags()
225 show_command_print_flag_string(args, ar_flags)
228 print(builder.get_core_path())
231 show_command_print_string_list(args, builder.get_prebuild_steps())
233 elif args.postbuilds:
234 show_command_print_string_list(args, builder.get_postbuild_steps())
236 elif args.upload_command:
237 print(builder.get_upload_line(args.upload_command, args.serial_port))
239 elif args.upload_tools:
240 tools = builder.get_upload_tool_names()
241 for tool_name in tools:
244 elif args.library_include_dirs:
245 show_command_print_string_list(args, builder.library_include_dirs())
247 elif args.library_includes:
248 show_command_print_string_list(args, builder.library_includes())
250 elif args.library_c_files:
251 show_command_print_string_list(args, builder.library_c_files())
253 elif args.library_s_files:
254 show_command_print_string_list(args, builder.library_s_files())
256 elif args.library_cpp_files:
257 show_command_print_string_list(args, builder.library_cpp_files())
259 elif args.core_c_files:
260 show_command_print_string_list(args, builder.core_c_files())
262 elif args.core_s_files:
263 show_command_print_string_list(args, builder.core_s_files())
265 elif args.core_cpp_files:
266 show_command_print_string_list(args, builder.core_cpp_files())
268 elif args.variant_c_files:
269 vfiles = builder.variant_c_files()
271 show_command_print_string_list(args, vfiles)
273 elif args.variant_s_files:
274 vfiles = builder.variant_s_files()
276 show_command_print_string_list(args, vfiles)
278 elif args.variant_cpp_files:
279 vfiles = builder.variant_cpp_files()
281 show_command_print_string_list(args, vfiles)
284 def add_common_parser_args(parser, serial_port, build_path, build_project_name,
285 project_path, project_source_path):
286 """Add command line options common to the run and show commands."""
290 help="Serial port for flashing. Default: '{}'".format(serial_port))
294 help="Build directory. Default: '{}'".format(build_path))
297 default=project_path,
298 help="Project directory. Default: '{}'".format(project_path))
300 "--project-source-path",
301 default=project_source_path,
302 help="Project directory. Default: '{}'".format(project_source_path))
303 parser.add_argument("--library-path",
307 help="Path to Arduino Library directory.")
309 "--build-project-name",
310 default=build_project_name,
311 help="Project name. Default: '{}'".format(build_project_name))
312 parser.add_argument("--board",
314 help="Name of the board to use.")
315 # nargs="+" is one or more args, e.g:
316 # --menu-options menu.usb.serialhid menu.speed.150
321 metavar="menu.usb.serial",
322 help="Desired Arduino menu options. See the "
323 "'list-menu-options' subcommand for available options.")
324 parser.add_argument("--set-variable",
326 metavar='some.variable=NEW_VALUE',
327 help="Override an Arduino recipe variable. May be "
328 "specified multiple times. For example: "
329 "--set-variable 'serial.port.label=/dev/ttyACM0' "
330 "--set-variable 'serial.port.protocol=Teensy'")
333 def check_for_missing_args(args):
334 if args.arduino_package_path is None:
335 raise MissingArduinoCore(
336 "Please specify the location of an Arduino core using "
337 "'--arduino-package-path' and '--arduino-package-name'.")
340 # TODO(tonymd): These defaults don't make sense anymore and should be removed.
341 def get_default_options():
343 defaults["build_path"] = os.path.realpath(
345 os.path.expandvars(os.path.join(os.getcwd(), "build"))))
346 defaults["project_path"] = os.path.realpath(
347 os.path.expanduser(os.path.expandvars(os.getcwd())))
348 defaults["project_source_path"] = os.path.join(defaults["project_path"],
350 defaults["build_project_name"] = os.path.basename(defaults["project_path"])
351 defaults["serial_port"] = "UNKNOWN"
355 def load_config_file(args):
356 """Load a config file and merge with command line options.
358 Command line takes precedence over values loaded from a config file."""
360 if args.save_config and not args.config_file:
361 raise FileNotFoundError(
362 "'--save-config' requires the '--config-file' option")
364 if not args.config_file:
367 default_options = get_default_options()
369 commandline_options = {
371 "arduino_package_path": args.arduino_package_path,
372 "arduino_package_name": args.arduino_package_name,
373 "compiler_path_override": args.compiler_path_override,
374 # These options may not exist unless show or run command
375 "build_path": getattr(args, "build_path", None),
376 "project_path": getattr(args, "project_path", None),
377 "project_source_path": getattr(args, "project_source_path", None),
378 "build_project_name": getattr(args, "build_project_name", None),
379 "board": getattr(args, "board", None),
380 "menu_options": getattr(args, "menu_options", None),
383 # Decode JSON config file.
384 json_file_options, config_file_path = decode_file_json(args.config_file)
386 # Merge config file with command line options.
388 for key, value in commandline_options.items():
389 # Use the command line specified option by default
390 merged_options[key] = value
392 # Is this option in the config file?
393 if json_file_options.get(key, None) is not None:
394 # Use the json defined option if it's not set on the command
395 # line (or is a default value).
396 if value is None or value == default_options.get(key, None):
397 merged_options[key] = json_file_options[key]
399 # Update args namespace to matched merged_options.
400 for key, value in merged_options.items():
401 setattr(args, key, value)
403 # Write merged_options if --save-config.
405 encoded_json = json.dumps(merged_options, indent=4)
406 # Create parent directories
407 os.makedirs(os.path.dirname(config_file_path), exist_ok=True)
409 with open(config_file_path, "w") as jfile:
410 jfile.write(encoded_json)
413 def _parse_args() -> argparse.Namespace:
414 """Setup argparse and parse command line args."""
415 def log_level(arg: str) -> int:
417 return getattr(logging, arg.upper())
418 except AttributeError:
419 raise argparse.ArgumentTypeError(
420 f'{arg.upper()} is not a valid log level')
422 def existing_directory(input_string: str):
423 """Argparse type that resolves to an absolute path."""
424 input_path = Path(os.path.expandvars(input_string)).absolute()
425 if not input_path.exists():
426 raise argparse.ArgumentTypeError(
427 "'{}' is not a valid directory.".format(str(input_path)))
428 return input_path.as_posix()
430 parser = argparse.ArgumentParser()
431 parser.add_argument("-q",
433 help="hide run command output",
435 parser.add_argument('-l',
438 default=logging.INFO,
439 help='Set the log level '
440 '(debug, info, warning, error, critical)')
442 default_options = get_default_options()
444 # Global command line options
445 parser.add_argument("--arduino-package-path",
446 type=existing_directory,
447 help="Path to the arduino IDE install location.")
448 parser.add_argument("--arduino-package-name",
449 help="Name of the Arduino board package to use.")
450 parser.add_argument("--compiler-path-override",
451 type=existing_directory,
452 help="Path to arm-none-eabi-gcc bin folder. "
453 "Default: Arduino core specified gcc")
454 parser.add_argument("-c", "--config-file", help="Path to a config file.")
455 parser.add_argument("--save-config",
457 help="Save command line arguments to the config file.")
460 subparsers = parser.add_subparsers(title="subcommand",
461 description="valid subcommands",
462 help="sub-command help",
466 # install-core command
467 install_core_parser = subparsers.add_parser(
468 "install-core", help="Download and install arduino cores")
469 install_core_parser.set_defaults(func=core_installer.install_core_command)
470 install_core_parser.add_argument("--prefix",
472 help="Path to install core files.")
473 install_core_parser.add_argument(
476 choices=core_installer.supported_cores(),
477 help="Name of the arduino core to install.")
479 # list-boards command
480 list_boards_parser = subparsers.add_parser("list-boards",
481 help="show supported boards")
482 list_boards_parser.set_defaults(func=list_boards_command)
484 # list-menu-options command
485 list_menu_options_parser = subparsers.add_parser(
487 help="show available menu options for selected board")
488 list_menu_options_parser.set_defaults(func=list_menu_options_command)
489 list_menu_options_parser.add_argument("--board",
491 help="Name of the board to use.")
494 show_parser = subparsers.add_parser("show",
495 help="Return compiler information.")
496 add_common_parser_args(show_parser, default_options["serial_port"],
497 default_options["build_path"],
498 default_options["build_project_name"],
499 default_options["project_path"],
500 default_options["project_source_path"])
501 show_parser.add_argument("--delimit-with-newlines",
502 help="Separate flag output with newlines.",
504 show_parser.add_argument("--library-names", nargs="+", type=str)
506 output_group = show_parser.add_mutually_exclusive_group(required=True)
507 output_group.add_argument("--c-compile", action="store_true")
508 output_group.add_argument("--cpp-compile", action="store_true")
509 output_group.add_argument("--link", action="store_true")
510 output_group.add_argument("--c-flags", action="store_true")
511 output_group.add_argument("--s-flags", action="store_true")
512 output_group.add_argument("--s-only-flags", action="store_true")
513 output_group.add_argument("--cpp-flags", action="store_true")
514 output_group.add_argument("--cpp-only-flags", action="store_true")
515 output_group.add_argument("--ld-flags", action="store_true")
516 output_group.add_argument("--ar-flags", action="store_true")
517 output_group.add_argument("--ld-libs", action="store_true")
518 output_group.add_argument("--ld-lib-names", action="store_true")
519 output_group.add_argument("--objcopy", help="objcopy step for SUFFIX")
520 output_group.add_argument("--objcopy-flags",
521 help="objcopy flags for SUFFIX")
522 output_group.add_argument("--core-path", action="store_true")
523 output_group.add_argument("--cc-binary", action="store_true")
524 output_group.add_argument("--cxx-binary", action="store_true")
525 output_group.add_argument("--ar-binary", action="store_true")
526 output_group.add_argument("--objcopy-binary", action="store_true")
527 output_group.add_argument("--size-binary", action="store_true")
528 output_group.add_argument("--prebuilds",
530 help="Show prebuild step commands.")
531 output_group.add_argument("--postbuilds",
533 help="Show postbuild step commands.")
534 output_group.add_argument("--upload-tools", action="store_true")
535 output_group.add_argument("--upload-command")
536 output_group.add_argument("--library-includes", action="store_true")
537 output_group.add_argument("--library-include-dirs", action="store_true")
538 output_group.add_argument("--library-c-files", action="store_true")
539 output_group.add_argument("--library-s-files", action="store_true")
540 output_group.add_argument("--library-cpp-files", action="store_true")
541 output_group.add_argument("--core-c-files", action="store_true")
542 output_group.add_argument("--core-s-files", action="store_true")
543 output_group.add_argument("--core-cpp-files", action="store_true")
544 output_group.add_argument("--variant-c-files", action="store_true")
545 output_group.add_argument("--variant-s-files", action="store_true")
546 output_group.add_argument("--variant-cpp-files", action="store_true")
548 show_parser.set_defaults(func=show_command)
551 run_parser = subparsers.add_parser("run", help="Run Arduino recipes.")
552 add_common_parser_args(run_parser, default_options["serial_port"],
553 default_options["build_path"],
554 default_options["build_project_name"],
555 default_options["project_path"],
556 default_options["project_source_path"])
557 run_parser.add_argument("--run-link",
560 help="Run the link command. Expected arguments: "
561 "the archive file followed by all obj files.")
562 run_parser.add_argument("--run-objcopy", action="store_true")
563 run_parser.add_argument("--run-prebuilds", action="store_true")
564 run_parser.add_argument("--run-postbuilds", action="store_true")
565 run_parser.add_argument("--run-upload-command")
567 run_parser.set_defaults(func=run_command)
569 return parser.parse_args()
573 """Main command line function.
575 Dispatches command line invocations to sub `*_command()` functions.
577 # Parse command line arguments.
579 _LOG.debug(_pretty_format(args))
581 log.install(args.loglevel)
583 # Check for and set alternate compiler path.
584 if args.compiler_path_override:
586 compiler_path_override = os.path.realpath(
587 os.path.expanduser(os.path.expandvars(
588 args.compiler_path_override)))
589 args.compiler_path_override = compiler_path_override
591 load_config_file(args)
593 if args.subcommand == "install-core":
595 elif args.subcommand in ["list-boards", "list-menu-options"]:
596 check_for_missing_args(args)
597 builder = ArduinoBuilder(args.arduino_package_path,
598 args.arduino_package_name)
599 builder.load_board_definitions()
600 args.func(args, builder)
601 else: # args.subcommand in ["run", "show"]
602 check_for_missing_args(args)
603 builder = ArduinoBuilder(
604 args.arduino_package_path,
605 args.arduino_package_name,
606 build_path=args.build_path,
607 build_project_name=args.build_project_name,
608 project_path=args.project_path,
609 project_source_path=args.project_source_path,
610 library_path=getattr(args, 'library_path', None),
611 library_names=getattr(args, 'library_names', None),
612 compiler_path_override=args.compiler_path_override)
613 builder.load_board_definitions()
614 builder.select_board(args.board, args.menu_options)
615 if args.set_variable:
616 builder.set_variables(args.set_variable)
617 args.func(args, builder)
622 if __name__ == '__main__':