diff --git a/changes/2026.bugfix.rst b/changes/2026.bugfix.rst new file mode 100644 index 000000000..17f161589 --- /dev/null +++ b/changes/2026.bugfix.rst @@ -0,0 +1 @@ +Python 3.12.7 introduced an incompatibility with the handling of ``-C``, ``-d`` and other flags that accept values. This incompatibility has been corrected. diff --git a/src/briefcase/cmdline.py b/src/briefcase/cmdline.py index 0ed3ac829..e79e30e86 100644 --- a/src/briefcase/cmdline.py +++ b/src/briefcase/cmdline.py @@ -22,7 +22,12 @@ from briefcase.console import MAX_TEXT_WIDTH, Console from briefcase.platforms import get_output_formats, get_platforms -from .exceptions import InvalidFormatError, NoCommandError, UnsupportedCommandError +from .exceptions import ( + InvalidFormatError, + InvalidPlatformError, + NoCommandError, + UnsupportedCommandError, +) COMMANDS = [ NewCommand, @@ -103,12 +108,6 @@ def parse_cmdline(args, console: Console | None = None): help=argparse.SUPPRESS, ) - # 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) - # argparse handles `--` specially, so make the passthrough args bypass the parser. def parse_known_args(args): args, passthrough = split_passthrough(args) @@ -136,48 +135,42 @@ def parse_known_args(args): Command = DevCommand elif options.command == "upgrade": Command = UpgradeCommand - - # Commands dependent on the platform and format else: - parser.add_argument( - "platform", - choices=list(platforms.keys()), - default={ + # Commands dependent on the platform and format. The general form of such a + # command is `briefcase `; but the format will be + # inferred from the platform if one isn't specified, and the platform will be + # inferred from the operating system if it isn't explicitly given. + # + # and aren't parsed as regular arguments due to ambiguities + # in interpreting those arguments; instead, they're handled directly from the + # argument list, with the expectation that they *must* be the first and second + # arguments (after the command) if provided. There's no other bare arguments, so + # we only need to look for whether the arguments start with "-". + if extra and not extra[0].startswith("-"): + name = extra.pop(0) + # Normalize the platform name to the registered capitalization + platform = {n.lower(): n for n in platforms.keys()}.get(name.lower(), name) + else: + platform = { "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", - ) - - # 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 arguments, now that we know it is a command that makes use - # of platform/output_format. - options, extra = parse_known_args(args) + }[sys.platform] # Import the platform module - platform_module = platforms[options.platform] + try: + platform_module = platforms[platform] + except KeyError: + raise InvalidPlatformError(platform, platforms.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 + if extra and not extra[0].startswith("-") and not extra[0] == "--": + output_format = extra.pop(0) else: - output_format = options.output_format + output_format = platform_module.DEFAULT_OUTPUT_FORMAT - output_formats = get_output_formats(options.platform) + output_formats = get_output_formats(platform) # Normalise casing of output_format to be more forgiving. output_format = {n.lower(): n for n in output_formats}.get( @@ -196,7 +189,7 @@ def parse_known_args(args): ) except AttributeError: raise UnsupportedCommandError( - platform=options.platform, + platform=platform, output_format=output_format, command=options.command, ) diff --git a/src/briefcase/exceptions.py b/src/briefcase/exceptions.py index 3197d5092..4f7723ca7 100644 --- a/src/briefcase/exceptions.py +++ b/src/briefcase/exceptions.py @@ -26,6 +26,17 @@ def __str__(self): return self.msg +class InvalidPlatformError(BriefcaseError): + def __init__(self, requested, choices): + super().__init__(error_code=-20, skip_logfile=True) + self.requested = requested + self.choices = choices + + def __str__(self): + choices = ", ".join(sorted(self.choices, key=str.lower)) + return f"Invalid platform {self.requested!r}; (choose from: {choices})" + + class InvalidFormatError(BriefcaseError): def __init__(self, requested, choices): super().__init__(error_code=-21, skip_logfile=True) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 3f6dcc349..616b305cb 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -9,6 +9,7 @@ from briefcase.console import Console, Log, LogLevel from briefcase.exceptions import ( InvalidFormatError, + InvalidPlatformError, NoCommandError, UnsupportedCommandError, ) @@ -456,15 +457,10 @@ def test_command_unknown_platform(monkeypatch, logger, console): # Pretend we're on macOS, regardless of where the tests run. monkeypatch.setattr(sys, "platform", "darwin") - with pytest.raises(SystemExit) as excinfo: + expected_exc_regex = r"Invalid platform 'foobar'; \(choose from: .*\)" + with pytest.raises(InvalidPlatformError, match=expected_exc_regex): do_cmdline_parse("create foobar".split(), logger, console) - assert excinfo.value.code == 2 - assert excinfo.value.__context__.argument_name == "platform" - assert excinfo.value.__context__.message.startswith( - "invalid choice: 'foobar' (choose from" - ) - def test_command_explicit_platform(monkeypatch, logger, console): """``briefcase create linux`` returns linux create app command."""