Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support specifying the license file in the pyproject.toml file #1812

Merged
merged 2 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/1812.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The name of the license file can now be specified using a PEP 621-compliant format for the ``license`` setting.
1 change: 1 addition & 0 deletions changes/1812.removal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The format for the ``license`` field has been converted to PEP 621 format. Existing projects that specify ``license`` as a string should update their configurations to point at the generated license file using ``license.file = "LICENSE"``.
1 change: 1 addition & 0 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 1 addition & 9 deletions src/briefcase/commands/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
Expand Down
21 changes: 19 additions & 2 deletions src/briefcase/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def __init__(
project_name,
version,
bundle,
license=None,
url=None,
author=None,
author_email=None,
Expand All @@ -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):
Expand All @@ -187,6 +189,7 @@ def __init__(
bundle,
description,
sources,
license,
formal_name=None,
url=None,
author=None,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
"Your app configuration has a `license` field that is specified as a string. "
"Briefcase now uses PEP 621 format for license definitions. To silence this "
'warning, replace the `license` declaration with `license.file = "LICENSE".'
)

return global_config, app_configs
42 changes: 34 additions & 8 deletions src/briefcase/platforms/linux/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,16 +704,38 @@ 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"""\
Your `pyproject.toml` specifies a license file of {str(license_file.relative_to(self.base_path))!r}.
However, this file does not exist.

Ensure you have correctly spelled the filename in your `license.file` setting.

"""
)
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(
"""
Your app specifies a license using `license.text`, but the value doesn't appear to be a
full license. Briefcase will generate a `copyright` file for your project; you should
ensure that the contents of this file is adequate.
"""
)
else:
raise BriefcaseCommandError(
"""\
Your project does not contain a LICENSE file.
Your project does not contain a LICENSE definition.

Create a file named `LICENSE` in the same directory as your `pyproject.toml`
with your app's licensing terms.
with your app's licensing terms, and set `license.file = 'LICENSE'` in your
app's configuration.
"""
)

Expand Down Expand Up @@ -792,7 +814,11 @@ class LinuxSystemRunCommand(LinuxSystemMixin, RunCommand):
supported_host_os_reason = "Linux system projects can only be executed on Linux."

def run_app(
self, app: AppConfig, test_mode: bool, passthrough: list[str], **kwargs
self,
app: AppConfig,
test_mode: bool,
passthrough: list[str],
**kwargs,
):
"""Start the application.

Expand Down Expand Up @@ -1037,7 +1063,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",
"",
Expand Down Expand Up @@ -1196,7 +1222,7 @@ def _package_pkg(self, app: AppConfig, **kwargs): # pragma: no-cover-if-is-wind
f'pkgdesc="{app.description}"',
f"arch=('{self.pkg_abi(app)}')",
f'url="{app.url}"',
f"license=('{app.license}')",
"license=('Unknown')",
f"depends=({system_runtime_requires})",
"changelog=CHANGELOG",
'source=("$pkgname-$pkgver.tar.gz")',
Expand Down
1 change: 1 addition & 0 deletions tests/commands/base/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,4 +114,5 @@ def my_app():
version="1.2.3",
description="This is a simple app",
sources=["src/my_app"],
license={"file": "LICENSE"},
)
2 changes: 2 additions & 0 deletions tests/commands/base/test_finalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def first_app():
version="0.0.1",
description="The first simple app",
sources=["src/first"],
license={"file": "LICENSE"},
)


Expand All @@ -24,6 +25,7 @@ def second_app():
version="0.0.2",
description="The second simple app",
sources=["src/second"],
license={"file": "LICENSE"},
)


Expand Down
5 changes: 5 additions & 0 deletions tests/commands/base/test_parse_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
""",
Expand All @@ -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]
""",
Expand All @@ -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']
Expand Down Expand Up @@ -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']
Expand Down Expand Up @@ -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']
Expand Down
1 change: 1 addition & 0 deletions tests/commands/build/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ def second_app_config():
version="0.0.2",
description="The second simple app",
sources=["src/second"],
license={"file": "LICENSE"},
)


Expand Down
57 changes: 52 additions & 5 deletions tests/commands/convert/test_migrate_necessary_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, "")
Expand All @@ -121,10 +140,34 @@ def test_pep621_wrong_license_filename(
test_source_dir,
dummy_app_name,
)
assert not (convert_command.base_path / "LICENSE").exists()


@pytest.mark.parametrize("test_source_dir", ["tests"])
def test_pep621_specified_license_text(
convert_command,
project_dir_with_files,
dummy_app_name,
test_source_dir,
):
"""A license file is copied if the license is specified as text and no LICENSE file
exists."""
convert_command.logger.warning = mock.MagicMock()
create_file(
convert_command.base_path / "pyproject.toml",
'[project]\nlicense = { text = "New BSD" }',
)
create_file(convert_command.base_path / "CHANGELOG", "")
convert_command.migrate_necessary_files(
project_dir_with_files,
test_source_dir,
dummy_app_name,
)
assert (convert_command.base_path / "LICENSE").exists()

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."
f"\nLicense file not found in '{convert_command.base_path}'. "
"Briefcase will create a template 'LICENSE' file."
)


Expand Down Expand Up @@ -162,7 +205,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,
Expand Down
3 changes: 3 additions & 0 deletions tests/commands/create/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,15 @@ 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",
bundle="com.example",
version="0.0.2",
description="The second simple app",
sources=["src/second"],
license={"file": "LICENSE"},
),
},
)
Expand All @@ -223,6 +225,7 @@ def myapp():
url="https://example.com",
author="First Last",
author_email="first@example.com",
license={"file": "LICENSE"},
)


Expand Down
1 change: 1 addition & 0 deletions tests/commands/create/test_create_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
)
)

Expand Down
1 change: 1 addition & 0 deletions tests/commands/create/test_generate_app_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading