diff --git a/changes/2699.bugfix.md b/changes/2699.bugfix.md new file mode 100644 index 000000000..3f48d1e9e --- /dev/null +++ b/changes/2699.bugfix.md @@ -0,0 +1 @@ +Debian packages can now be built for projects that contain an underscore in the app name. Previously, this would raise an error as underscores are not allowed in Debian package names. The Debian package name will be based on the bundle name - that is, the app name, but with underscores replaced with hyphens. diff --git a/changes/2699.removal.md b/changes/2699.removal.md new file mode 100644 index 000000000..2f61c5c9e --- /dev/null +++ b/changes/2699.removal.md @@ -0,0 +1 @@ +Bundle names and identifiers are now normalized to lower case. Bundle identifiers are derived from domain names, so they *should* always be lower case; Briefcase now enforces this. If you have historically used upper case letters in your bundle identifier or app name, your bundle name will change as a result of this update. diff --git a/src/briefcase/config.py b/src/briefcase/config.py index 0f12572b0..9d5141cf4 100644 --- a/src/briefcase/config.py +++ b/src/briefcase/config.py @@ -470,7 +470,7 @@ def __init__( self.app_name = app_name self.version = version - self.bundle = bundle + self.bundle = bundle.lower() # Description can only be a single line. Ignore everything else. self.description = description.split("\n")[0] self.sources = sources @@ -581,7 +581,7 @@ def bundle_name(self): This is derived from the app name, but: * all `_` have been replaced with `-`. """ - return self.app_name.replace("_", "-") + return self.app_name.replace("_", "-").lower() @property def bundle_identifier(self): diff --git a/src/briefcase/platforms/linux/system.py b/src/briefcase/platforms/linux/system.py index 09d3d251b..bdf3718b9 100644 --- a/src/briefcase/platforms/linux/system.py +++ b/src/briefcase/platforms/linux/system.py @@ -113,7 +113,7 @@ def pkg_abi(self, app: AppConfig) -> str: def distribution_filename(self, app: AppConfig) -> str: if app.packaging_format == "deb": return ( - f"{app.app_name}" + f"{app.bundle_name}" f"_{app.version}" f"-{getattr(app, 'revision', 1)}" f"~{app.target_vendor}" @@ -1107,7 +1107,7 @@ def _package_deb(self, app: AppConfig, **kwargs): f.write( "\n".join( [ - f"Package: {app.app_name}", + f"Package: {app.bundle_name}", f"Version: {app.version}", f"Architecture: {self.deb_abi(app)}", f"Maintainer: {app.author} <{app.author_email}>", diff --git a/tests/config/test_AppConfig.py b/tests/config/test_AppConfig.py index 0c9e9a021..fa8c9b362 100644 --- a/tests/config/test_AppConfig.py +++ b/tests/config/test_AppConfig.py @@ -500,3 +500,28 @@ def test_non_unique_uninstall_options(): }, ], ) + + +def test_capitalization(): + """Capitalization is prohibited and normalized out in some properties.""" + config = AppConfig( + app_name="MyApp", + version="1.2.3", + bundle="Org.Beeware", + description="A simple app", + sources=["src/MyApp", "somewhere/else/interesting", "local_app"], + license={"file": "LICENSE"}, + ) + + # The basic properties have been set. + assert config.app_name == "MyApp" + assert config.bundle == "org.beeware" + + # Derived properties have been set. + assert config.bundle_name == "myapp" + assert config.bundle_identifier == "org.beeware.myapp" + assert config.formal_name == "MyApp" + assert config.class_name == "MyApp" + + # The object has a meaningful REPL + assert repr(config) == "" diff --git a/tests/platforms/linux/system/conftest.py b/tests/platforms/linux/system/conftest.py index 97e64db49..c2abd7cef 100644 --- a/tests/platforms/linux/system/conftest.py +++ b/tests/platforms/linux/system/conftest.py @@ -1,5 +1,6 @@ import pytest +from briefcase.config import AppConfig from briefcase.platforms.linux.system import LinuxSystemCreateCommand from ....utils import create_file @@ -59,3 +60,41 @@ def first_app(first_app_config, tmp_path): (lib_dir / "app_packages/secondlib/second_b.so").touch() return first_app_config + + +@pytest.fixture +def underscore_app(tmp_path): + """A fixture for an app with an underscore in the name, rolled out on disk.""" + app_config = AppConfig( + app_name="underscore_app", + project_name="Underscore Project", + formal_name="Underscore App", + author="Megacorp", + author_email="maintainer@example.com", + url="https://example.com/underscore_app", + bundle="com.example", + version="0.0.1", + description="The first simple app \\ demonstration", + sources=["src/underscore_app"], + license={"file": "LICENSE"}, + ) + + # Specify a system python app for a dummy vendor + app_config.target_vendor = "somevendor" + app_config.target_codename = "surprising" + app_config.target_vendor_base = "basevendor" + + # Targeted Python version + app_config.python_version_tag = "3" + + # Some project-level files. + create_file(tmp_path / "base_path/LICENSE", "Underscore App License") + create_file(tmp_path / "base_path/CHANGELOG", "Underscore App Changelog") + + # Make it look like the template has been generated + bundle_dir = tmp_path / "base_path/build/underscore_app/somevendor/surprising" + + lib_dir = bundle_dir / "underscore_app-0.0.1/usr/lib/underscore_app" + (lib_dir / "app").mkdir(parents=True, exist_ok=True) + + return app_config diff --git a/tests/platforms/linux/system/test_package__deb.py b/tests/platforms/linux/system/test_package__deb.py index 1f4ecab34..745fc40cc 100644 --- a/tests/platforms/linux/system/test_package__deb.py +++ b/tests/platforms/linux/system/test_package__deb.py @@ -55,6 +55,18 @@ def first_app_deb(first_app): return first_app +@pytest.fixture +def underscore_app_deb(underscore_app): + # Mock a debian app + underscore_app.python_version_tag = "3.10" + underscore_app.target_vendor_base = "debian" + underscore_app.packaging_format = "deb" + underscore_app.glibc_version = "2.99" + underscore_app.long_description = "Long description\nfor the app" + + return underscore_app + + @pytest.fixture def external_first_app_deb(first_app_deb, tmp_path): # Make the app external @@ -244,6 +256,62 @@ def test_deb_re_package(package_command, first_app_deb, tmp_path): ) +@pytest.mark.skipif(sys.platform == "win32", reason="Can't build debs on Windows") +def test_deb_package_underscore(package_command, underscore_app_deb, tmp_path): + """A deb app can be packaged.""" + package_command.tools.app_tools[underscore_app_deb].app_context = mock.MagicMock() + + bundle_path = tmp_path / "base_path/build/underscore_app/somevendor/surprising" + + # Package the app + package_command.package_app(underscore_app_deb) + + # The control file is written + assert (bundle_path / "underscore_app-0.0.1/DEBIAN/control").exists() + with (bundle_path / "underscore_app-0.0.1/DEBIAN/control").open( + encoding="utf-8" + ) as f: + assert ( + f.read() + == "\n".join( + [ + "Package: underscore-app", + "Version: 0.0.1", + "Architecture: wonky", + "Maintainer: Megacorp ", + "Homepage: https://example.com/underscore_app", + "Description: The first simple app \\ demonstration", + " Long description", + " for the app", + "Depends: libc6 (>=2.99), libpython3.10", + "Section: utils", + "Priority: optional", + ] + ) + + "\n" + ) + + package_command.tools.app_tools[ + underscore_app_deb + ].app_context.run.assert_called_once_with( + [ + "dpkg-deb", + "--build", + "--root-owner-group", + bundle_path / "underscore_app-0.0.1", + ], + check=True, + cwd=bundle_path, + ) + + # The deb was moved into the final location + package_command.tools.shutil.move.assert_called_once_with( + bundle_path / "underscore_app-0.0.1.deb", + tmp_path + / "base_path/dist/underscore-app_0.0.1-1~somevendor-surprising_wonky.deb", + ) + + def test_deb_package_no_long_description(package_command, first_app_deb, tmp_path): """A deb app without a long description raises an error.""" bundle_path = tmp_path / "base_path/build/first-app/somevendor/surprising"