diff --git a/changes/1812.feature.rst b/changes/1812.feature.rst new file mode 100644 index 000000000..0950eb030 --- /dev/null +++ b/changes/1812.feature.rst @@ -0,0 +1 @@ +The name of the LICENSE file can now be specified in the PEP621-section of the pyproject.toml file diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py index 160108308..15d2a0274 100644 --- a/src/briefcase/commands/base.py +++ b/src/briefcase/commands/base.py @@ -831,6 +831,7 @@ def parse_config(self, filename, overrides): config_file, platform=self.platform, output_format=self.output_format, + logger=self.logger, ) # Create the global config diff --git a/src/briefcase/commands/convert.py b/src/briefcase/commands/convert.py index b8afcad3b..48c6c9652 100644 --- a/src/briefcase/commands/convert.py +++ b/src/briefcase/commands/convert.py @@ -647,16 +647,8 @@ def migrate_necessary_files(self, project_dir, test_source_dir, module_name): # Copy license file if not already there license_file = self.pep621_data.get("license", {}).get("file") - if license_file is not None and Path(license_file).name != "LICENSE": - self.logger.warning( - f"\nLicense file found in '{self.base_path}', but its name is " - f"'{Path(license_file).name}', not 'LICENSE'. Briefcase will create a " - "template 'LICENSE' file, but you might want to consider renaming the " - "existing file." - ) - copy2(project_dir / "LICENSE", self.base_path / "LICENSE") - elif not (self.base_path / "LICENSE").exists(): + if license_file is None and not (self.base_path / "LICENSE").exists(): self.logger.warning( f"\nLicense file not found in '{self.base_path}'. " "Briefcase will create a template 'LICENSE' file." diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 34d50800a..e9e08997a 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -154,6 +154,7 @@ def __init__( project_name, version, bundle, + license=None, url=None, author=None, author_email=None, @@ -166,6 +167,7 @@ def __init__( self.url = url self.author = author self.author_email = author_email + self.license = license # Version number is PEP440 compliant: if not is_pep440_canonical_version(self.version): @@ -187,6 +189,7 @@ def __init__( bundle, description, sources, + license, formal_name=None, url=None, author=None, @@ -228,6 +231,7 @@ def __init__( self.test_requires = test_requires self.supported = supported self.long_description = long_description + self.license = license if not is_valid_app_name(self.app_name): raise BriefcaseConfigError( @@ -393,7 +397,7 @@ def maybe_update(field, *project_fields): # Keys that map directly maybe_update("description", "description") - maybe_update("license", "license", "text") + maybe_update("license", "license") maybe_update("url", "urls", "Homepage") maybe_update("version", "version") @@ -426,7 +430,7 @@ def maybe_update(field, *project_fields): pass -def parse_config(config_file, platform, output_format): +def parse_config(config_file, platform, output_format, logger): """Parse the briefcase section of the pyproject.toml configuration file. This method only does basic structural parsing of the TOML, looking for, @@ -551,4 +555,17 @@ def parse_config(config_file, platform, output_format): # of configurations that are being handled. app_configs[app_name] = config + old_license_format = False + for config in [global_config, *app_configs.values()]: + if isinstance(config.get("license"), str): + config["license"] = {"file": "LICENSE"} + old_license_format = True + if old_license_format: + logger.warning( + "The license was specified using a string, however Briefcase prefers a PEP621 " + "compatible dictionary on the form {'file': '{filename}'} or {'text': '{license_text}'}.\n" + "Therefore, when given the license as a string, Briefcase ignores the license string and " + "defaults to {'file': 'LICENSE'}." + ) + return global_config, app_configs diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index e6cd5ea75..5013c52e3 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -704,9 +704,28 @@ def build_app(self, app: AppConfig, **kwargs): doc_folder.mkdir(parents=True, exist_ok=True) with self.input.wait_bar("Installing license..."): - license_file = self.base_path / "LICENSE" - if license_file.is_file(): - self.tools.shutil.copy(license_file, doc_folder / "copyright") + if license_file := app.license.get("file"): + license_file = self.base_path / license_file + if license_file.is_file(): + self.tools.shutil.copy(license_file, doc_folder / "copyright") + else: + raise BriefcaseCommandError( + f"""\ +You specified that the license file is {license_file}. However, this file does not exist. + +Make sure you spelled the name of the license file correctly in `pyproject.toml`. +""" + ) + elif license_text := app.license.get("text"): + (doc_folder / "copyright").write_text(license_text, encoding="utf-8") + if len(license_text.splitlines()) <= 1: + self.logger.warning( + """ +You specified the license using a text string. However, this string is only one line. + +Make sure that the license text is a full license (or specify a license file). +""" + ) else: raise BriefcaseCommandError( """\ @@ -1037,7 +1056,7 @@ def _package_rpm(self, app: AppConfig, **kwargs): # pragma: no-cover-if-is-wind f"Release: {getattr(app, 'revision', 1)}%{{?dist}}", f"Summary: {app.description}", "", - f"License: {getattr(app, 'license', 'Unknown')}", + "License: Unknown", # TODO: Add license information (see #1829) f"URL: {app.url}", "Source0: %{name}-%{version}.tar.gz", "", diff --git a/tests/commands/base/conftest.py b/tests/commands/base/conftest.py index d1c2a2585..b8e251602 100644 --- a/tests/commands/base/conftest.py +++ b/tests/commands/base/conftest.py @@ -114,4 +114,5 @@ def my_app(): version="1.2.3", description="This is a simple app", sources=["src/my_app"], + license={"file": "LICENSE"}, ) diff --git a/tests/commands/base/test_finalize.py b/tests/commands/base/test_finalize.py index e0740702b..98f001f48 100644 --- a/tests/commands/base/test_finalize.py +++ b/tests/commands/base/test_finalize.py @@ -13,6 +13,7 @@ def first_app(): version="0.0.1", description="The first simple app", sources=["src/first"], + license={"file": "LICENSE"}, ) @@ -24,6 +25,7 @@ def second_app(): version="0.0.2", description="The second simple app", sources=["src/second"], + license={"file": "LICENSE"}, ) diff --git a/tests/commands/base/test_parse_config.py b/tests/commands/base/test_parse_config.py index 03fa549f6..25d9aaf60 100644 --- a/tests/commands/base/test_parse_config.py +++ b/tests/commands/base/test_parse_config.py @@ -23,6 +23,7 @@ def test_incomplete_global_config(base_command): [tool.briefcase] version = "1.2.3" description = "A sample app" + license.file = "LICENSE" [tool.briefcase.app.my-app] """, @@ -47,6 +48,7 @@ def test_incomplete_config(base_command): version = "1.2.3" bundle = "com.example" description = "A sample app" + license.file = "LICENSE" [tool.briefcase.app.my-app] """, @@ -71,6 +73,7 @@ def test_parse_config(base_command): description = "A sample app" bundle = "org.beeware" mystery = 'default' + license.file = "LICENSE" [tool.briefcase.app.firstapp] sources = ['src/firstapp'] @@ -127,6 +130,7 @@ def test_parse_config_with_overrides(base_command): description = "A sample app" bundle = "org.beeware" mystery = 'default' + license.file = "LICENSE" [tool.briefcase.app.firstapp] sources = ['src/firstapp'] @@ -197,6 +201,7 @@ def test_parse_config_with_invalid_override(base_command): description = "A sample app" bundle = "org.beeware" mystery = 'default' + license.file = "LICENSE" [tool.briefcase.app.firstapp] sources = ['src/firstapp'] diff --git a/tests/commands/build/conftest.py b/tests/commands/build/conftest.py index af40d3f79..89d7c78da 100644 --- a/tests/commands/build/conftest.py +++ b/tests/commands/build/conftest.py @@ -95,6 +95,7 @@ def second_app_config(): version="0.0.2", description="The second simple app", sources=["src/second"], + license={"file": "LICENSE"}, ) diff --git a/tests/commands/convert/test_migrate_necessary_files.py b/tests/commands/convert/test_migrate_necessary_files.py index 0a4a4610c..02a478eca 100644 --- a/tests/commands/convert/test_migrate_necessary_files.py +++ b/tests/commands/convert/test_migrate_necessary_files.py @@ -102,12 +102,31 @@ def test_warning_without_license_file( @pytest.mark.parametrize("test_source_dir", ["tests"]) -def test_pep621_wrong_license_filename( +def test_file_is_copied_if_no_license_file_specified( convert_command, project_dir_with_files, dummy_app_name, test_source_dir, ): + """A license file is copied if no license file is specified in pyproject.toml.""" + create_file(convert_command.base_path / "CHANGELOG", "") + assert not (convert_command.base_path / "LICENSE").exists() + convert_command.migrate_necessary_files( + project_dir_with_files, + test_source_dir, + dummy_app_name, + ) + assert (convert_command.base_path / "LICENSE").exists() + + +@pytest.mark.parametrize("test_source_dir", ["tests"]) +def test_pep621_specified_license_filename( + convert_command, + project_dir_with_files, + dummy_app_name, + test_source_dir, +): + """No license file is copied if a license file is specified in pyproject.toml.""" convert_command.logger.warning = mock.MagicMock() license_name = "LICENSE.txt" create_file(convert_command.base_path / license_name, "") @@ -121,11 +140,7 @@ def test_pep621_wrong_license_filename( test_source_dir, dummy_app_name, ) - convert_command.logger.warning.assert_called_once_with( - f"\nLicense file found in '{convert_command.base_path}', but its name is " - f"'{license_name}', not 'LICENSE'. Briefcase will create a template 'LICENSE' " - "file, but you might want to consider renaming the existing file." - ) + assert not (convert_command.base_path / "LICENSE").exists() @pytest.mark.parametrize("test_source_dir", ["tests"]) @@ -162,7 +177,11 @@ def test_no_warning_with_license_and_changelog_file( """No warning is raised if both license file and changelog file is present.""" convert_command.logger.warning = mock.MagicMock() - create_file(convert_command.base_path / "LICENSE", "") + create_file( + convert_command.base_path / "pyproject.toml", + '[project]\nlicense = { file = "LICENSE.txt" }', + ) + create_file(convert_command.base_path / "LICENSE.txt", "") create_file(convert_command.base_path / "CHANGELOG", "") convert_command.migrate_necessary_files( project_dir_with_files, diff --git a/tests/commands/create/conftest.py b/tests/commands/create/conftest.py index 50f067f18..b6de00c45 100644 --- a/tests/commands/create/conftest.py +++ b/tests/commands/create/conftest.py @@ -199,6 +199,7 @@ def tracking_create_command(tmp_path, mock_git, monkeypatch_tool_host_os): version="0.0.1", description="The first simple app", sources=["src/first"], + license={"file": "LICENSE"}, ), "second": AppConfig( app_name="second", @@ -206,6 +207,7 @@ def tracking_create_command(tmp_path, mock_git, monkeypatch_tool_host_os): version="0.0.2", description="The second simple app", sources=["src/second"], + license={"file": "LICENSE"}, ), }, ) @@ -223,6 +225,7 @@ def myapp(): url="https://example.com", author="First Last", author_email="first@example.com", + license={"file": "LICENSE"}, ) diff --git a/tests/commands/create/test_create_app.py b/tests/commands/create/test_create_app.py index 2a7b2e1ea..36fd3fdfc 100644 --- a/tests/commands/create/test_create_app.py +++ b/tests/commands/create/test_create_app.py @@ -155,6 +155,7 @@ def test_create_app_not_supported(tracking_create_command, tmp_path): description="The third simple app", sources=["src/third"], supported=False, + license={"file": "LICENSE"}, ) ) diff --git a/tests/commands/create/test_generate_app_template.py b/tests/commands/create/test_generate_app_template.py index 4b5ed110c..e73fd8323 100644 --- a/tests/commands/create/test_generate_app_template.py +++ b/tests/commands/create/test_generate_app_template.py @@ -41,6 +41,7 @@ def full_context(): "custom_permissions": {}, "requests": {}, "document_types": {}, + "license": {"file": "LICENSE"}, # Properties of the generating environment "python_version": platform.python_version(), "host_arch": "gothic", diff --git a/tests/commands/create/test_install_app_resources.py b/tests/commands/create/test_install_app_resources.py index 9fae614e7..43d6de01b 100644 --- a/tests/commands/create/test_install_app_resources.py +++ b/tests/commands/create/test_install_app_resources.py @@ -12,6 +12,7 @@ def test_no_resources(create_command): version="1.2.3", description="This is a simple app", sources=["src/my_app"], + license={"file": "LICENSE"}, ) # Prime the path index with no targets @@ -37,6 +38,7 @@ def test_icon_target(create_command, tmp_path): description="This is a simple app", sources=["src/my_app"], icon="images/icon", + license={"file": "LICENSE"}, ) # Prime the path index with 2 icon targets @@ -106,6 +108,7 @@ def test_icon_variant_target(create_command, tmp_path): "round": "images/round", "square": "images/square", }, + license={"file": "LICENSE"}, ) # Prime the path index with 2 icon targets @@ -190,6 +193,7 @@ def test_splash_target(create_command, capsys): description="This is a simple app", sources=["src/my_app"], splash="images/splash", + license={"file": "LICENSE"}, ) # Prime an empty path index @@ -221,6 +225,7 @@ def test_splash_variant_target(create_command, capsys): "portrait": "images/portrait", "landscape": "images/landscape", }, + license={"file": "LICENSE"}, ) # Prime an empty path index @@ -254,6 +259,7 @@ def test_doctype_icon_target(create_command, tmp_path): "icon": "images/other-icon", }, }, + license={"file": "LICENSE"}, ) # Prime the path index with 2 document types; diff --git a/tests/commands/dev/conftest.py b/tests/commands/dev/conftest.py index 6974b81c6..cc37ed8cf 100644 --- a/tests/commands/dev/conftest.py +++ b/tests/commands/dev/conftest.py @@ -28,6 +28,7 @@ def first_app_uninstalled(tmp_path): version="0.0.1", description="The first simple app", sources=["src/first"], + license={"file": "LICENSE"}, ) @@ -55,6 +56,7 @@ def second_app(tmp_path): version="0.0.2", description="The second simple app", sources=["src/second"], + license={"file": "LICENSE"}, ) @@ -75,4 +77,5 @@ def third_app(tmp_path): description="The third simple app", sources=["src/third", "src/common", "other"], test_sources=["tests", "path/to/other"], + license={"file": "LICENSE"}, ) diff --git a/tests/commands/open/conftest.py b/tests/commands/open/conftest.py index 4a8b2f0df..6fd9b3d7c 100644 --- a/tests/commands/open/conftest.py +++ b/tests/commands/open/conftest.py @@ -89,6 +89,7 @@ def open_command(tmp_path): version="0.0.1", description="The first simple app", sources=["src/first"], + license={"file": "LICENSE"}, ), "second": AppConfig( app_name="second", @@ -96,6 +97,7 @@ def open_command(tmp_path): version="0.0.2", description="The second simple app", sources=["src/second"], + license={"file": "LICENSE"}, ), }, ) diff --git a/tests/commands/package/conftest.py b/tests/commands/package/conftest.py index 78df51592..5ec68a229 100644 --- a/tests/commands/package/conftest.py +++ b/tests/commands/package/conftest.py @@ -132,6 +132,7 @@ def first_app_config(): version="0.0.1", description="The first simple app", sources=["src/first"], + license={"file": "LICENSE"}, ) @@ -167,6 +168,7 @@ def second_app_config(): version="0.0.2", description="The second simple app", sources=["src/second"], + license={"file": "LICENSE"}, ) diff --git a/tests/commands/publish/conftest.py b/tests/commands/publish/conftest.py index 9a7a63d43..e8d77b1c4 100644 --- a/tests/commands/publish/conftest.py +++ b/tests/commands/publish/conftest.py @@ -104,6 +104,7 @@ def first_app_config(): version="0.0.1", description="The first simple app", sources=["src/first"], + license={"file": "LICENSE"}, ) @@ -146,6 +147,7 @@ def second_app_config(): version="0.0.2", description="The second simple app", sources=["src/second"], + license={"file": "LICENSE"}, ) diff --git a/tests/commands/run/conftest.py b/tests/commands/run/conftest.py index e14070bdd..eb3c9451b 100644 --- a/tests/commands/run/conftest.py +++ b/tests/commands/run/conftest.py @@ -108,6 +108,7 @@ def first_app_config(): version="0.0.1", description="The first simple app", sources=["src/first"], + license={"file": "LICENSE"}, ) @@ -150,6 +151,7 @@ def second_app_config(): version="0.0.2", description="The second simple app", sources=["src/second"], + license={"file": "LICENSE"}, ) diff --git a/tests/commands/update/conftest.py b/tests/commands/update/conftest.py index 3bf783e88..68ab2d022 100644 --- a/tests/commands/update/conftest.py +++ b/tests/commands/update/conftest.py @@ -88,6 +88,7 @@ def update_command(tmp_path): version="0.0.1", description="The first simple app", sources=["src/first"], + license={"file": "LICENSE"}, ), "second": AppConfig( app_name="second", @@ -95,6 +96,7 @@ def update_command(tmp_path): version="0.0.2", description="The second simple app", sources=["src/second"], + license={"file": "LICENSE"}, ), }, ) diff --git a/tests/config/test_AppConfig.py b/tests/config/test_AppConfig.py index bddd122c7..f6ca05807 100644 --- a/tests/config/test_AppConfig.py +++ b/tests/config/test_AppConfig.py @@ -12,6 +12,7 @@ def test_minimal_AppConfig(): bundle="org.beeware", description="A simple app", sources=["src/myapp", "somewhere/else/interesting", "local_app"], + license={"file": "LICENSE"}, ) # The basic properties have been set. @@ -49,6 +50,7 @@ def test_extra_attrs(): bundle="org.beeware", description="A simple app", long_description="A longer description\nof the app", + license={"file": "LICENSE"}, template="/path/to/template", sources=["src/myapp"], requires=["first", "second", "third"], @@ -111,6 +113,7 @@ def test_valid_app_name(name): bundle="org.beeware", description="A simple app", sources=["src/" + name.replace("-", "_")], + license={"file": "LICENSE"}, ) except BriefcaseConfigError: pytest.fail(f"{name} should be valid") @@ -137,6 +140,7 @@ def test_invalid_app_name(name): bundle="org.beeware", description="A simple app", sources=["src/invalid"], + license={"file": "LICENSE"}, ) @@ -157,6 +161,7 @@ def test_valid_bundle(bundle): bundle=bundle, description="A simple app", sources=["src/myapp"], + license={"file": "LICENSE"}, ) except BriefcaseConfigError: pytest.fail(f"{bundle} should be valid") @@ -186,6 +191,7 @@ def test_invalid_bundle_identifier(bundle): bundle=bundle, description="A simple app", sources=["src/invalid"], + license={"file": "LICENSE"}, ) @@ -197,6 +203,7 @@ def test_valid_app_version(): bundle="org.beeware", description="A simple app", sources=["src/myapp"], + license={"file": "LICENSE"}, ) except BriefcaseConfigError: pytest.fail("1.2.3 should be a valid version number") @@ -213,6 +220,7 @@ def test_invalid_app_version(): bundle="org.beeware", description="A simple app", sources=["src/invalid"], + license={"file": "LICENSE"}, ) @@ -230,6 +238,7 @@ def test_module_name(name, module_name): bundle="org.beeware", description="A simple app", sources=["src/" + module_name], + license={"file": "LICENSE"}, ) assert config.module_name == module_name @@ -249,6 +258,7 @@ def test_package_name(bundle, package_name): bundle=bundle, description="A simple app", sources=["src/myapp"], + license={"file": "LICENSE"}, ) assert config.package_name == package_name @@ -268,6 +278,7 @@ def test_bundle_name(app_name, bundle_name): bundle="com.example", description="A simple app", sources=["src/my_app"], + license={"file": "LICENSE"}, ) assert config.bundle_name == bundle_name @@ -289,6 +300,7 @@ def test_bundle_identifier(app_name, bundle_name): bundle=bundle, description="A simple app", sources=["src/my_app"], + license={"file": "LICENSE"}, ) assert config.bundle_identifier == f"{bundle}.{bundle_name}" @@ -313,6 +325,7 @@ def test_duplicated_source(sources): bundle="org.beeware", description="A simple app", sources=sources, + license={"file": "LICENSE"}, ) @@ -326,4 +339,5 @@ def test_no_source_for_app(): bundle="org.beeware", description="A simple app", sources=["src/something", "src/other"], + license={"file": "LICENSE"}, ) diff --git a/tests/config/test_GlobalConfig.py b/tests/config/test_GlobalConfig.py index efae046ec..9cab990fd 100644 --- a/tests/config/test_GlobalConfig.py +++ b/tests/config/test_GlobalConfig.py @@ -10,6 +10,7 @@ def test_minimal_GlobalConfig(): project_name="My Project", version="1.2.3", bundle="org.beeware", + license={"file": "LICENSE"}, ) # The basic properties have been set. @@ -31,6 +32,7 @@ def test_extra_attrs(): author_email="jane@example.com", first="value 1", second=42, + license={"file": "LICENSE"}, ) # The basic properties have been set. @@ -56,6 +58,7 @@ def test_valid_app_version(): project_name="My Project", version="1.2.3", bundle="org.beeware", + license={"file": "LICENSE"}, ) except BriefcaseConfigError: pytest.fail("1.2.3 should be a valid version number") @@ -69,4 +72,5 @@ def test_invalid_app_version(): project_name="My Project", version="foobar", bundle="org.beeware", + license={"file": "LICENSE"}, ) diff --git a/tests/config/test_merge_pep621_config.py b/tests/config/test_merge_pep621_config.py index 3bf42455f..f1a2f7813 100644 --- a/tests/config/test_merge_pep621_config.py +++ b/tests/config/test_merge_pep621_config.py @@ -28,7 +28,7 @@ def test_base_keys(): "key": "value", "description": "It's cool", "version": "1.2.3", - "license": "BSD License", + "license": {"text": "BSD License"}, "url": "https://example.com", } @@ -70,13 +70,24 @@ def test_missing_subkeys(): briefcase_config, { "urls": {"Sponsorship": "https://example.com"}, + }, + ) + + assert briefcase_config == {"key": "value"} + + +def test_specified_license_file(): + "The license file is included in the briefcase config if specified in the PEP621 config" + briefcase_config = {"key": "value"} + + merge_pep621_config( + briefcase_config, + { "license": {"file": "license.txt"}, }, ) - assert briefcase_config == { - "key": "value", - } + assert briefcase_config == {"key": "value", "license": {"file": "license.txt"}} def test_empty_authors(): diff --git a/tests/config/test_parse_config.py b/tests/config/test_parse_config.py index ba24a26cd..1cd15363b 100644 --- a/tests/config/test_parse_config.py +++ b/tests/config/test_parse_config.py @@ -1,4 +1,5 @@ from io import BytesIO +from unittest.mock import Mock import pytest @@ -11,7 +12,9 @@ def test_invalid_toml(): config_file = BytesIO(b"this is not toml!") with pytest.raises(BriefcaseConfigError, match="Invalid pyproject.toml"): - parse_config(config_file, platform="macOS", output_format="Xcode") + parse_config( + config_file, platform="macOS", output_format="Xcode", logger=Mock() + ) def test_no_briefcase_section(): @@ -25,7 +28,9 @@ def test_no_briefcase_section(): ) with pytest.raises(BriefcaseConfigError, match="No tool.briefcase section"): - parse_config(config_file, platform="macOS", output_format="Xcode") + parse_config( + config_file, platform="macOS", output_format="Xcode", logger=Mock() + ) def test_no_apps(): @@ -39,7 +44,9 @@ def test_no_apps(): ) with pytest.raises(BriefcaseConfigError, match="No Briefcase apps defined"): - parse_config(config_file, platform="macOS", output_format="Xcode") + parse_config( + config_file, platform="macOS", output_format="Xcode", logger=Mock() + ) def test_single_minimal_app(): @@ -48,21 +55,24 @@ def test_single_minimal_app(): b""" [tool.briefcase] value = 42 + license.file = "LICENSE" [tool.briefcase.app.my_app] """ ) global_options, apps = parse_config( - config_file, platform="macOS", output_format="Xcode" + config_file, platform="macOS", output_format="Xcode", logger=Mock() ) # There's a single global option - assert global_options == {"value": 42} + assert global_options == {"value": 42, "license": {"file": "LICENSE"}} # The app gets the name from its header line. # It inherits the value from the base definition. - assert apps == {"my_app": {"app_name": "my_app", "value": 42}} + assert apps == { + "my_app": {"app_name": "my_app", "value": 42, "license": {"file": "LICENSE"}} + } def test_multiple_minimal_apps(): @@ -71,15 +81,17 @@ def test_multiple_minimal_apps(): b""" [tool.briefcase.app.first] number=37 + license.file = "LICENSE" [tool.briefcase.app.second] app_name="my_app" number=42 + license.file = "LICENSE.txt" """ ) global_options, apps = parse_config( - config_file, platform="macOS", output_format="Xcode" + config_file, platform="macOS", output_format="Xcode", logger=Mock() ) # There are no global options @@ -88,13 +100,11 @@ def test_multiple_minimal_apps(): # The apps get their name from the header lines. # The second tool overrides its app name assert apps == { - "first": { - "app_name": "first", - "number": 37, - }, + "first": {"app_name": "first", "number": 37, "license": {"file": "LICENSE"}}, "second": { "app_name": "my_app", "number": 42, + "license": {"file": "LICENSE.txt"}, }, } @@ -106,6 +116,7 @@ def test_platform_override(): [tool.briefcase] value = 0 basevalue = "the base" + license.file = "LICENSE" [tool.briefcase.app.my_app] value = 1 @@ -126,13 +137,14 @@ def test_platform_override(): ) global_options, apps = parse_config( - config_file, platform="macOS", output_format="Xcode" + config_file, platform="macOS", output_format="Xcode", logger=Mock() ) # The global options are exactly as specified assert global_options == { "value": 0, "basevalue": "the base", + "license": {"file": "LICENSE"}, } # Since a macOS app has been requested, the macOS platform values @@ -148,12 +160,14 @@ def test_platform_override(): "basevalue": "the base", "appvalue": "the app", "platformvalue": "macos platform", + "license": {"file": "LICENSE"}, }, "other_app": { "app_name": "other_app", "value": 4, "basevalue": "the base", "platformvalue": "other macos platform", + "license": {"file": "LICENSE"}, }, } @@ -165,6 +179,7 @@ def test_platform_override_ordering(): [tool.briefcase] value = 0 basevalue = "the base" + license.file = "LICENSE" [tool.briefcase.app.my_app] value = 1 @@ -185,11 +200,15 @@ def test_platform_override_ordering(): ) global_options, apps = parse_config( - config_file, platform="macOS", output_format="Xcode" + config_file, platform="macOS", output_format="Xcode", logger=Mock() ) # The global options are exactly as specified - assert global_options == {"value": 0, "basevalue": "the base"} + assert global_options == { + "value": 0, + "basevalue": "the base", + "license": {"file": "LICENSE"}, + } # Since a macOS app has been requested, the macOS platform values # take priority. Linux configuration values are dropped. @@ -204,12 +223,14 @@ def test_platform_override_ordering(): "basevalue": "the base", "appvalue": "the app", "platformvalue": "macos platform", + "license": {"file": "LICENSE"}, }, "other_app": { "app_name": "other_app", "value": 4, "basevalue": "the base", "platformvalue": "other macos platform", + "license": {"file": "LICENSE"}, }, } @@ -221,6 +242,7 @@ def test_format_override(): [tool.briefcase] value = 0 basevalue = "the base" + license.file = "LICENSE" [tool.briefcase.app.my_app] value = 1 @@ -257,11 +279,15 @@ def test_format_override(): ) global_options, apps = parse_config( - config_file, platform="macOS", output_format="app" + config_file, platform="macOS", output_format="app", logger=Mock() ) # The global options are exactly as specified - assert global_options == {"value": 0, "basevalue": "the base"} + assert global_options == { + "value": 0, + "basevalue": "the base", + "license": {"file": "LICENSE"}, + } # Since a macOS app has been requested, the macOS app format values # take priority. Linux configuration values are dropped. @@ -277,12 +303,14 @@ def test_format_override(): "appvalue": "the app", "platformvalue": "macos platform", "formatvalue": "app format", + "license": {"file": "LICENSE"}, }, "other_app": { "app_name": "other_app", "value": 41, "basevalue": "the base", "formatvalue": "other macos app format", + "license": {"file": "LICENSE"}, }, } @@ -294,6 +322,7 @@ def test_format_override_ordering(): [tool.briefcase] value = 0 basevalue = "the base" + license.file = "LICENSE" [tool.briefcase.app.my_app] value = 1 @@ -330,11 +359,15 @@ def test_format_override_ordering(): ) global_options, apps = parse_config( - config_file, platform="macOS", output_format="app" + config_file, platform="macOS", output_format="app", logger=Mock() ) # The global options are exactly as specified - assert global_options == {"value": 0, "basevalue": "the base"} + assert global_options == { + "value": 0, + "basevalue": "the base", + "license": {"file": "LICENSE"}, + } # Since a macOS dmg has been requested, the macOS dmg format values # take priority. Linux configuration values are dropped. @@ -350,11 +383,13 @@ def test_format_override_ordering(): "appvalue": "the app", "platformvalue": "macos platform", "formatvalue": "app format", + "license": {"file": "LICENSE"}, }, "other_app": { "app_name": "other_app", "value": 0, "basevalue": "the base", + "license": {"file": "LICENSE"}, }, } @@ -366,6 +401,7 @@ def test_requires(): [tool.briefcase] value = 0 requires = ["base value"] + license.file = "LICENSE" [tool.briefcase.app.my_app] requires = ["my_app value"] @@ -391,13 +427,14 @@ def test_requires(): # Request a macOS app global_options, apps = parse_config( - config_file, platform="macOS", output_format="app" + config_file, platform="macOS", output_format="app", logger=Mock() ) # The global options are exactly as specified assert global_options == { "value": 0, "requires": ["base value"], + "license": {"file": "LICENSE"}, } # The macOS my_app app specifies a full inherited chain. @@ -412,6 +449,7 @@ def test_requires(): "app value", ], "value": 0, + "license": {"file": "LICENSE"}, }, "other_app": { "app_name": "other_app", @@ -419,17 +457,22 @@ def test_requires(): "base value", ], "value": 0, + "license": {"file": "LICENSE"}, }, } # Request a macOS xcode project config_file.seek(0) global_options, apps = parse_config( - config_file, platform="macOS", output_format="Xcode" + config_file, platform="macOS", output_format="Xcode", logger=Mock() ) # The global options are exactly as specified - assert global_options == {"value": 0, "requires": ["base value"]} + assert global_options == { + "value": 0, + "requires": ["base value"], + "license": {"file": "LICENSE"}, + } # The macOS my_app dmg specifies a full inherited chain. # The other_app dmg doesn't specify any options. @@ -443,6 +486,7 @@ def test_requires(): "xcode value", ], "value": 0, + "license": {"file": "LICENSE"}, }, "other_app": { "app_name": "other_app", @@ -450,16 +494,21 @@ def test_requires(): "base value", ], "value": 0, + "license": {"file": "LICENSE"}, }, } config_file.seek(0) global_options, apps = parse_config( - config_file, platform="linux", output_format="appimage" + config_file, platform="linux", output_format="appimage", logger=Mock() ) # The global options are exactly as specified - assert global_options == {"value": 0, "requires": ["base value"]} + assert global_options == { + "value": 0, + "requires": ["base value"], + "license": {"file": "LICENSE"}, + } # The linux my_app appimage overrides the *base* value, but extends for linux. assert apps == { @@ -472,6 +521,7 @@ def test_requires(): "appimage value", ], "value": 0, + "license": {"file": "LICENSE"}, }, "other_app": { "app_name": "other_app", @@ -479,6 +529,7 @@ def test_requires(): "base value", ], "value": 0, + "license": {"file": "LICENSE"}, }, } @@ -489,6 +540,7 @@ def test_document_types(): b""" [tool.briefcase] value = 0 + license.file = "LICENSE" [tool.briefcase.app.my_app] @@ -509,7 +561,7 @@ def test_document_types(): # Request a macOS app global_options, apps = parse_config( - config_file, platform="macOS", output_format="Xcode" + config_file, platform="macOS", output_format="Xcode", logger=Mock() ) # The macOS my_app app specifies a full inherited chain. @@ -528,10 +580,12 @@ def test_document_types(): }, }, "value": 0, + "license": {"file": "LICENSE"}, }, "other_app": { "app_name": "other_app", "value": 0, + "license": {"file": "LICENSE"}, }, } @@ -578,7 +632,7 @@ def test_pep621_defaults(): ) global_options, apps = parse_config( - config_file, platform="macOS", output_format="app" + config_file, platform="macOS", output_format="app", logger=Mock() ) awesome = apps["awesome"] @@ -586,7 +640,7 @@ def test_pep621_defaults(): "project_name": "Awesome app", "bundle": "com.example", "version": "1.2.3", - "license": "You can use it while standing on one foot", + "license": {"text": "You can use it while standing on one foot"}, "author": "Kim Park", "author_email": "kim@example.com", "url": "https://example.com/awesome", @@ -599,3 +653,38 @@ def test_pep621_defaults(): "formal_name": "Awesome Application", "long_description": "The application is very awesome", } + + +def test_license_is_string(): + config_file = BytesIO( + b""" + [tool.briefcase] + value = 0 + license = "Some license" + + [tool.briefcase.app.my_app] + appvalue = "the app" + """ + ) + + logger = Mock() + global_options, apps = parse_config( + config_file, platform="macOS", output_format="app", logger=logger + ) + + assert global_options == { + "value": 0, + "license": {"file": "LICENSE"}, + } + assert apps["my_app"] == { + "app_name": "my_app", + "value": 0, + "appvalue": "the app", + "license": {"file": "LICENSE"}, + } + logger.warning.assert_called_once_with( + "The license was specified using a string, however Briefcase prefers a PEP621 " + "compatible dictionary on the form {'file': '{filename}'} or {'text': '{license_text}'}.\n" + "Therefore, when given the license as a string, Briefcase ignores the license string and " + "defaults to {'file': 'LICENSE'}." + ) diff --git a/tests/conftest.py b/tests/conftest.py index 31e4203f3..b703c4f05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -95,6 +95,7 @@ def first_app_config(): version="0.0.1", description="The first simple app", sources=["src/first"], + license={"file": "LICENSE"}, ) diff --git a/tests/integrations/conftest.py b/tests/integrations/conftest.py index 15c0fcc38..85d6cae47 100644 --- a/tests/integrations/conftest.py +++ b/tests/integrations/conftest.py @@ -54,4 +54,5 @@ def first_app_config(): version="0.0.1", description="The first simple app", sources=["src/first_app"], + license={"file": "LICENSE"}, ) diff --git a/tests/integrations/docker/conftest.py b/tests/integrations/docker/conftest.py index 0af268d1c..252005f6c 100644 --- a/tests/integrations/docker/conftest.py +++ b/tests/integrations/docker/conftest.py @@ -53,6 +53,7 @@ def my_app() -> AppConfig: bundle="com.example", version="1.2.3", description="This is a simple app", + license={"file": "LICENSE"}, sources=["path/to/src/myapp", "other/stuff"], system_requires=["things==1.2", "stuff>=3.4"], system_runtime_requires=["runtime_things==1.42", "stuff>=3.4"], diff --git a/tests/platforms/conftest.py b/tests/platforms/conftest.py index 644877692..4b975e68b 100644 --- a/tests/platforms/conftest.py +++ b/tests/platforms/conftest.py @@ -18,6 +18,7 @@ def first_app_config(): sources=["src/first_app"], requires=["foo==1.2.3", "bar>=4.5"], test_requires=["pytest"], + license={"file": "LICENSE"}, ) @@ -32,6 +33,7 @@ def uppercase_app_config(): version="0.0.1", description="The first simple app", sources=["src/First_App"], + license={"file": "LICENSE"}, ) @@ -48,6 +50,7 @@ def underscore_app_config(first_app_config): version="0.0.1", description="The first simple app \\ demonstration", sources=["src/first_app"], + license={"file": "LICENSE"}, requires=["foo==1.2.3", "bar>=4.5"], test_requires=["pytest"], ) diff --git a/tests/platforms/linux/system/test_build.py b/tests/platforms/linux/system/test_build.py index 1693b3146..1e315ec63 100644 --- a/tests/platforms/linux/system/test_build.py +++ b/tests/platforms/linux/system/test_build.py @@ -10,6 +10,8 @@ from briefcase.exceptions import BriefcaseCommandError from briefcase.platforms.linux.system import LinuxSystemBuildCommand +from ....utils import create_file + @pytest.fixture def build_command(tmp_path, first_app): @@ -116,7 +118,7 @@ def test_missing_license(build_command, first_app, tmp_path): # Build the app; it will fail with pytest.raises( BriefcaseCommandError, - match=r"Your project does not contain a LICENSE file.", + match=r"You specified that the license file is ", ): build_command.build_app(first_app) @@ -128,6 +130,91 @@ def test_missing_license(build_command, first_app, tmp_path): ) +def test_specified_license_file_is_copied(build_command, first_app, tmp_path): + """The specified license file is copied if a license file is specified.""" + create_file(tmp_path / "base_path/LICENSE.txt", "The Actual First App License") + first_app.license["file"] = "LICENSE.txt" + + # Build the app + build_command.build_app(first_app) + + # The correct license file has been copied + doc_folder = ( + build_command.bundle_path(first_app) + / f"{first_app.app_name}-{first_app.version}" + / "usr" + / "share" + / "doc" + / first_app.app_name + ) + assert (doc_folder / "copyright").read_text( + encoding="utf-8" + ) == "The Actual First App License" + + +def test_license_text_is_saved(build_command, first_app): + """The license text is saved as a file in the bundle.""" + first_app.license = {"text": "Some license text"} + + # Build the app + build_command.build_app(first_app) + + # The license text has been saved + doc_folder = ( + build_command.bundle_path(first_app) + / f"{first_app.app_name}-{first_app.version}" + / "usr" + / "share" + / "doc" + / first_app.app_name + ) + + assert (doc_folder / "copyright").read_text(encoding="utf-8") == "Some license text" + + +def test_license_text_warns_with_single_line_license(build_command, first_app): + """A warning is logged if the license text is a single line.""" + + first_app.license = {"text": "Some license text"} + build_command.logger.warning = mock.MagicMock() + + # Build the app + build_command.build_app(first_app) + + build_command.logger.warning.assert_called_once_with( + """ +You specified the license using a text string. However, this string is only one line. + +Make sure that the license text is a full license (or specify a license file). +""" + ) + + +def test_exception_with_no_license(build_command, first_app): + """An exception is raised if there is no license.""" + + first_app.license = {} + build_command.logger.warning = mock.MagicMock() + + # Build the app + with pytest.raises( + BriefcaseCommandError, match="Your project does not contain a LICENSE file." + ): + build_command.build_app(first_app) + + +def test_license_text_doesnt_warn_with_multi_line_license( + build_command, first_app, tmp_path +): + """No warning is logged if the license text is multi-line.""" + first_app.license = {"text": "Some license text\nsome more text"} + build_command.logger.warning = mock.MagicMock() + + # Build the app + build_command.build_app(first_app) + build_command.logger.warning.assert_not_called() + + def test_missing_changelog(build_command, first_app, tmp_path): """If the changelog source file is missing, an error is raised.""" bundle_path = tmp_path / "base_path/build/first-app/somevendor/surprising" diff --git a/tests/test_mainline.py b/tests/test_mainline.py index 6af1a200f..9246fa337 100644 --- a/tests/test_mainline.py +++ b/tests/test_mainline.py @@ -27,6 +27,7 @@ def pyproject_toml(monkeypatch, tmp_path): project_name = "Hello World" bundle = "com.example" version = "0.0.1" +license.file = "LICENSE" [tool.briefcase.app.myapp] description = "My first application"