diff --git a/changes/2080.feature.rst b/changes/2080.feature.rst new file mode 100644 index 000000000..a0beea04a --- /dev/null +++ b/changes/2080.feature.rst @@ -0,0 +1 @@ +The web template now targets PyScript version 2024.11.1. In addition, the web template can provide a base ``pyscript.toml`` that Briefcase will update as required during the build process. diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 5e09a94a9..87132e715 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -32,6 +32,7 @@ class StaticWebMixin: output_format = "static" platform = "web" + platform_target_version = "0.3.21" def project_path(self, app): return self.bundle_path(app) / "www" @@ -180,33 +181,40 @@ def build_app(self, app: AppConfig, **kwargs): ) from e with self.input.wait_bar("Writing Pyscript configuration file..."): + # Load any pre-existing pyscript.toml provided by the template. If the file + # doesn't exist, assume an empty pyscript.toml as a starting point. + try: + with (self.project_path(app) / "pyscript.toml").open("rb") as f: + config = tomllib.load(f) + except tomllib.TOMLDecodeError as e: + raise BriefcaseConfigError( + f"pyscript.toml content isn't valid TOML: {e}" + ) from e + except FileNotFoundError: + config = {} + + # Add the packages declaration to the existing pyscript.toml. + # Ensure that we're using Unix path separators, as the content + # will be parsed by pyscript in the browser. + config["packages"] = [ + f'/{"/".join(wheel.relative_to(self.project_path(app)).parts)}' + for wheel in sorted(self.wheel_path(app).glob("*.whl")) + ] + + # Parse any additional pyscript.toml content, and merge it into + # the overall content + try: + extra = tomllib.loads(app.extra_pyscript_toml_content) + config.update(extra) + except tomllib.TOMLDecodeError as e: + raise BriefcaseConfigError( + f"Extra pyscript.toml content isn't valid TOML: {e}" + ) from e + except AttributeError: + pass + + # Write the final configuration. with (self.project_path(app) / "pyscript.toml").open("wb") as f: - config = { - "name": app.formal_name, - "description": app.description, - "version": app.version, - "splashscreen": {"autoclose": True}, - "terminal": False, - # Ensure that we're using Unix path separators, as the content - # will be parsed by pyscript in the browser. - "packages": [ - f'/{"/".join(wheel.relative_to(self.project_path(app)).parts)}' - for wheel in sorted(self.wheel_path(app).glob("*.whl")) - ], - } - # Parse any additional pyscript.toml content, and merge it into - # the overall content - try: - extra = tomllib.loads(app.extra_pyscript_toml_content) - config.update(extra) - except tomllib.TOMLDecodeError as e: - raise BriefcaseConfigError( - f"Extra pyscript.toml content isn't valid TOML: {e}" - ) from e - except AttributeError: - pass - - # Write the final configuration. tomli_w.dump(config, f) self.logger.info("Compile static web content from wheels") diff --git a/tests/platforms/web/static/conftest.py b/tests/platforms/web/static/conftest.py index 3a43f77bb..d71fd25d4 100644 --- a/tests/platforms/web/static/conftest.py +++ b/tests/platforms/web/static/conftest.py @@ -19,6 +19,15 @@ def first_app_generated(first_app_config, tmp_path): # Create index.html create_file(bundle_path / "www/index.html", "") + # Create the initial pyscript.toml + create_file( + bundle_path / "www/pyscript.toml", + """ +existing-key-1 = "value-1" +existing-key-2 = 2 +""", + ) + # Create the initial briefcase.css create_file( bundle_path / "www/static/css/briefcase.css", diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index b4927eccc..0aa40e7cc 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -15,7 +15,7 @@ from briefcase.integrations.subprocess import Subprocess from briefcase.platforms.web.static import StaticWebBuildCommand -from ....utils import create_wheel +from ....utils import create_file, create_wheel @pytest.fixture @@ -124,11 +124,8 @@ def mock_run(*args, **kwargs): # Pyscript.toml has been written with (bundle_path / "www/pyscript.toml").open("rb") as f: assert tomllib.load(f) == { - "name": "First App", - "description": "The first simple app \\ demonstration", - "version": "0.0.1", - "splashscreen": {"autoclose": True}, - "terminal": False, + "existing-key-1": "value-1", + "existing-key-2": 2, "packages": [ "/static/wheels/dependency-1.2.3-py3-none-any.whl", "/static/wheels/first_app-1.2.3-py3-none-any.whl", @@ -205,12 +202,10 @@ def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_ # Pyscript.toml has been written with (bundle_path / "www/pyscript.toml").open("rb") as f: assert tomllib.load(f) == { - "name": "First App", - "description": "The first simple app \\ demonstration", - "version": "0.0.1", + "existing-key-1": "value-1", + "existing-key-2": 2, "something": "custom", "splashscreen": {"wiggle": False}, - "terminal": False, "packages": ["something-custom"], "runtimes": [ {"src": "https://example.com/pyodide.js"}, @@ -218,7 +213,57 @@ def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_ } -def test_build_app_invalid_custom_pyscript_toml( +def test_build_app_no_template_pyscript_toml( + build_command, first_app_generated, tmp_path +): + """An app whose template doesn't provide pyscript.toml gets a basic config.""" + # Remove the templated pyscript.toml + bundle_path = tmp_path / "base_path/build/first-app/web/static" + (bundle_path / "www/pyscript.toml").unlink() + + # Mock the side effect of invoking shutil + build_command.tools.shutil.rmtree.side_effect = lambda *args: shutil.rmtree( + bundle_path / "www/static/wheels" + ) + + # Build the web app. + build_command.build_app(first_app_generated) + + # Pyscript.toml has been written with only the packages content + with (bundle_path / "www/pyscript.toml").open("rb") as f: + assert tomllib.load(f) == { + "packages": [], + } + + +def test_build_app_invalid_template_pyscript_toml( + build_command, first_app_generated, tmp_path +): + """An app with an invalid pyscript.toml raises an error.""" + # Re-write an invalid templated pyscript.toml + bundle_path = tmp_path / "base_path/build/first-app/web/static" + (bundle_path / "www/pyscript.toml").unlink() + create_file( + bundle_path / "www/pyscript.toml", + """ +This is not valid toml. +""", + ) + + # Mock the side effect of invoking shutil + build_command.tools.shutil.rmtree.side_effect = lambda *args: shutil.rmtree( + bundle_path / "www/static/wheels" + ) + + # Building the web app raises an error + with pytest.raises( + BriefcaseConfigError, + match=r"Briefcase configuration error: pyscript.toml content isn't valid TOML: Expected", + ): + build_command.build_app(first_app_generated) + + +def test_build_app_invalid_extra_pyscript_toml_content( build_command, first_app_generated, tmp_path ): """An app with invalid extra pyscript.toml content raises an error.""" @@ -374,11 +419,8 @@ def mock_run(*args, **kwargs): # Pyscript.toml has been written with (bundle_path / "www/pyscript.toml").open("rb") as f: assert tomllib.load(f) == { - "name": "First App", - "description": "The first simple app \\ demonstration", - "version": "0.0.1", - "splashscreen": {"autoclose": True}, - "terminal": False, + "existing-key-1": "value-1", + "existing-key-2": 2, "packages": [ "/static/wheels/first_app-1.2.3-py3-none-any.whl", ], @@ -458,8 +500,12 @@ def test_app_package_fail(build_command, first_app_generated, tmp_path): # Wheels folder still exists assert (bundle_path / "www/static/wheels").is_dir() - # Pyscript.toml was not written - assert not (bundle_path / "www/pyscript.toml").exists() + # Pyscript.toml content has not changed + with (bundle_path / "www/pyscript.toml").open("rb") as f: + assert tomllib.load(f) == { + "existing-key-1": "value-1", + "existing-key-2": 2, + } def test_dependency_fail(build_command, first_app_generated, tmp_path): @@ -530,5 +576,9 @@ def test_dependency_fail(build_command, first_app_generated, tmp_path): # Wheels folder still exists assert (bundle_path / "www/static/wheels").is_dir() - # Pyscript.toml was not written - assert not (bundle_path / "www/pyscript.toml").exists() + # Pyscript.toml content has not changed + with (bundle_path / "www/pyscript.toml").open("rb") as f: + assert tomllib.load(f) == { + "existing-key-1": "value-1", + "existing-key-2": 2, + }