def parse_cmdline(args): parser = argparse.ArgumentParser( prog="briefcase", description="Package Python code for distribution.", usage="briefcase [-h] <command> [<platform>] [<format>] ...", epilog=("Each command, platform and format has additional options. " "Use the -h option on a specific command for more details."), add_help=False, ) parser.add_argument( "-f", "--formats", action="store_true", dest="show_output_formats", help= "show the available output formats and exit (specify a platform for more details).", ) parser.add_argument("-V", "--version", action="version", version=__version__) # <command> isn't actually optional; but if it's marked as required, # there's no way to get help for subcommands. So; treat <command> # as optional, handle the case where <command> isn't provided # as the case where top-level help is displayed, and provide an explicit # usage string so that the instructions displayed are correct parser.add_argument( "command", choices=[ "new", "dev", "upgrade", "create", "update", "build", "run", "package", "publish", ], metavar="command", nargs="?", help="the command to execute (one of: %(choices)s)", ) # <platform> *is* optional, with the default value based on the platform # that you're on. platforms = get_platforms() # To make the UX a little forgiving, we normalize *any* case to the case # actually used to register the platform. This function maps the lower-case # version of the registered name to the actual registered name. def normalize(name): return {n.lower(): n for n in platforms.keys()}.get(name.lower(), name) # Use parse_known_args to ensure any extra arguments can be ignored, # and parsed as part of subcommand handling. This will capture the # command, platform (filling a default if unspecified) and format # (with no value if unspecified). options, extra = parser.parse_known_args(args) # If no command has been provided, display top-level help. if options.command is None: raise NoCommandError(parser.format_help()) elif options.command == "new": command = NewCommand(base_path=Path.cwd()) options = command.parse_options(extra=extra) return command, options elif options.command == "dev": command = DevCommand(base_path=Path.cwd()) options = command.parse_options(extra=extra) return command, options elif options.command == "upgrade": command = UpgradeCommand(base_path=Path.cwd()) options = command.parse_options(extra=extra) return command, options parser.add_argument( "platform", choices=list(platforms.keys()), default={ "darwin": "macOS", "linux": "linux", "win32": "windows", }[sys.platform], metavar="platform", nargs="?", type=normalize, help="The platform to target (one of %(choices)s; default: %(default)s", ) # <format> is also optional, with the default being platform dependent. # There's no way to encode option-dependent choices, so allow *any* # input, and we'll manually validate. parser.add_argument( "output_format", metavar="format", nargs="?", help= "The output format to use (the available output formats are platform dependent)", ) # Re-parse the aruments, now that we know it is a command that makes use # of platform/output_format. options, extra = parser.parse_known_args(args) # Import the platform module platform_module = platforms[options.platform] output_formats = get_output_formats(options.platform) # If the user requested a list of available output formats, output them. if options.show_output_formats: raise ShowOutputFormats( platform=options.platform, default=platform_module.DEFAULT_OUTPUT_FORMAT, choices=list(output_formats.keys()), ) # If the output format wasn't explicitly specified, check to see # Otherwise, extract and use the default output_format for the platform. if options.output_format is None: output_format = platform_module.DEFAULT_OUTPUT_FORMAT else: output_format = options.output_format # Normalise casing of output_format to be more forgiving. output_format = {n.lower(): n for n in output_formats}.get(output_format.lower(), output_format) # We now know the command, platform, and format. # Get the command class that corresponds to that definition. try: format_module = output_formats[output_format] Command = getattr(format_module, options.command) except KeyError: raise InvalidFormatError( requested=output_format, choices=list(output_formats.keys()), ) except AttributeError: raise UnsupportedCommandError( platform=options.platform, output_format=output_format, command=options.command, ) # Construct a command, and parse the remaining arguments. command = Command(base_path=Path.cwd()) options = command.parse_options(extra=extra) return command, options
def parse_config(config_file, platform, output_format): """ Parse the briefcase section of the pyproject.toml configuration file. This method only does basic structural parsing of the TOML, looking for, at a minimum, a ``[tool.briefcase.app.<appname>]`` section declaring the existence of a single app. It will also search for: * ``[tool.briefcase]`` - global briefcase settings * ``[tool.briefcase.app.<appname>]`` - settings specific to the app * ``[tool.briefcase.app.<appname>.<platform>]`` - settings specific to the platform * ``[tool.briefcase.app.<appname>.<platform>.<format>]`` - settings specific to the output format A configuration can define multiple apps; the final output is the merged content of the global, app, platform and output format settings for each app, with output format definitions taking precendence over platform, over app-level, over global. The final result is a single (mostly) flat dictionary for each app. :param config_file: A file-like object containing TOML to be parsed. :param platform: The platform being targetted :param output_format: The output format :returns: A dictionary of configuration data. The top level dictionary is keyed by the names of the apps that are declared; each value is itself the configuration data merged from global, app, platform and format definitions. """ try: pyproject = toml.load(config_file) global_config = pyproject['tool']['briefcase'] except toml.TomlDecodeError as e: raise BriefcaseConfigError('Invalid pyproject.toml: {e}'.format(e=e)) except KeyError: raise BriefcaseConfigError( 'No tool.briefcase section in pyproject.toml') # For consistent results, sort the platforms and formats all_platforms = sorted(get_platforms().keys()) all_formats = sorted(get_output_formats(platform).keys()) try: all_apps = global_config.pop('app') except KeyError: raise BriefcaseConfigError( 'No Briefcase apps defined in pyproject.toml') # Build the flat configuration for each app, # based on the requested platform and output format app_configs = {} for app_name, app_data in all_apps.items(): # At this point, the base configuration will contain a section # for each configured platform. Iterate over all the known platforms, # and remove these platform configurations. Keep a copy of the platform # configuration if it matches the requested platform, and merge it # into the app's configuration platform_data = None for p in all_platforms: try: platform_block = app_data.pop(p) if p == platform: # If the platform matches the requested format, preserve # it for later use. platform_data = platform_block merge_config(platform_data, platform_data) # The platform configuration will contain a section # for each configured output format. Iterate over all # the output format of the platform, and remove these # output format configurations. Keep a copy of the output # format configuration if it matches the requested output # format, and merge it into the platform's configuration. format_data = None for f in all_formats: try: format_block = platform_data.pop(f) if f == output_format: # If the output format matches the requested # one, preserve it. format_data = format_block except KeyError: pass # If we found a specific configuration for the requested # output format, add it to the platform configuration, # overwriting any platform-level settings with format-level # values. if format_data: merge_config(platform_data, format_data) except KeyError: pass # Now construct the final configuration. # First, convert the requirement definition at the global level merge_config(global_config, global_config) # The app's config starts as a copy of the base briefcase configuation. config = copy.deepcopy(global_config) # The app name is both the key, and a property of the configuration config['app_name'] = app_name # Merge the app-specific requirements merge_config(config, app_data) # If there is platform-specific configuration, merge the requirements, # the overwrite the platform-specific values. # This will already include any format-specific configuration. if platform_data: merge_config(config, platform_data) # Construct a configuration object, and add it to the list # of configurations that are being handled. app_configs[app_name] = config return global_config, app_configs