From 3e101271bcc63b312bf46fa14db7babf5c9421c8 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Mon, 4 Aug 2025 13:06:08 +0800 Subject: [PATCH 001/105] Update the build logic to support toolkit config and pyscript.toml merging --- src/briefcase/platforms/web/static.py | 41 +++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index fdbed8028..149d79612 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -3,6 +3,7 @@ import sys import webbrowser from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from importlib.resources import files from pathlib import Path from typing import Any from zipfile import ZipFile @@ -131,6 +132,34 @@ def build_app(self, app: AppConfig, **kwargs): """ self.console.info("Building web project...", prefix=app.app_name) + deploy_path = files("toga_web.deploy") + deploy_config_path = deploy_path / "config.toml" + deploy_pyscript_path = deploy_path / "pyscript.toml" + + if deploy_config_path.exists(): + try: + with deploy_config_path.open("rb") as f: + deploy_config = tomllib.load(f) + except tomllib.TOMLDecodeError as e: + raise BriefcaseConfigError(f"Invalid config.toml: {e}") from e + else: + deploy_config = {} + + if "backend" in deploy_config and deploy_config["backend"] != "pyscript": + raise BriefcaseConfigError( + "Only 'pyscript' backend is currently supported for web static builds." + ) + + if deploy_pyscript_path.exists(): + try: + extra_pyscript_toml = deploy_pyscript_path.read_text(encoding="utf-8") + except Exception as e: + raise BriefcaseConfigError( + f"Unable to read deploy/pyscript.toml: {e}" + ) from e + else: + extra_pyscript_toml = "" + if self.wheel_path(app).exists(): with self.console.wait_bar("Removing old wheels..."): self.tools.shutil.rmtree(self.wheel_path(app)) @@ -218,6 +247,18 @@ def build_app(self, app: AppConfig, **kwargs): except AttributeError: pass + # Parse any deploy pyscript.toml content, and merge it into + # the overall content + try: + extra = tomllib.loads(extra_pyscript_toml) + config.update(extra) + except tomllib.TOMLDecodeError as e: + raise BriefcaseConfigError( + f"Deploy 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: tomli_w.dump(config, f) From 107f1e685808260da3bbd6ae09641a29ad70c9fc Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 11 Aug 2025 11:38:29 +0800 Subject: [PATCH 002/105] Added functions to gather backend configuration files Draft to replace code loading pyscript.toml from the web-template files with a process to gather them from toga_web or 3rd party GUI toolkit --- src/briefcase/platforms/web/static.py | 179 +++++++++++++++++++------- 1 file changed, 131 insertions(+), 48 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 149d79612..4aa5fc189 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -1,4 +1,5 @@ import errno +import shutil import subprocess import sys import webbrowser @@ -46,8 +47,11 @@ def project_path(self, app): def binary_path(self, app): return self.bundle_path(app) / "www/index.html" + def static_path(self, app): + return self.project_path(app) / "static" + def wheel_path(self, app): - return self.project_path(app) / "static/wheels" + return self.static_path(app) / "wheels" def distribution_path(self, app): return self.dist_path / f"{app.formal_name}-{app.version}.web.zip" @@ -125,6 +129,93 @@ def _process_wheel(self, wheelfile, css_file): ) css_file.write(wheel.read(filename).decode("utf-8")) + def _gather_backend_config(self, wheels): + """Processes multiple wheels to gather a config.toml and a base pyscript.toml file. + + :param wheels: A list of wheel files to be scanned. + """ + config_counter = 0 + pyscript_config = None + + for wheelfile in wheels: + with ZipFile(wheelfile) as wheel: + for filename in wheel.namelist(): + path = Path(filename) + if ( + len(path.parts) > 1 + and path.parts[1] == "deploy" + and path.suffix == ".toml" + and path.name == "config" + ): + self.console.info(f" Found {filename}") + config_counter += 1 + # Raise an error if more than one configuration file is supplied. + if config_counter > 1: + raise BriefcaseConfigError( + "Only 1 backend configuration file can be supplied." + ) + # Check which backend type is used. Raise error if no backend is present in config.toml + with wheel.open(filename) as config_file: + config_data = tomllib.load(config_file) + + if "backend" in config_data: + backend = config_data.get("backend") + + # Currently, only pyscript is supported, will raise an error if another backend is found. + if backend != "pyscript": + raise BriefcaseConfigError( + "Only 'pyscript' backend is currently supported for web static builds." + ) + + pyscript_config = self._gather_backend_config_file(wheel, backend, path) + + else: + raise BriefcaseConfigError( + 'No backend was provided in config.toml file.' + ) + # Return a blank pyscript config if no configuration file is found. + if ( + config_counter == 0 + and pyscript_config is None + ): + pyscript_config = {} + + return pyscript_config + + def _gather_backend_config_file(self, wheel, backend, path): + """Find backend config file (eg: pyscript.toml) from a wheel and save it to project pyscript.toml if found. + + :param wheel: Wheel file to scan for configuration file. + :param backend: The backend type as a String (eg "pyscript") + :param path: Path to the wheels configuration file (config.toml). This should be in the same directory as the backend configuration file. + """ + backend_counter = 0 + backend_config = None + deploy_dir = path.parent + + for deploy_file in wheel.namelist(): + deploy_parent = Path(deploy_file).parent + if ( + deploy_parent == deploy_dir + and Path(deploy_file).name == f"{backend}.toml" + ): + backend_counter += 1 + # Raise an error if more than one pyscript.toml file is found. + if backend_counter > 1: + raise BriefcaseConfigError( + "Only 1 pyscript configuration file can be supplied." + ) + # Save pyscript config file. + else: + try: + with wheel.open(deploy_file) as pyscript_file: + backend_config = tomllib.load(pyscript_file) + except tomllib.TOMLDecodeError as e: + raise BriefcaseConfigError( + f"pyscript.toml content isn't valid TOML: {e}" + ) from e + return backend_config + def build_app(self, app: AppConfig, **kwargs): """Build the static web deployment for the application. @@ -132,33 +223,33 @@ def build_app(self, app: AppConfig, **kwargs): """ self.console.info("Building web project...", prefix=app.app_name) - deploy_path = files("toga_web.deploy") - deploy_config_path = deploy_path / "config.toml" - deploy_pyscript_path = deploy_path / "pyscript.toml" - - if deploy_config_path.exists(): - try: - with deploy_config_path.open("rb") as f: - deploy_config = tomllib.load(f) - except tomllib.TOMLDecodeError as e: - raise BriefcaseConfigError(f"Invalid config.toml: {e}") from e - else: - deploy_config = {} - - if "backend" in deploy_config and deploy_config["backend"] != "pyscript": - raise BriefcaseConfigError( - "Only 'pyscript' backend is currently supported for web static builds." - ) - - if deploy_pyscript_path.exists(): - try: - extra_pyscript_toml = deploy_pyscript_path.read_text(encoding="utf-8") - except Exception as e: - raise BriefcaseConfigError( - f"Unable to read deploy/pyscript.toml: {e}" - ) from e - else: - extra_pyscript_toml = "" + # deploy_path = files("toga_web.deploy") + # deploy_config_path = deploy_path / "config.toml" + # deploy_pyscript_path = deploy_path / "pyscript.toml" + + # if deploy_config_path.exists(): + # try: + # with deploy_config_path.open("rb") as f: + # deploy_config = tomllib.load(f) + # except tomllib.TOMLDecodeError as e: + # raise BriefcaseConfigError(f"Invalid config.toml: {e}") from e + # else: + # deploy_config = {} + + # if "backend" in deploy_config and deploy_config["backend"] != "pyscript": + # raise BriefcaseConfigError( + # "Only 'pyscript' backend is currently supported for web static builds." + # ) + + # if deploy_pyscript_path.exists(): + # try: + # extra_pyscript_toml = deploy_pyscript_path.read_text(encoding="utf-8") + # except Exception as e: + # raise BriefcaseConfigError( + # f"Unable to read deploy/pyscript.toml: {e}" + # ) from e + # else: + # extra_pyscript_toml = "" if self.wheel_path(app).exists(): with self.console.wait_bar("Removing old wheels..."): @@ -217,15 +308,7 @@ def build_app(self, app: AppConfig, **kwargs): with self.console.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 = {} + config = self._gather_backend_config(self.wheel_path(app).glob("*.whl")) # Add the packages declaration to the existing pyscript.toml. # Ensure that we're using Unix path separators, as the content @@ -247,17 +330,17 @@ def build_app(self, app: AppConfig, **kwargs): except AttributeError: pass - # Parse any deploy pyscript.toml content, and merge it into - # the overall content - try: - extra = tomllib.loads(extra_pyscript_toml) - config.update(extra) - except tomllib.TOMLDecodeError as e: - raise BriefcaseConfigError( - f"Deploy pyscript.toml content isn't valid TOML: {e}" - ) from e - except AttributeError: - pass + # # Parse any deploy pyscript.toml content, and merge it into + # # the overall content + # try: + # extra = tomllib.loads(extra_pyscript_toml) + # config.update(extra) + # except tomllib.TOMLDecodeError as e: + # raise BriefcaseConfigError( + # f"Deploy 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: From eed7ec4fb47af5b20a679d4299ffc395163211d9 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 11 Aug 2025 13:39:39 +0800 Subject: [PATCH 003/105] If statement logic fix --- src/briefcase/platforms/web/static.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 4aa5fc189..7bef0f467 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -144,8 +144,7 @@ def _gather_backend_config(self, wheels): if ( len(path.parts) > 1 and path.parts[1] == "deploy" - and path.suffix == ".toml" - and path.name == "config" + and path.name == "config.toml" ): self.console.info(f" Found {filename}") config_counter += 1 From 07c14f3d42fe4c450a721cb9dc5b2a3ad0d288d3 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 4 Aug 2025 13:09:18 +0800 Subject: [PATCH 004/105] Added _process_wheel function from PR#1285 --- src/briefcase/platforms/web/static.py | 45 ++++++++++++++++----------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 7bef0f467..8f96f5007 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -106,28 +106,37 @@ def _process_wheel(self, wheelfile, css_file): final project. :param wheelfile: The path to the wheel file to be processed. - :param css_file: A file handle, opened for write/append, to which any extracted - CSS content will be appended. + :param inserts: The insert collection of html for the app + :param static_path: The location where static content should be unpacked """ - package = " ".join(wheelfile.name.split("-")[:2]) + parts = wheelfile.name.split("-") + package_name = parts[0] + package_version = parts[1] + package_key = f"{package_name} {package_version}" + + if (static_path / package_name).exists(): + self.tools.shutil.rmtree(static_path / package_name) + with ZipFile(wheelfile) as wheel: for filename in wheel.namelist(): path = Path(filename) - # Any CSS file in a `static` folder is appended - if ( - len(path.parts) > 1 - and path.parts[1] == "static" - and path.suffix == ".css" - ): - self.console.info(f" Found {filename}") - css_file.write( - "\n/*******************************************************\n" - ) - css_file.write(f" * {package}::{'/'.join(path.parts[2:])}\n") - css_file.write( - " *******************************************************/\n\n" - ) - css_file.write(wheel.read(filename).decode("utf-8")) + if len(path.parts) > 1: + if path.parts[1] == "inserts": + source = str(Path(*path.parts[2:])) + content = wheel.read(filename).decode("utf-8") + if ":" in path.name: + target, insert = source.split(":") + self.logger.info( + f" {source}: Adding {insert} insert for {target}" + ) + else: + target = path.suffix[1:].upper() + insert = source + self.logger.info(f" {source}: Adding {target} insert") + + insert.setdefault(target, {}).setdefault(insert, {})[ + + ] def _gather_backend_config(self, wheels): """Processes multiple wheels to gather a config.toml and a base pyscript.toml file. From 251914fab641e23ead5b671f770a5fc20195ab43 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 4 Aug 2025 13:37:01 +0800 Subject: [PATCH 005/105] Additional _process_wheel content from PR#1285 --- src/briefcase/platforms/web/static.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 8f96f5007..aef63a2ef 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -101,10 +101,18 @@ def _trim_file(self, path, sentinel): for line in content: f.write(line) - def _process_wheel(self, wheelfile, css_file): + def _process_wheel(self, wheelfile, static_path): """Process a wheel, extracting any content that needs to be compiled into the final project. + Extracted content is received in two forms: + * inserts - html content to be inserted into existing html files. + * static - content to be copied as a whole. Content in a ``static`` + folder inside the wheel is copied as-is to the static folder, + namespaced by the package name of the wheel. + + Any pre-existing static content for the wheel will be deleted. + :param wheelfile: The path to the wheel file to be processed. :param inserts: The insert collection of html for the app :param static_path: The location where static content should be unpacked @@ -135,8 +143,14 @@ def _process_wheel(self, wheelfile, css_file): self.logger.info(f" {source}: Adding {target} insert") insert.setdefault(target, {}).setdefault(insert, {})[ - - ] + package_key + ] = content + elif path.parts[1] == "static": + content = wheel.read(filename) + outfilename = static_path / package_name / Path(*path.parts[2:]) + outfilename.parent.mkdir(parents=True, exist_ok=True) + with outfilename.open("wb") as f: + f.write(content) def _gather_backend_config(self, wheels): """Processes multiple wheels to gather a config.toml and a base pyscript.toml file. From ac2a30ad012fc88952f7b592e937d1f7e134a540 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 4 Aug 2025 20:29:04 +0800 Subject: [PATCH 006/105] Added _merge_insert_content and _write_inserts functions from PR#1285 --- src/briefcase/platforms/web/static.py | 281 ++++++++++++++++++-------- 1 file changed, 202 insertions(+), 79 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index aef63a2ef..be144468f 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -3,6 +3,7 @@ import subprocess import sys import webbrowser +import re from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from importlib.resources import files from pathlib import Path @@ -51,7 +52,7 @@ def static_path(self, app): return self.project_path(app) / "static" def wheel_path(self, app): - return self.static_path(app) / "wheels" + return self.static_path / "wheels" def distribution_path(self, app): return self.dist_path / f"{app.formal_name}-{app.version}.web.zip" @@ -101,20 +102,142 @@ def _trim_file(self, path, sentinel): for line in content: f.write(line) - def _process_wheel(self, wheelfile, static_path): + def _merge_insert_content(self, inserts, key, path): + """Merge multi-file insert content into a single insert file. + + Rewrites the inserts, removing the entry for ``key``, + producing a merged entry for ``path`` that has a single + ``key`` insert. + This is used to merge multiple contributed CSS files into + a single CSS insert. + + :param inserts: All inserts + :param key: The key to merge + :param path: The path for the merge insert. + """ + + try: + original = inserts.pop(key) + except KeyError: + # No merging + pass + else: + merged = {} + for filename, package_inserts in original.items(): + for package, css in package_inserts.items(): + try: + old_css = merged[package] + except KeyError: + old_css = "" + + full_css = f"{old_css}/********** {filename} **********/\n{css}\n" + merged[package] = full_css + + # Preserve the merged content as a single insert + inserts[path] = {key: merged} + + def _write_inserts(self, app: AppConfig, filename: Path, inserts: dict): + """Write inserts into an existing file. + + This function looks for start and end markers in the named file and + replaces the content inside the markers with the inserted content. + + Multiple formats of insert marker are inspected to accommodate HTML + and CSS/JS comment conventions: + * HTML: ```` and ```` + * CSS/JS: ``/*@@ insert:start @@*/`` and ``/*@@ insert:end @@*/`` + + :param app: The application whose ``pyscript.toml`` is being written. + :param filename: The file whose insert is to be written. + :param inserts: The inserts for the file. A 2 level dictionary, keyed by + the name of the insert to add, and then package that contributed the + insert. + """ + # Read the current content + target_path = self.project_path(app) / filename + if not target_path.exists(): + self.console.warning(f" Target {filename} not found; skipping inserts.") + return + + with target_path.open("r", encoding="utf-8") as f: + file_text = f.read() + + for insert in sorted(inserts.keys()): + packages = inserts[insert] + + html_banner = ( + "\n" + "{content}" + ) + css_banner = ( + "/**************************************************\n" + " * {package}\n" + " *************************************************/\n" + "{content}" + ) + + marker_styles = [ + # HTML + ( + r".*?", + r"\n{content}", + "html", + ), + # CSS/JS + ( + r"/\*@@ {insert}:start @@\*/.*?/\*@@ {insert}:end @@\*/", + r"/*@@ {insert}:start @@*/\n{content}/*@@ {insert}:end @@*/", + "css", + ), + ] + + html_body = "\n".join( + html_banner.format(package=pkg, content=packages[pkg]) + for pkg in sorted(packages.keys()) + ) + css_body = "\n".join( + css_banner.format(package=pkg, content=packages[pkg]) + for pkg in sorted(packages.keys()) + ) + + replaced = False + for pattern_tmpl, repl_tmpl, kind in marker_styles: + pattern = re.compile( + pattern_tmpl.format(nsert=insert), flags=re.MULTILINE | re.DOTALL + ) + if pattern.search(file_text): + body = html_body if kind == "html" else css_body + file_text = pattern.sub( + repl_tmpl.format(insert=insert, content=body), + file_text, + ) + replaced = True + break + + if not replaced: + self.console.warning( + f" Slot '{insert}' markers not found in {filename}; skipping." + ) + + with target_path.open("w", encoding="utf-8") as f: + f.write(file_text) + + def _process_wheel(self, wheelfile, inserts, static_path): """Process a wheel, extracting any content that needs to be compiled into the final project. - Extracted content is received in two forms: - * inserts - html content to be inserted into existing html files. - * static - content to be copied as a whole. Content in a ``static`` - folder inside the wheel is copied as-is to the static folder, - namespaced by the package name of the wheel. + Extracted content comes in two forms: + * inserts - pieces of content that will be inserted into existing files + * static - content that will be copied wholesale. Any content in a ``static`` + folder inside the wheel will be copied as-is to the static folder, + namespaced by the package name of the wheel. Any pre-existing static content for the wheel will be deleted. :param wheelfile: The path to the wheel file to be processed. - :param inserts: The insert collection of html for the app + :param inserts: The inserts collection for the app :param static_path: The location where static content should be unpacked """ parts = wheelfile.name.split("-") @@ -122,35 +245,64 @@ def _process_wheel(self, wheelfile, static_path): package_version = parts[1] package_key = f"{package_name} {package_version}" - if (static_path / package_name).exists(): - self.tools.shutil.rmtree(static_path / package_name) + # Purge any existing extracted static files for this wheel + pkg_static_root = static_path / package_name + if pkg_static_root.exists(): + self.tools.shutil.rmtree(pkg_static_root) with ZipFile(wheelfile) as wheel: for filename in wheel.namelist(): path = Path(filename) - if len(path.parts) > 1: - if path.parts[1] == "inserts": - source = str(Path(*path.parts[2:])) - content = wheel.read(filename).decode("utf-8") - if ":" in path.name: - target, insert = source.split(":") - self.logger.info( - f" {source}: Adding {insert} insert for {target}" - ) - else: - target = path.suffix[1:].upper() - insert = source - self.logger.info(f" {source}: Adding {target} insert") - - insert.setdefault(target, {}).setdefault(insert, {})[ - package_key - ] = content - elif path.parts[1] == "static": - content = wheel.read(filename) - outfilename = static_path / package_name / Path(*path.parts[2:]) - outfilename.parent.mkdir(parents=True, exist_ok=True) - with outfilename.open("wb") as f: - f.write(content) + if len(path.parts) < 2: + continue + + is_inserts = (path.parts[1] == "inserts") or ( + len(path.parts) >= 3 + and path.parts[1] == "deploy" + and path.parts[2] == "inserts" + ) + if is_inserts and path.name: + source = ( + str(Path(*path.parts[2:])) + if path.parts[1] == "inserts" + else str(Path(*path.parts[3:])) + ) + if ":" not in path.name: + self.console.warning( + f" {source}: missing ':'; skipping insert." + ) + continue + target, insert = source.split(":", 1) + self.console.info( + f" {source}: Adding {insert} insert for {target}" + ) + try: + text = wheel.read(filename).decode("utf-8") + except UnicodeDecodeError: + self.console.warning( + f" {source}: non-UTF8 insert; skipping." + ) + continue + inserts.setdefault(target, {}).setdefault(insert, {})[ + package_key + ] = text + continue + + is_static = (path.parts[1] == "static") or ( + len(path.parts) >= 3 + and path.parts[1] == "deploy" + and path.parts[2] == "static" + ) + if is_static: + if filename.endswith("/"): + continue + rel_parts = ( + path.parts[2:] if path.parts[1] == "static" else path.parts[3:] + ) + outfilename = pkg_static_root / Path(*rel_parts) + outfilename.parent.mkdir(parents=True, exist_ok=True) + with outfilename.open("wb") as f: + f.write(wheel.read(filename)) def _gather_backend_config(self, wheels): """Processes multiple wheels to gather a config.toml and a base pyscript.toml file. @@ -205,11 +357,13 @@ def _gather_backend_config(self, wheels): return pyscript_config def _gather_backend_config_file(self, wheel, backend, path): - """Find backend config file (eg: pyscript.toml) from a wheel and save it to project pyscript.toml if found. + """Find backend config file (eg: pyscript.toml) from a wheel and save it to + project pyscript.toml if found. :param wheel: Wheel file to scan for configuration file. :param backend: The backend type as a String (eg "pyscript") - :param path: Path to the wheels configuration file (config.toml). This should be in the same directory as the backend configuration file. + :param path: Path to the wheels configuration file (config.toml). This should be + in the same directory as the backend configuration file. """ backend_counter = 0 backend_config = None @@ -245,34 +399,6 @@ def build_app(self, app: AppConfig, **kwargs): """ self.console.info("Building web project...", prefix=app.app_name) - # deploy_path = files("toga_web.deploy") - # deploy_config_path = deploy_path / "config.toml" - # deploy_pyscript_path = deploy_path / "pyscript.toml" - - # if deploy_config_path.exists(): - # try: - # with deploy_config_path.open("rb") as f: - # deploy_config = tomllib.load(f) - # except tomllib.TOMLDecodeError as e: - # raise BriefcaseConfigError(f"Invalid config.toml: {e}") from e - # else: - # deploy_config = {} - - # if "backend" in deploy_config and deploy_config["backend"] != "pyscript": - # raise BriefcaseConfigError( - # "Only 'pyscript' backend is currently supported for web static builds." - # ) - - # if deploy_pyscript_path.exists(): - # try: - # extra_pyscript_toml = deploy_pyscript_path.read_text(encoding="utf-8") - # except Exception as e: - # raise BriefcaseConfigError( - # f"Unable to read deploy/pyscript.toml: {e}" - # ) from e - # else: - # extra_pyscript_toml = "" - if self.wheel_path(app).exists(): with self.console.wait_bar("Removing old wheels..."): self.tools.shutil.rmtree(self.wheel_path(app)) @@ -352,18 +478,6 @@ def build_app(self, app: AppConfig, **kwargs): except AttributeError: pass - # # Parse any deploy pyscript.toml content, and merge it into - # # the overall content - # try: - # extra = tomllib.loads(extra_pyscript_toml) - # config.update(extra) - # except tomllib.TOMLDecodeError as e: - # raise BriefcaseConfigError( - # f"Deploy 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: tomli_w.dump(config, f) @@ -377,14 +491,23 @@ def build_app(self, app: AppConfig, **kwargs): sentinel=" ******************* Wheel contributed styles **********************/", ) - # Extract static resources from packaged wheels + inserts: dict[str, dict[str, dict[str, str]]] = {} + static_root = self.static_path(app) + for wheelfile in sorted(self.wheel_path(app).glob("*.whl")): self.console.info(f" Processing {wheelfile.name}...") - with briefcase_css_path.open("a", encoding="utf-8") as css_file: - self._process_wheel(wheelfile, css_file=css_file) + self._process_wheel( + wheelfile=wheelfile, + inserts=inserts, + static_path=static_root, + ) - return {} + # Write inserts per target + for target in sorted(inserts.keys()): + self._write_inserts(app, Path(target), inserts[target]) + return {} + class HTTPHandler(SimpleHTTPRequestHandler): """Convert any HTTP request into a path request on the static content folder.""" From 2ad86c2eda69301ca78e61cbd295da175584ed95 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Mon, 11 Aug 2025 16:30:01 +0800 Subject: [PATCH 007/105] Add phase 3 insert injection support with HTML & CSS markers --- src/briefcase/platforms/web/static.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index be144468f..b95b40dbf 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -1,9 +1,9 @@ import errno import shutil +import re import subprocess import sys import webbrowser -import re from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from importlib.resources import files from pathlib import Path @@ -52,7 +52,7 @@ def static_path(self, app): return self.project_path(app) / "static" def wheel_path(self, app): - return self.static_path / "wheels" + return self.static_path(app) / "wheels" def distribution_path(self, app): return self.dist_path / f"{app.formal_name}-{app.version}.web.zip" From 48b47ad78b0a434124a5b40a785c5ca60bc4ff2c Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 18 Aug 2025 14:17:59 +0800 Subject: [PATCH 008/105] More specific configuration file exception and edits to pyscript.toml finding _gather_backend_config_file function might be obsolete. This now attempts to find pyscript.toml in the same directory as config.toml using wheel.open() --- src/briefcase/platforms/web/static.py | 78 ++++++++++++++------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index b95b40dbf..c4e2fbdca 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -310,6 +310,7 @@ def _gather_backend_config(self, wheels): :param wheels: A list of wheel files to be scanned. """ config_counter = 0 + config_package = None pyscript_config = None for wheelfile in wheels: @@ -326,8 +327,11 @@ def _gather_backend_config(self, wheels): # Raise an error if more than one configuration file is supplied. if config_counter > 1: raise BriefcaseConfigError( - "Only 1 backend configuration file can be supplied." + f"""Only 1 backend configuration file can be supplied. + Initial config.toml found in package: {config_package} + Duplicate config.toml found in package: {wheel.filename}""" ) + config_package = wheel.filename # Check which backend type is used. Raise error if no backend is present in config.toml with wheel.open(filename) as config_file: config_data = tomllib.load(config_file) @@ -341,7 +345,15 @@ def _gather_backend_config(self, wheels): "Only 'pyscript' backend is currently supported for web static builds." ) - pyscript_config = self._gather_backend_config_file(wheel, backend, path) + # Try to find pyscript.toml configuration file. + try: + with wheel.open(f"{path.parent}/pyscript.toml") as pyscript_file: + pyscript_config = tomllib.load(pyscript_file) + except KeyError: + raise BriefcaseConfigError( + f"Pyscript configuration file not found in package: {config_package}" + ) + # pyscript_config = self._gather_backend_config_file(wheel, backend, path) else: raise BriefcaseConfigError( @@ -356,41 +368,33 @@ def _gather_backend_config(self, wheels): return pyscript_config - def _gather_backend_config_file(self, wheel, backend, path): - """Find backend config file (eg: pyscript.toml) from a wheel and save it to - project pyscript.toml if found. - - :param wheel: Wheel file to scan for configuration file. - :param backend: The backend type as a String (eg "pyscript") - :param path: Path to the wheels configuration file (config.toml). This should be - in the same directory as the backend configuration file. - """ - backend_counter = 0 - backend_config = None - deploy_dir = path.parent - - for deploy_file in wheel.namelist(): - deploy_parent = Path(deploy_file).parent - if ( - deploy_parent == deploy_dir - and Path(deploy_file).name == f"{backend}.toml" - ): - backend_counter += 1 - # Raise an error if more than one pyscript.toml file is found. - if backend_counter > 1: - raise BriefcaseConfigError( - "Only 1 pyscript configuration file can be supplied." - ) - # Save pyscript config file. - else: - try: - with wheel.open(deploy_file) as pyscript_file: - backend_config = tomllib.load(pyscript_file) - except tomllib.TOMLDecodeError as e: - raise BriefcaseConfigError( - f"pyscript.toml content isn't valid TOML: {e}" - ) from e - return backend_config + # def _gather_backend_config_file(self, wheel, backend, path): + # """Find backend config file (eg: pyscript.toml) from a wheel and save it to + # project pyscript.toml if found. + + # :param wheel: Wheel file to scan for configuration file. + # :param backend: The backend type as a String (eg "pyscript") + # :param path: Path to the wheels configuration file (config.toml). This should be + # in the same directory as the backend configuration file. + # """ + # backend_config = None + # deploy_dir = path.parent + + # for deploy_file in wheel.namelist(): + # deploy_parent = Path(deploy_file).parent + # if ( + # deploy_parent == deploy_dir + # and Path(deploy_file).name == "pyscript.toml" + # ): + # # Save pyscript config file. + # try: + # with wheel.open(deploy_file) as pyscript_file: + # backend_config = tomllib.load(pyscript_file) + # except tomllib.TOMLDecodeError as e: + # raise BriefcaseConfigError( + # f"pyscript.toml content isn't valid TOML: {e}" + # ) from e + # return backend_config def build_app(self, app: AppConfig, **kwargs): """Build the static web deployment for the application. From 2504757fffa8079bddd907033e4c9122437ebd13 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Mon, 18 Aug 2025 22:28:19 +0800 Subject: [PATCH 009/105] Address review comments in _write_inserts and wheel processing --- src/briefcase/platforms/web/static.py | 190 ++++++++++++++------------ 1 file changed, 99 insertions(+), 91 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index c4e2fbdca..8dd3b6cfd 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -1,11 +1,9 @@ import errno -import shutil import re import subprocess import sys import webbrowser from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer -from importlib.resources import files from pathlib import Path from typing import Any from zipfile import ZipFile @@ -102,39 +100,39 @@ def _trim_file(self, path, sentinel): for line in content: f.write(line) - def _merge_insert_content(self, inserts, key, path): - """Merge multi-file insert content into a single insert file. - - Rewrites the inserts, removing the entry for ``key``, - producing a merged entry for ``path`` that has a single - ``key`` insert. - This is used to merge multiple contributed CSS files into - a single CSS insert. - - :param inserts: All inserts - :param key: The key to merge - :param path: The path for the merge insert. - """ - - try: - original = inserts.pop(key) - except KeyError: - # No merging - pass - else: - merged = {} - for filename, package_inserts in original.items(): - for package, css in package_inserts.items(): - try: - old_css = merged[package] - except KeyError: - old_css = "" - - full_css = f"{old_css}/********** {filename} **********/\n{css}\n" - merged[package] = full_css - - # Preserve the merged content as a single insert - inserts[path] = {key: merged} + # def _merge_insert_content(self, inserts, key, path): + # """Merge multi-file insert content into a single insert file. + + # Rewrites the inserts, removing the entry for ``key``, + # producing a merged entry for ``path`` that has a single + # ``key`` insert. + # This is used to merge multiple contributed CSS files into + # a single CSS insert. + + # :param inserts: All inserts + # :param key: The key to merge + # :param path: The path for the merge insert. + # """ + + # try: + # original = inserts.pop(key) + # except KeyError: + # No merging + # pass + # else: + # merged = {} + # for filename, package_inserts in original.items(): + # for package, css in package_inserts.items(): + # try: + # old_css = merged[package] + # except KeyError: + # old_css = "" + + # full_css = f"{old_css}/********** {filename} **********/\n{css}\n" + # merged[package] = full_css + + # Preserve the merged content as a single insert + # inserts[path] = {key: merged} def _write_inserts(self, app: AppConfig, filename: Path, inserts: dict): """Write inserts into an existing file. @@ -153,18 +151,16 @@ def _write_inserts(self, app: AppConfig, filename: Path, inserts: dict): the name of the insert to add, and then package that contributed the insert. """ - # Read the current content + # Load file content, skip if file not found target_path = self.project_path(app) / filename - if not target_path.exists(): + try: + file_text = target_path.read_text(encoding="utf-8") + except FileNotFoundError: self.console.warning(f" Target {filename} not found; skipping inserts.") return - with target_path.open("r", encoding="utf-8") as f: - file_text = f.read() - - for insert in sorted(inserts.keys()): - packages = inserts[insert] - + # Each insert slot may have multiple package contributions + for insert, packages in inserts.items(): html_banner = ( "`` and ```` * CSS/JS: ``/*@@ insert:start @@*/`` and ``/*@@ insert:end @@*/`` + Inserts and package contributions are processed in sorted order to ensure deterministic builds. + :param app: The application whose ``pyscript.toml`` is being written. :param filename: The file whose insert is to be written. :param inserts: The inserts for the file. A 2 level dictionary, keyed by @@ -159,8 +127,10 @@ def _write_inserts(self, app: AppConfig, filename: Path, inserts: dict): self.console.warning(f" Target {filename} not found; skipping inserts.") return - # Each insert slot may have multiple package contributions - for insert, packages in inserts.items(): + # Each insert slot and its package contributions are processed in sorted order + for insert, packages in sorted(inserts.items()): + packages = inserts[insert] + html_banner = ( "`` + * Example: ```` + + Pyscript versions are processed in sorted order to ensure deterministic builds. + + :param app: The application being written. + :param filename: The html file whose pyscript version is to be written. + :param pyscript_version: The pyscript version number to be inserted. + """ + # Load file content, skip if file not found + target_path = self.project_path(app) / filename + try: + file_text = target_path.read_text(encoding="utf-8") + except FileNotFoundError: + raise BriefcaseConfigError(f"{filename} not found; pyscript version could not be inserted.") + + marker = "" + if marker in file_text: + file_text = file_text.replace(marker, pyscript_version) + else: + raise BriefcaseConfigError(f"No pyscript markers found in {filename}; pyscript may not be configured correctly.") + + target_path.write_text(file_text, encoding="utf-8") + def _process_wheel( self, wheelfile, inserts: dict[str, dict[str, dict[str, str]]], static_path ): @@ -293,6 +325,7 @@ def extract_backend_config(self, wheels): config_package = None config_package_list = [] config_filename = None + pyscript_version = "2024.11.1" pyscript_config = None # Find packages containing a config.toml file. @@ -335,6 +368,11 @@ def extract_backend_config(self, wheels): raise BriefcaseConfigError( "Only 'pyscript' backend is currently supported for web static builds." ) + + # Get pyscript version from config.toml. Use default if not present. + + if "version" in config_data: + pyscript_version = config_data.get("version") pyscript_path = config_filename.replace( "config.toml", "pyscript.toml" @@ -353,7 +391,7 @@ def extract_backend_config(self, wheels): "No backend was provided in config.toml file." ) - return pyscript_config + return pyscript_config, pyscript_version def build_app(self, app: AppConfig, **kwargs): """Build the static web deployment for the application. @@ -419,7 +457,7 @@ def build_app(self, app: AppConfig, **kwargs): with self.console.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. - config = self.extract_backend_config(self.wheel_path(app).glob("*.whl")) + config, pyscript_version = self.extract_backend_config(self.wheel_path(app).glob("*.whl")) # Add the packages declaration to the existing pyscript.toml. # Ensure that we're using Unix path separators, as the content @@ -447,6 +485,10 @@ def build_app(self, app: AppConfig, **kwargs): self.console.info("Compile static web content from wheels") with self.console.wait_bar("Compiling static web content from wheels..."): + + # Add pyscript_version to index.html + self.write_pyscript_version(app, Path("index.html"), pyscript_version) + # Trim previously compiled content out of briefcase.css briefcase_css_path = self.project_path(app) / "static/css/briefcase.css" self._trim_file( From 6416bd1a0a595191a17e1fc287c9e6db9cd7109c Mon Sep 17 00:00:00 2001 From: kavi2du Date: Mon, 8 Sep 2025 16:38:49 +0800 Subject: [PATCH 028/105] Update /static handling Refactor process_wheel method to restore legacy '/static' CSS insert handling and handle CSS files from both legacy and deploy/inserts paths. Adds a warning when legacy '/static' CSS is detected. All CSS files are now added to static/css/briefcase.css with banner comments. --- src/briefcase/platforms/web/static.py | 177 ++++++++++++++++---------- 1 file changed, 110 insertions(+), 67 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 8bc7a8288..cc4d375fd 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -204,13 +204,15 @@ def write_inserts( # Save modified content target_path.write_text(file_text, encoding="utf-8") - def write_pyscript_version(self, app: AppConfig, filename: Path, pyscript_version: str): + def write_pyscript_version( + self, app: AppConfig, filename: Path, pyscript_version: str + ): """Write pyscript version into an existing html file. - This function looks for markers in the named file and replaces the + This function looks for markers in the named file and replaces the markers with the pyscript version. - - The markers are in pyscript declartions within the html. + + The markers are in pyscript declarations within the html. * Marker: ```` * Example: ```` @@ -226,95 +228,135 @@ def write_pyscript_version(self, app: AppConfig, filename: Path, pyscript_versio try: file_text = target_path.read_text(encoding="utf-8") except FileNotFoundError: - raise BriefcaseConfigError(f"{filename} not found; pyscript version could not be inserted.") - + raise BriefcaseConfigError( + f"{filename} not found; pyscript version could not be inserted." + ) + marker = "" if marker in file_text: file_text = file_text.replace(marker, pyscript_version) else: - raise BriefcaseConfigError(f"No pyscript markers found in {filename}; pyscript may not be configured correctly.") - + raise BriefcaseConfigError( + f"No pyscript markers found in {filename}; pyscript may not be configured correctly." + ) + target_path.write_text(file_text, encoding="utf-8") def _process_wheel( self, wheelfile, inserts: dict[str, dict[str, dict[str, str]]], static_path ): - """Process a wheel, extracting any content that needs to be compiled into the - final project. - - Extracted content comes in two forms: - * inserts - pieces of content that will be inserted into existing files - * static - content that will be copied wholesale. Any content in a ``static`` - folder inside the wheel will be copied as-is to the static folder, - namespaced by the package name of the wheel. + name_parts = wheelfile.name.split("-") + package_key = f"{name_parts[0]} {name_parts[1]}" - Any pre-existing static content for the wheel will be deleted. - - :param wheelfile: The path to the wheel file to be processed. - :param inserts: The inserts collection for the app - :param static_path: The location where static content should be unpacked - """ - parts = wheelfile.name.split("-") - package_name = parts[0] - package_version = parts[1] - package_key = f"{package_name} {package_version}" - - # Purge any old static files for this wheel - pkg_static_root = static_path / package_name - if pkg_static_root.exists(): - self.tools.shutil.rmtree(pkg_static_root) + # Warning flag for legacy CSS + legacy_css_warning = False with ZipFile(wheelfile) as wheel: - for filename in wheel.namelist(): + for filename in sorted(wheel.namelist()): # Skip directories and shallow paths path = Path(filename) parts = path.parts - if len(parts) >= 3 and not (filename.endswith("/")): - # Handle inserts under deploy/inserts + + if not (filename.endswith("/") or len(parts) < 2): if ( - parts[1] == "deploy" - and parts[2] == "inserts" + len(parts) > 1 + and parts[1] == "static" + and path.suffix == ".css" ): self.console.info(f" Found {filename}") - try: - insert, target = parts[-1].split(".", 1) - - self.console.info( - f" {filename}: Adding {insert} insert for {target}" + if not legacy_css_warning: + self.console.warning( + f" {wheelfile.name}: legacy '/static' CSS detected; " + "treating as insert into briefcase.css; this legacy handling will be removed in the future." ) + legacy_css_warning = True + try: + css_text = wheel.read(filename).decode("utf-8") + except UnicodeDecodeError as e: + raise BriefcaseCommandError( + f"{filename}: CSS content must be UTF-8 encoded" + ) from e + + # legacy banner text/format + rel_inside = "/".join(path.parts[2:]) + css_payload = ( + "\n/*******************************************************\n" + f" * {package_key}::{rel_inside}\n" + " *******************************************************/\n\n" + ) + css_text + + # Funnel into CSS insert slot + target = "static/css/briefcase.css" + insert = "CSS" + pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) + if package_key in pkg_map and pkg_map[package_key]: + pkg_map[package_key] += "\n" + css_payload + else: + pkg_map[package_key] = css_payload + + # New deploy/inserts handling + elif ( + len(parts) >= 3 + and parts[1] == "deploy" + and parts[2] == "inserts" + ): + self.console.info(f" Found {filename}") + basename = parts[-1] + + # HTML inserts first + if not basename.endswith(".css"): try: - text = wheel.read(filename).decode("utf-8") + insert, target = basename.split(".", 1) + self.console.info( + f" {filename}: Adding {insert} insert for {target}" + ) + try: + text = wheel.read(filename).decode("utf-8") + except UnicodeDecodeError as e: + raise BriefcaseCommandError( + f"{filename}: insert must be UTF-8 encoded" + ) from e + + pkg_map = inserts.setdefault(target, {}).setdefault( + insert, {} + ) + if package_key in pkg_map and pkg_map[package_key]: + pkg_map[package_key] += "\n" + text + else: + pkg_map[package_key] = text + + except ValueError: + self.console.debug( + f" {filename}: not an . name; skipping generic insert handling." + ) + + # CSS inserts + if basename.endswith(".css"): + try: + css_text = wheel.read(filename).decode("utf-8") except UnicodeDecodeError as e: raise BriefcaseCommandError( f"{filename}: insert must be UTF-8 encoded" ) from e - # Store raw contribution text per package - pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) - # Append if the same package contributes multiple files for the same slot + rel_inside = "/".join(parts[3:]) or basename + css_payload = ( + "\n/*******************************************************\n" + f" * {package_key}::{rel_inside}\n" + " *******************************************************/\n\n" + ) + css_text + + target = "static/css/briefcase.css" + insert = "CSS" + pkg_map = inserts.setdefault(target, {}).setdefault( + insert, {} + ) if package_key in pkg_map and pkg_map[package_key]: - pkg_map[package_key] += "\n" + text + pkg_map[package_key] += "\n" + css_payload else: - pkg_map[package_key] = text - - except ValueError: - self.console.warning( - f" {source}: missing ':'; skipping insert." - ) - - # Handle static files under deploy/static - elif ( - parts[1] == "deploy" - and parts[2] == "static" - ): - self.console.info(f" Found {filename}") - rel = Path(*parts[2:]) - outfilename = pkg_static_root / rel - outfilename.parent.mkdir(parents=True, exist_ok=True) - with outfilename.open("wb") as f: - f.write(wheel.read(filename)) + pkg_map[package_key] = css_payload def extract_backend_config(self, wheels): """Processes multiple wheels to gather a config.toml and a base pyscript.toml @@ -368,7 +410,7 @@ def extract_backend_config(self, wheels): raise BriefcaseConfigError( "Only 'pyscript' backend is currently supported for web static builds." ) - + # Get pyscript version from config.toml. Use default if not present. if "version" in config_data: @@ -457,7 +499,9 @@ def build_app(self, app: AppConfig, **kwargs): with self.console.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. - config, pyscript_version = self.extract_backend_config(self.wheel_path(app).glob("*.whl")) + config, pyscript_version = self.extract_backend_config( + self.wheel_path(app).glob("*.whl") + ) # Add the packages declaration to the existing pyscript.toml. # Ensure that we're using Unix path separators, as the content @@ -485,7 +529,6 @@ def build_app(self, app: AppConfig, **kwargs): self.console.info("Compile static web content from wheels") with self.console.wait_bar("Compiling static web content from wheels..."): - # Add pyscript_version to index.html self.write_pyscript_version(app, Path("index.html"), pyscript_version) From 0029857d5981f9713ff1edc7550ffd9f6cdadf12 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Mon, 8 Sep 2025 16:57:10 +0800 Subject: [PATCH 029/105] Update the docstring and the inline comments of the process_wheel method --- src/briefcase/platforms/web/static.py | 33 +++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index cc4d375fd..8820883f1 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -245,6 +245,23 @@ def write_pyscript_version( def _process_wheel( self, wheelfile, inserts: dict[str, dict[str, dict[str, str]]], static_path ): + """Process a wheel to collect insert and style content for the final project. + + Scans the wheel for: + * Legacy CSS – `.css` files under ``static/`` are appended to the + ``briefcase.css`` insert slot with a deprecation warning. + * HTML inserts – HTML header files under ``deploy/inserts/.`` + are added to the corresponding insert slot for the target file. + * CSS inserts – Any `.css` under ``deploy/inserts/`` is appended to + the ``briefcase.css`` insert slot. + + Inserts are grouped by `` `` for ordering and + provenance. All content must be UTF-8 encoded. + + :param wheelfile: Path to the wheel file. + :param inserts: Nested dict of inserts keyed by target - insert - package. + :param static_path: Path for static content. + """ name_parts = wheelfile.name.split("-") package_key = f"{name_parts[0]} {name_parts[1]}" @@ -258,13 +275,15 @@ def _process_wheel( parts = path.parts if not (filename.endswith("/") or len(parts) < 2): + # Legacy CSS handling if ( len(parts) > 1 and parts[1] == "static" - and path.suffix == ".css" + and path.suffix.lower == ".css" ): self.console.info(f" Found {filename}") + # Show deprecation warning once per wheel if not legacy_css_warning: self.console.warning( f" {wheelfile.name}: legacy '/static' CSS detected; " @@ -279,7 +298,7 @@ def _process_wheel( f"{filename}: CSS content must be UTF-8 encoded" ) from e - # legacy banner text/format + # Wrap CSS with a source banner showing package and file rel_inside = "/".join(path.parts[2:]) css_payload = ( "\n/*******************************************************\n" @@ -287,7 +306,7 @@ def _process_wheel( " *******************************************************/\n\n" ) + css_text - # Funnel into CSS insert slot + # Add CSS content to briefcase.css insert slot target = "static/css/briefcase.css" insert = "CSS" pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) @@ -305,9 +324,10 @@ def _process_wheel( self.console.info(f" Found {filename}") basename = parts[-1] - # HTML inserts first + # HTML/other inserts if not basename.endswith(".css"): try: + # Split filename into slot and insert, target = basename.split(".", 1) self.console.info( f" {filename}: Adding {insert} insert for {target}" @@ -319,9 +339,11 @@ def _process_wheel( f"{filename}: insert must be UTF-8 encoded" ) from e + # Store insert under the correct target and slot pkg_map = inserts.setdefault(target, {}).setdefault( insert, {} ) + # Append if package already contributed to this slot if package_key in pkg_map and pkg_map[package_key]: pkg_map[package_key] += "\n" + text else: @@ -341,6 +363,7 @@ def _process_wheel( f"{filename}: insert must be UTF-8 encoded" ) from e + # Wrap CSS with a source banner showing package and file rel_inside = "/".join(parts[3:]) or basename css_payload = ( "\n/*******************************************************\n" @@ -348,11 +371,13 @@ def _process_wheel( " *******************************************************/\n\n" ) + css_text + # Add CSS content to briefcase.css insert slot target = "static/css/briefcase.css" insert = "CSS" pkg_map = inserts.setdefault(target, {}).setdefault( insert, {} ) + # Append if package already has content for this slot if package_key in pkg_map and pkg_map[package_key]: pkg_map[package_key] += "\n" + css_payload else: From 6a08c52ee3d7908e3e3d6ba414533a62833e9dc8 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Tue, 9 Sep 2025 11:22:36 +0800 Subject: [PATCH 030/105] Apply one-arg-per-line style to long function defs --- src/briefcase/platforms/web/static.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 8820883f1..ca61f46e4 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -243,7 +243,10 @@ def write_pyscript_version( target_path.write_text(file_text, encoding="utf-8") def _process_wheel( - self, wheelfile, inserts: dict[str, dict[str, dict[str, str]]], static_path + self, + wheelfile, + inserts: dict[str, dict[str, dict[str, str]]], + static_path, ): """Process a wheel to collect insert and style content for the final project. From bd1ea40ee6833bdafcf396c625df7bb109296113 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Tue, 9 Sep 2025 11:30:44 +0800 Subject: [PATCH 031/105] Apply one-arg-per-line style to long function defs --- src/briefcase/platforms/web/static.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index ca61f46e4..7d65b7fed 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -101,7 +101,10 @@ def _trim_file(self, path, sentinel): f.write(line) def write_inserts( - self, app: AppConfig, filename: Path, inserts: dict[str, dict[str, str]] + self, + app: AppConfig, + filename: Path, + inserts: dict[str, dict[str, str]] ): """Write inserts into an existing file. @@ -205,7 +208,10 @@ def write_inserts( target_path.write_text(file_text, encoding="utf-8") def write_pyscript_version( - self, app: AppConfig, filename: Path, pyscript_version: str + self, + app: AppConfig, + filename: Path, + pyscript_version: str ): """Write pyscript version into an existing html file. From 7c8cb8fd893db584fde2b21115a1c2b4de43ef2f Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 15 Sep 2025 11:38:05 +0800 Subject: [PATCH 032/105] Change pyscript insertion to non-destructive --- src/briefcase/platforms/web/static.py | 31 +++++++++++++++++++-------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 7d65b7fed..824a46b2d 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -101,9 +101,9 @@ def _trim_file(self, path, sentinel): f.write(line) def write_inserts( - self, - app: AppConfig, - filename: Path, + self, + app: AppConfig, + filename: Path, inserts: dict[str, dict[str, str]] ): """Write inserts into an existing file. @@ -208,9 +208,9 @@ def write_inserts( target_path.write_text(file_text, encoding="utf-8") def write_pyscript_version( - self, - app: AppConfig, - filename: Path, + self, + app: AppConfig, + filename: Path, pyscript_version: str ): """Write pyscript version into an existing html file. @@ -238,9 +238,22 @@ def write_pyscript_version( f"{filename} not found; pyscript version could not be inserted." ) - marker = "" - if marker in file_text: - file_text = file_text.replace(marker, pyscript_version) + marker = r".*" + insertion = f""" + + + + + """ + insertion = insertion.replace("pyscript_version", pyscript_version) + if re.search(marker, file_text, flags=re.DOTALL): + file_text = re.sub(marker, insertion, file_text, flags=re.DOTALL) else: raise BriefcaseConfigError( f"No pyscript markers found in {filename}; pyscript may not be configured correctly." From 3905608d6668c944b82fb57d35fe5e282cef4e63 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Mon, 15 Sep 2025 12:59:46 +0800 Subject: [PATCH 033/105] Simplify process_wheel by removing redundant guard and fix .lower() call --- src/briefcase/platforms/web/static.py | 172 ++++++++++++-------------- 1 file changed, 79 insertions(+), 93 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 824a46b2d..f199ddd7f 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -101,10 +101,7 @@ def _trim_file(self, path, sentinel): f.write(line) def write_inserts( - self, - app: AppConfig, - filename: Path, - inserts: dict[str, dict[str, str]] + self, app: AppConfig, filename: Path, inserts: dict[str, dict[str, str]] ): """Write inserts into an existing file. @@ -208,10 +205,7 @@ def write_inserts( target_path.write_text(file_text, encoding="utf-8") def write_pyscript_version( - self, - app: AppConfig, - filename: Path, - pyscript_version: str + self, app: AppConfig, filename: Path, pyscript_version: str ): """Write pyscript version into an existing html file. @@ -265,7 +259,6 @@ def _process_wheel( self, wheelfile, inserts: dict[str, dict[str, dict[str, str]]], - static_path, ): """Process a wheel to collect insert and style content for the final project. @@ -296,32 +289,92 @@ def _process_wheel( path = Path(filename) parts = path.parts - if not (filename.endswith("/") or len(parts) < 2): - # Legacy CSS handling - if ( - len(parts) > 1 - and parts[1] == "static" - and path.suffix.lower == ".css" - ): - self.console.info(f" Found {filename}") + # Legacy CSS handling + if ( + len(parts) > 1 + and parts[1] == "static" + and path.suffix.lower() == ".css" + ): + self.console.info(f" Found {filename}") + + # Show deprecation warning once per wheel + if not legacy_css_warning: + self.console.warning( + f" {wheelfile.name}: legacy '/static' CSS detected; " + "treating as insert into briefcase.css; this legacy handling will be removed in the future." + ) + legacy_css_warning = True + + try: + css_text = wheel.read(filename).decode("utf-8") + except UnicodeDecodeError as e: + raise BriefcaseCommandError( + f"{filename}: CSS content must be UTF-8 encoded" + ) from e + + # Wrap CSS with a source banner showing package and file + rel_inside = "/".join(path.parts[2:]) + css_payload = ( + "\n/*******************************************************\n" + f" * {package_key}::{rel_inside}\n" + " *******************************************************/\n\n" + ) + css_text + + # Add CSS content to briefcase.css insert slot + target = "static/css/briefcase.css" + insert = "CSS" + pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) + if package_key in pkg_map and pkg_map[package_key]: + pkg_map[package_key] += "\n" + css_payload + else: + pkg_map[package_key] = css_payload + + # New deploy/inserts handling + elif len(parts) >= 3 and parts[1] == "deploy" and parts[2] == "inserts": + self.console.info(f" Found {filename}") + basename = parts[-1] + + # HTML/other inserts + if not basename.endswith(".css"): + try: + # Split filename into slot and + insert, target = basename.split(".", 1) + self.console.info( + f" {filename}: Adding {insert} insert for {target}" + ) + try: + text = wheel.read(filename).decode("utf-8") + except UnicodeDecodeError as e: + raise BriefcaseCommandError( + f"{filename}: insert must be UTF-8 encoded" + ) from e + + # Store insert under the correct target and slot + pkg_map = inserts.setdefault(target, {}).setdefault( + insert, {} + ) + # Append if package already contributed to this slot + if package_key in pkg_map and pkg_map[package_key]: + pkg_map[package_key] += "\n" + text + else: + pkg_map[package_key] = text - # Show deprecation warning once per wheel - if not legacy_css_warning: - self.console.warning( - f" {wheelfile.name}: legacy '/static' CSS detected; " - "treating as insert into briefcase.css; this legacy handling will be removed in the future." + except ValueError: + self.console.debug( + f" {filename}: not an . name; skipping generic insert handling." ) - legacy_css_warning = True + # CSS inserts + if basename.endswith(".css"): try: css_text = wheel.read(filename).decode("utf-8") except UnicodeDecodeError as e: raise BriefcaseCommandError( - f"{filename}: CSS content must be UTF-8 encoded" + f"{filename}: insert must be UTF-8 encoded" ) from e # Wrap CSS with a source banner showing package and file - rel_inside = "/".join(path.parts[2:]) + rel_inside = "/".join(parts[3:]) or basename css_payload = ( "\n/*******************************************************\n" f" * {package_key}::{rel_inside}\n" @@ -332,79 +385,12 @@ def _process_wheel( target = "static/css/briefcase.css" insert = "CSS" pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) + # Append if package already has content for this slot if package_key in pkg_map and pkg_map[package_key]: pkg_map[package_key] += "\n" + css_payload else: pkg_map[package_key] = css_payload - # New deploy/inserts handling - elif ( - len(parts) >= 3 - and parts[1] == "deploy" - and parts[2] == "inserts" - ): - self.console.info(f" Found {filename}") - basename = parts[-1] - - # HTML/other inserts - if not basename.endswith(".css"): - try: - # Split filename into slot and - insert, target = basename.split(".", 1) - self.console.info( - f" {filename}: Adding {insert} insert for {target}" - ) - try: - text = wheel.read(filename).decode("utf-8") - except UnicodeDecodeError as e: - raise BriefcaseCommandError( - f"{filename}: insert must be UTF-8 encoded" - ) from e - - # Store insert under the correct target and slot - pkg_map = inserts.setdefault(target, {}).setdefault( - insert, {} - ) - # Append if package already contributed to this slot - if package_key in pkg_map and pkg_map[package_key]: - pkg_map[package_key] += "\n" + text - else: - pkg_map[package_key] = text - - except ValueError: - self.console.debug( - f" {filename}: not an . name; skipping generic insert handling." - ) - - # CSS inserts - if basename.endswith(".css"): - try: - css_text = wheel.read(filename).decode("utf-8") - except UnicodeDecodeError as e: - raise BriefcaseCommandError( - f"{filename}: insert must be UTF-8 encoded" - ) from e - - # Wrap CSS with a source banner showing package and file - rel_inside = "/".join(parts[3:]) or basename - css_payload = ( - "\n/*******************************************************\n" - f" * {package_key}::{rel_inside}\n" - " *******************************************************/\n\n" - ) + css_text - - # Add CSS content to briefcase.css insert slot - target = "static/css/briefcase.css" - insert = "CSS" - pkg_map = inserts.setdefault(target, {}).setdefault( - insert, {} - ) - # Append if package already has content for this slot - if package_key in pkg_map and pkg_map[package_key]: - pkg_map[package_key] += "\n" + css_payload - else: - pkg_map[package_key] = css_payload - def extract_backend_config(self, wheels): """Processes multiple wheels to gather a config.toml and a base pyscript.toml file. From 36ec34b99ec35a71a8ce6367735139ed626efd51 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Mon, 15 Sep 2025 13:47:40 +0800 Subject: [PATCH 034/105] Remove duplicate CSS headers Remove duplicate CSS headers and use a unique key per CSS file to avoid collisions --- src/briefcase/platforms/web/static.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index f199ddd7f..54e6419ab 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -312,22 +312,14 @@ def _process_wheel( f"{filename}: CSS content must be UTF-8 encoded" ) from e - # Wrap CSS with a source banner showing package and file rel_inside = "/".join(path.parts[2:]) - css_payload = ( - "\n/*******************************************************\n" - f" * {package_key}::{rel_inside}\n" - " *******************************************************/\n\n" - ) + css_text + contrib_key = f"{package_key} (legacy static CSS: {rel_inside})" # Add CSS content to briefcase.css insert slot target = "static/css/briefcase.css" insert = "CSS" pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) - if package_key in pkg_map and pkg_map[package_key]: - pkg_map[package_key] += "\n" + css_payload - else: - pkg_map[package_key] = css_payload + pkg_map[contrib_key] = css_text # New deploy/inserts handling elif len(parts) >= 3 and parts[1] == "deploy" and parts[2] == "inserts": @@ -375,21 +367,13 @@ def _process_wheel( # Wrap CSS with a source banner showing package and file rel_inside = "/".join(parts[3:]) or basename - css_payload = ( - "\n/*******************************************************\n" - f" * {package_key}::{rel_inside}\n" - " *******************************************************/\n\n" - ) + css_text + contrib_key = f"{package_key} (deploy CSS: {rel_inside})" # Add CSS content to briefcase.css insert slot target = "static/css/briefcase.css" insert = "CSS" pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) - # Append if package already has content for this slot - if package_key in pkg_map and pkg_map[package_key]: - pkg_map[package_key] += "\n" + css_payload - else: - pkg_map[package_key] = css_payload + pkg_map[contrib_key] = css_text def extract_backend_config(self, wheels): """Processes multiple wheels to gather a config.toml and a base pyscript.toml From e03a754163776ba8832b37278b0fef43f9f60707 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Tue, 16 Sep 2025 09:19:21 +0800 Subject: [PATCH 035/105] Simplified if-else failure logic in extract_backend_config --- src/briefcase/platforms/web/static.py | 48 +++++++++++++-------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 54e6419ab..47e37200d 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -412,44 +412,42 @@ def extract_backend_config(self, wheels): Duplicate config.toml found in package: {wheel.filename}""" ) # Gather a backend configuration file from the package. - # For now, is a pyscript.toml as no other backend is currently supported. + # For now, this is a pyscript.toml as no other backend is currently supported. else: with ZipFile(config_package_list[0]) as wheel: # Check which backend type is used. with wheel.open(config_filename) as config_file: config_data = tomllib.load(config_file) - if "backend" in config_data: - backend = config_data.get("backend") - - # Currently, only pyscript is supported, will raise an error if another backend is found. - if backend != "pyscript": - raise BriefcaseConfigError( - "Only 'pyscript' backend is currently supported for web static builds." - ) - - # Get pyscript version from config.toml. Use default if not present. + # Fail if no backend is present in config.toml + if "backend" not in config_data: + raise BriefcaseConfigError( + "No backend was provided in config.toml file." + ) - if "version" in config_data: - pyscript_version = config_data.get("version") + backend = config_data.get("backend") - pyscript_path = config_filename.replace( - "config.toml", "pyscript.toml" + # Currently, only pyscript is supported. Warn if another backend is found. + if backend is not "pyscript": + self.console.warning( + "Only 'pyscript' backend is currently supported for web static builds." + "This project may not work correctly." ) - try: - with wheel.open(pyscript_path) as pyscript_file: - pyscript_config = tomllib.load(pyscript_file) - except KeyError: - raise BriefcaseConfigError( - f"Pyscript configuration file not found in package: {config_package_list[0]}" - ) - # Raise error if no backend is present in config.toml - else: + # Get pyscript version from config.toml. Use default if not present. + pyscript_version = config_data.get("version", pyscript_version) + + # Extract pyscript.toml + pyscript_path = config_filename.replace("config.toml", "pyscript.toml") + try: + with wheel.open(pyscript_path) as pyscript_file: + pyscript_config = tomllib.load(pyscript_file) + except KeyError: raise BriefcaseConfigError( - "No backend was provided in config.toml file." + f"Pyscript configuration file not found in package: {config_package_list[0]}" ) + return pyscript_config, pyscript_version def build_app(self, app: AppConfig, **kwargs): From 5ad56fe37623d85667aaffaa2bbb4899bfc1e9a2 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Tue, 16 Sep 2025 09:43:35 +0800 Subject: [PATCH 036/105] Change failures to warnings in write_pryscript_version for backwards compatibility. --- src/briefcase/platforms/web/static.py | 31 +++++++++++++++------------ 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 47e37200d..d0c334904 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -209,30 +209,29 @@ def write_pyscript_version( ): """Write pyscript version into an existing html file. - This function looks for markers in the named file and replaces the - markers with the pyscript version. + This function looks for markers in the named file and appends + pyscript definitions between the markers. - The markers are in pyscript declarations within the html. - - * Marker: ```` - * Example: ```` - - Pyscript versions are processed in sorted order to ensure deterministic builds. + * Start: + * End: :param app: The application being written. :param filename: The html file whose pyscript version is to be written. :param pyscript_version: The pyscript version number to be inserted. """ - # Load file content, skip if file not found + # Load file content. Raise a warning if the file is not found target_path = self.project_path(app) / filename try: file_text = target_path.read_text(encoding="utf-8") except FileNotFoundError: - raise BriefcaseConfigError( - f"{filename} not found; pyscript version could not be inserted." + self.console.warning( + f"Target {filename} not found in {target_path}; skipping pyscript insertion." + "This project may not work correctly." ) marker = r".*" + + # PyScript definitions for insertion: insertion = f""" """ - insertion = insertion.replace("pyscript_version", pyscript_version) + + # Replace content between markers with PyScript definition insertions. if re.search(marker, file_text, flags=re.DOTALL): file_text = re.sub(marker, insertion, file_text, flags=re.DOTALL) + # Warning if no markers were found. + # NOTE: this is likely due to using an older version of the Web Template. else: - raise BriefcaseConfigError( - f"No pyscript markers found in {filename}; pyscript may not be configured correctly." + self.console.warning( + f"No pyscript markers found in {filename}; PyScript may not be configured correctly." + "Please ensure you are using the latest version of Briefcase Static Web Template" ) target_path.write_text(file_text, encoding="utf-8") From 58a0da08a48b5ebbe200a624fff91609917546b0 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Tue, 16 Sep 2025 09:48:11 +0800 Subject: [PATCH 037/105] enforce one-arg-per-line formatting --- src/briefcase/platforms/web/static.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index d0c334904..ce3d75e90 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -101,7 +101,10 @@ def _trim_file(self, path, sentinel): f.write(line) def write_inserts( - self, app: AppConfig, filename: Path, inserts: dict[str, dict[str, str]] + self, + app: AppConfig, + filename: Path, + inserts: dict[str, dict[str, str]], ): """Write inserts into an existing file. @@ -205,7 +208,10 @@ def write_inserts( target_path.write_text(file_text, encoding="utf-8") def write_pyscript_version( - self, app: AppConfig, filename: Path, pyscript_version: str + self, + app: AppConfig, + filename: Path, + pyscript_version: str, ): """Write pyscript version into an existing html file. From 926d28856846c44dd430430c8db8237e7e2692c7 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Tue, 16 Sep 2025 17:39:27 +0800 Subject: [PATCH 038/105] Minor edits edited comment to match updated process removed duplicate console logging. --- src/briefcase/platforms/web/static.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index ce3d75e90..1c88fc40e 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -521,7 +521,7 @@ def build_app(self, app: AppConfig, **kwargs): ) from e with self.console.wait_bar("Writing Pyscript configuration file..."): - # Load any pre-existing pyscript.toml provided by the template. If the file + # Load any pre-existing pyscript.toml provided by a GUI Toolkit. If the file # doesn't exist, assume an empty pyscript.toml as a starting point. config, pyscript_version = self.extract_backend_config( self.wheel_path(app).glob("*.whl") @@ -551,7 +551,6 @@ def build_app(self, app: AppConfig, **kwargs): with (self.project_path(app) / "pyscript.toml").open("wb") as f: tomli_w.dump(config, f) - self.console.info("Compile static web content from wheels") with self.console.wait_bar("Compiling static web content from wheels..."): # Add pyscript_version to index.html self.write_pyscript_version(app, Path("index.html"), pyscript_version) From 482a169e3c3d3bcf15168b54f7f2192df901c52c Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Tue, 16 Sep 2025 18:41:30 +0800 Subject: [PATCH 039/105] Further simplify nested if-else logic within extract_backend_config() --- src/briefcase/platforms/web/static.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 1c88fc40e..31dfbe437 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -424,20 +424,19 @@ def extract_backend_config(self, wheels): # For now, this is a pyscript.toml as no other backend is currently supported. else: with ZipFile(config_package_list[0]) as wheel: - # Check which backend type is used. with wheel.open(config_filename) as config_file: config_data = tomllib.load(config_file) + # Gather backend type from config.toml + backend = config_data.get("backend", None) + # Fail if no backend is present in config.toml - if "backend" not in config_data: + if backend is None: raise BriefcaseConfigError( "No backend was provided in config.toml file." ) - - backend = config_data.get("backend") - # Currently, only pyscript is supported. Warn if another backend is found. - if backend is not "pyscript": + elif backend is not "pyscript": self.console.warning( "Only 'pyscript' backend is currently supported for web static builds." "This project may not work correctly." From 0a9f76c5b1af99bb19247710014c9ef41617e22d Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Tue, 16 Sep 2025 19:12:31 +0800 Subject: [PATCH 040/105] remove unused static_path variable --- src/briefcase/platforms/web/static.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 31dfbe437..6a47f1d86 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -284,7 +284,6 @@ def _process_wheel( :param wheelfile: Path to the wheel file. :param inserts: Nested dict of inserts keyed by target - insert - package. - :param static_path: Path for static content. """ name_parts = wheelfile.name.split("-") package_key = f"{name_parts[0]} {name_parts[1]}" @@ -436,7 +435,7 @@ def extract_backend_config(self, wheels): "No backend was provided in config.toml file." ) # Currently, only pyscript is supported. Warn if another backend is found. - elif backend is not "pyscript": + elif backend != "pyscript": self.console.warning( "Only 'pyscript' backend is currently supported for web static builds." "This project may not work correctly." @@ -569,7 +568,6 @@ def build_app(self, app: AppConfig, **kwargs): self._process_wheel( wheelfile=wheelfile, inserts=inserts, - static_path=static_root, ) # Write inserts per target From 3cfbfc78ba0c7c73c0de31dd98bbe090b0df4735 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Wed, 17 Sep 2025 14:08:36 +0800 Subject: [PATCH 041/105] Unify wheel insert handling and escape slot regex Use full insert path to avoid filename collisions in write_inserts, and treat CSS like any other insert in process_wheel --- src/briefcase/platforms/web/static.py | 112 +++++++++++--------------- 1 file changed, 47 insertions(+), 65 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 6a47f1d86..0f238cde8 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -113,12 +113,12 @@ def write_inserts( Multiple formats of insert marker are inspected to accommodate HTML and CSS/JS comment conventions: - * HTML: ```` and ```` - * CSS/JS: ``/*@@ insert:start @@*/`` and ``/*@@ insert:end @@*/`` + * HTML: ` and + * CSS/JS: `/*@@ insert:start @@*/ and /*@@ insert:end @@*/ Inserts and package contributions are processed in sorted order to ensure deterministic builds. - :param app: The application whose ``pyscript.toml`` is being written. + :param app: The application whose `pyscript.toml is being written. :param filename: The file whose insert is to be written. :param inserts: The inserts for the file. A 2 level dictionary, keyed by the name of the insert to add, and then package that contributed the @@ -160,33 +160,26 @@ def write_inserts( ) body_map = {"html": html_body, "css": css_body} - # Marker patterns for HTML and CSS/JS - marker_styles = [ - # HTML + slot = re.escape(insert) + + # Build the compiled patterns directly once per slot + compiled_markers = [ ( - r".*?", + re.compile( + rf".*?", + flags=re.MULTILINE | re.DOTALL, + ), r"\n{content}", "html", ), - # CSS/JS - ( - r"/\*@@ {insert}:start @@\*/.*?/\*@@ {insert}:end @@\*/", - r"/*@@ {insert}:start @@*/\n{content}/*@@ {insert}:end @@*/", - "css", - ), - ] - - # Pre-compile patterns once per insert - compiled_markers = [ ( re.compile( - pattern_tmpl.format(insert=insert), + rf"/\*@@ {slot}:start @@\*/.*?/\*@@ {slot}:end @@\*/", flags=re.MULTILINE | re.DOTALL, ), - repl_tmpl, - kind, - ) - for (pattern_tmpl, repl_tmpl, kind) in marker_styles + r"/*@@ {insert}:start @@*/\n{content}/*@@ {insert}:end @@*/", + "css", + ), ] # Apply all matching marker styles @@ -272,15 +265,11 @@ def _process_wheel( """Process a wheel to collect insert and style content for the final project. Scans the wheel for: - * Legacy CSS – `.css` files under ``static/`` are appended to the - ``briefcase.css`` insert slot with a deprecation warning. - * HTML inserts – HTML header files under ``deploy/inserts/.`` - are added to the corresponding insert slot for the target file. - * CSS inserts – Any `.css` under ``deploy/inserts/`` is appended to - the ``briefcase.css`` insert slot. - - Inserts are grouped by `` `` for ordering and - provenance. All content must be UTF-8 encoded. + * Legacy CSS – .css files under `static/ are appended to the + `briefcase.css insert slot with a deprecation warning. + * Deploy inserts – Any files under `deploy/inserts/. + (HTML, CSS, JS, etc.) are added to the corresponding insert slot for + the target file. :param wheelfile: Path to the wheel file. :param inserts: Nested dict of inserts keyed by target - insert - package. @@ -327,18 +316,30 @@ def _process_wheel( target = "static/css/briefcase.css" insert = "CSS" pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) - pkg_map[contrib_key] = css_text + if contrib_key in pkg_map and pkg_map[contrib_key]: + pkg_map[contrib_key] += "\n" + css_text + else: + pkg_map[contrib_key] = css_text # New deploy/inserts handling elif len(parts) >= 3 and parts[1] == "deploy" and parts[2] == "inserts": self.console.info(f" Found {filename}") - basename = parts[-1] + rel_inside = "/".join(parts[3:]) - # HTML/other inserts - if not basename.endswith(".css"): + if not rel_inside or rel_inside.endswith("/"): + self.console.debug( + f" {filename}: skipping, not a valid insert file." + ) + else: try: - # Split filename into slot and - insert, target = basename.split(".", 1) + dot_idx = rel_inside.index(".") + insert = rel_inside[:dot_idx] + target = rel_inside[dot_idx + 1 :] + except ValueError: + self.console.debug( + f" {filename}: skipping, filename must match '.'." + ) + else: self.console.info( f" {filename}: Adding {insert} insert for {target}" ) @@ -349,39 +350,20 @@ def _process_wheel( f"{filename}: insert must be UTF-8 encoded" ) from e - # Store insert under the correct target and slot + contrib_key = f"{package_key} (deploy insert: {rel_inside} from {filename})" + pkg_map = inserts.setdefault(target, {}).setdefault( insert, {} ) - # Append if package already contributed to this slot if package_key in pkg_map and pkg_map[package_key]: pkg_map[package_key] += "\n" + text else: pkg_map[package_key] = text - except ValueError: - self.console.debug( - f" {filename}: not an . name; skipping generic insert handling." - ) - - # CSS inserts - if basename.endswith(".css"): - try: - css_text = wheel.read(filename).decode("utf-8") - except UnicodeDecodeError as e: - raise BriefcaseCommandError( - f"{filename}: insert must be UTF-8 encoded" - ) from e - - # Wrap CSS with a source banner showing package and file - rel_inside = "/".join(parts[3:]) or basename - contrib_key = f"{package_key} (deploy CSS: {rel_inside})" - - # Add CSS content to briefcase.css insert slot - target = "static/css/briefcase.css" - insert = "CSS" - pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) - pkg_map[contrib_key] = css_text + else: + self.console.debug( + f" {filename}: skipping, not a supported insert." + ) def extract_backend_config(self, wheels): """Processes multiple wheels to gather a config.toml and a base pyscript.toml @@ -445,7 +427,9 @@ def extract_backend_config(self, wheels): pyscript_version = config_data.get("version", pyscript_version) # Extract pyscript.toml - pyscript_path = config_filename.replace("config.toml", "pyscript.toml") + pyscript_path = config_filename.replace( + "config.toml", "pyscript.toml" + ) try: with wheel.open(pyscript_path) as pyscript_file: pyscript_config = tomllib.load(pyscript_file) @@ -454,7 +438,6 @@ def extract_backend_config(self, wheels): f"Pyscript configuration file not found in package: {config_package_list[0]}" ) - return pyscript_config, pyscript_version def build_app(self, app: AppConfig, **kwargs): @@ -561,7 +544,6 @@ def build_app(self, app: AppConfig, **kwargs): ) inserts: dict[str, dict[str, dict[str, str]]] = {} - static_root = self.static_path(app) for wheelfile in sorted(self.wheel_path(app).glob("*.whl")): self.console.info(f" Processing {wheelfile.name}...") From a124d40da57403ee552a1d8aaaf51b07c256d398 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Wed, 17 Sep 2025 19:56:41 +0800 Subject: [PATCH 042/105] Indent insertion content and markers within write_inserts() --- src/briefcase/platforms/web/static.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 0f238cde8..f5760d71d 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any from zipfile import ZipFile +from textwrap import indent from briefcase.console import Console from briefcase.exceptions import ( @@ -113,12 +114,12 @@ def write_inserts( Multiple formats of insert marker are inspected to accommodate HTML and CSS/JS comment conventions: - * HTML: ` and - * CSS/JS: `/*@@ insert:start @@*/ and /*@@ insert:end @@*/ + * HTML: ` and ` + * CSS/JS: `/*@@ insert:start @@*/ and /*@@ insert:end @@*/` Inserts and package contributions are processed in sorted order to ensure deterministic builds. - :param app: The application whose `pyscript.toml is being written. + :param app: The application whose `pyscript.toml` is being written. :param filename: The file whose insert is to be written. :param inserts: The inserts for the file. A 2 level dictionary, keyed by the name of the insert to add, and then package that contributed the @@ -166,18 +167,18 @@ def write_inserts( compiled_markers = [ ( re.compile( - rf".*?", + rf"(^\s*).*?", flags=re.MULTILINE | re.DOTALL, ), - r"\n{content}", + r"{indent}\n{content}{indent}", "html", ), ( re.compile( - rf"/\*@@ {slot}:start @@\*/.*?/\*@@ {slot}:end @@\*/", + rf"(^\s*)/\*@@ {slot}:start @@\*/.*?/\*@@ {slot}:end @@\*/", flags=re.MULTILINE | re.DOTALL, ), - r"/*@@ {insert}:start @@*/\n{content}/*@@ {insert}:end @@*/", + r"{indent}/*@@ {insert}:start @@*/\n{content}{indent}/*@@ {insert}:end @@*/", "css", ), ] @@ -185,9 +186,13 @@ def write_inserts( # Apply all matching marker styles any_match = False for pattern, repl_tmpl, kind in compiled_markers: - if pattern.search(file_text): + # Search for pattern within file. + pattern_found = pattern.search(file_text) + if pattern_found: + # Indent content to align with markers. + indented_content = indent(body_map.get(kind, ""), pattern_found.group(1)) file_text = pattern.sub( - repl_tmpl.format(insert=insert, content=body_map.get(kind, "")), + repl_tmpl.format(indent=pattern_found.group(1), insert=insert, content=indented_content), file_text, ) any_match = True From 1e8814c7fdd209b969a9c63bf4bff5e7e378b239 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Wed, 17 Sep 2025 21:04:11 +0800 Subject: [PATCH 043/105] Apply matching indentation to write_pyscript_version --- src/briefcase/platforms/web/static.py | 48 ++++++++++++++++----------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index f5760d71d..771a92ae4 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Any from zipfile import ZipFile -from textwrap import indent +from textwrap import indent, dedent from briefcase.console import Console from briefcase.exceptions import ( @@ -167,7 +167,7 @@ def write_inserts( compiled_markers = [ ( re.compile( - rf"(^\s*).*?", + rf"(^[ \t]*).*?", flags=re.MULTILINE | re.DOTALL, ), r"{indent}\n{content}{indent}", @@ -175,7 +175,7 @@ def write_inserts( ), ( re.compile( - rf"(^\s*)/\*@@ {slot}:start @@\*/.*?/\*@@ {slot}:end @@\*/", + rf"(^[ \t]*)/\*@@ {slot}:start @@\*/.*?/\*@@ {slot}:end @@\*/", flags=re.MULTILINE | re.DOTALL, ), r"{indent}/*@@ {insert}:start @@*/\n{content}{indent}/*@@ {insert}:end @@*/", @@ -230,28 +230,36 @@ def write_pyscript_version( except FileNotFoundError: self.console.warning( f"Target {filename} not found in {target_path}; skipping pyscript insertion." - "This project may not work correctly." + "PyScript may not be configured correctly. This project may not work correctly." ) - marker = r".*" - # PyScript definitions for insertion: - insertion = f""" - - - - - """ + content = (f"""\ + + + + + + + """ + ) # Replace content between markers with PyScript definition insertions. - if re.search(marker, file_text, flags=re.DOTALL): - file_text = re.sub(marker, insertion, file_text, flags=re.DOTALL) + marker = r"(^[ \t]*).*" + marker_found = re.search(marker, file_text, flags=re.DOTALL | re.MULTILINE) + + if marker_found: + indented_content = indent( + dedent(content), + marker_found.group(1), + ) + file_text = re.sub(marker, indented_content, file_text, flags=re.DOTALL | re.MULTILINE) # Warning if no markers were found. # NOTE: this is likely due to using an older version of the Web Template. else: From 5689006be7f37b604f52d2c7aa96bf08c5ffebb4 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Fri, 19 Sep 2025 09:48:10 +0800 Subject: [PATCH 044/105] Update insert handling for `filename.filetype~insert` file name convention --- src/briefcase/platforms/web/static.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 771a92ae4..67560e959 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -280,7 +280,7 @@ def _process_wheel( Scans the wheel for: * Legacy CSS – .css files under `static/ are appended to the `briefcase.css insert slot with a deprecation warning. - * Deploy inserts – Any files under `deploy/inserts/. + * Deploy inserts – Any files under `deploy/inserts/~ (HTML, CSS, JS, etc.) are added to the corresponding insert slot for the target file. @@ -345,12 +345,12 @@ def _process_wheel( ) else: try: - dot_idx = rel_inside.index(".") - insert = rel_inside[:dot_idx] - target = rel_inside[dot_idx + 1 :] + dot_idx = rel_inside.index("~") + target = rel_inside[:dot_idx] + insert = rel_inside[dot_idx + 1 :] except ValueError: self.console.debug( - f" {filename}: skipping, filename must match '.'." + f" {filename}: skipping, filename must match '~'." ) else: self.console.info( From 0117229127a08e539d37623517e7f5b57a682afc Mon Sep 17 00:00:00 2001 From: kavi2du Date: Sat, 20 Sep 2025 09:29:51 +0800 Subject: [PATCH 045/105] Minor update in process_wheel docstring --- src/briefcase/platforms/web/static.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 67560e959..d1908dcac 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -5,9 +5,9 @@ import webbrowser from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path +from textwrap import dedent, indent from typing import Any from zipfile import ZipFile -from textwrap import indent, dedent from briefcase.console import Console from briefcase.exceptions import ( @@ -190,9 +190,15 @@ def write_inserts( pattern_found = pattern.search(file_text) if pattern_found: # Indent content to align with markers. - indented_content = indent(body_map.get(kind, ""), pattern_found.group(1)) + indented_content = indent( + body_map.get(kind, ""), pattern_found.group(1) + ) file_text = pattern.sub( - repl_tmpl.format(indent=pattern_found.group(1), insert=insert, content=indented_content), + repl_tmpl.format( + indent=pattern_found.group(1), + insert=insert, + content=indented_content, + ), file_text, ) any_match = True @@ -234,7 +240,7 @@ def write_pyscript_version( ) # PyScript definitions for insertion: - content = (f"""\ + content = f"""\ """ - ) # Replace content between markers with PyScript definition insertions. marker = r"(^[ \t]*).*" @@ -259,7 +264,9 @@ def write_pyscript_version( dedent(content), marker_found.group(1), ) - file_text = re.sub(marker, indented_content, file_text, flags=re.DOTALL | re.MULTILINE) + file_text = re.sub( + marker, indented_content, file_text, flags=re.DOTALL | re.MULTILINE + ) # Warning if no markers were found. # NOTE: this is likely due to using an older version of the Web Template. else: @@ -284,6 +291,10 @@ def _process_wheel( (HTML, CSS, JS, etc.) are added to the corresponding insert slot for the target file. + Insert content is grouped by `` `` (with an + extra suffix for legacy CSS) to preserve ordering and provenance. + All content must be UTF-8 encoded, non-UTF-8 files raise an error. + :param wheelfile: Path to the wheel file. :param inserts: Nested dict of inserts keyed by target - insert - package. """ From 30bf837305427dddcfc2c3f91eed5e756ed1bb82 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Sat, 20 Sep 2025 18:13:46 +0800 Subject: [PATCH 046/105] Remove pyscript.toml from conftest.py --- tests/platforms/web/static/conftest.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/tests/platforms/web/static/conftest.py b/tests/platforms/web/static/conftest.py index d71fd25d4..1a87029dc 100644 --- a/tests/platforms/web/static/conftest.py +++ b/tests/platforms/web/static/conftest.py @@ -19,15 +19,6 @@ 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", @@ -50,12 +41,6 @@ def first_app_generated(first_app_config, tmp_path): def first_app_built(first_app_generated, tmp_path): bundle_path = tmp_path / "base_path/build/first-app/web/static" - # Create pyscript.toml - create_file( - bundle_path / "www/pyscript.toml", - 'packages = ["dummy-1.2.3-py3-none-all.whl"]', - ) - # Create an app wheel create_wheel(bundle_path / "www/static/wheels") From 84cd196341be8feac00392a312a1f0519e72ec53 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Sat, 20 Sep 2025 18:16:44 +0800 Subject: [PATCH 047/105] initial mock pyscript.toml extraction from wheels and remove template pyscript tests tests removed for template related pyscript.toml tests will need to reconfigured for wheel supplied pyscript.toml test cases --- tests/platforms/web/static/test_build.py | 78 +++++++++--------------- 1 file changed, 28 insertions(+), 50 deletions(-) diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index cb927e9c4..7d533faab 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -47,6 +47,22 @@ def mock_run(*args, **kwargs): "first_app", extra_content=[ ("dependency/static/style.css", "span { margin: 10px; }\n"), + ( + "dependency/deploy/config.toml", + """ +backend = "pyscript" + +[pyscript] +version = "2024.11.1" +""" + ), + ( + "dependency/deploy/pyscript.toml", + """ +existing-key-1 = "value-1" +existing-key-2 = 2 +""" + ), ], ) elif args[0][5] == "pip": @@ -195,6 +211,12 @@ def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_ bundle_path / "www/static/wheels" ) + # Mock extracting pyscript.toml from a wheel. + build_command.extract_backend_config = lambda _: ( + {"existing-key-1": "value-1", "existing-key-2": 2}, + "2024.11.1" + ) + # Build the web app. build_command.build_app(first_app_generated) @@ -212,56 +234,6 @@ def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_ } -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 ): @@ -370,6 +342,12 @@ def mock_run(*args, **kwargs): bundle_path / "www/static/wheels" ) + # Mock extracting pyscript.toml from a wheel. + build_command.extract_backend_config = lambda _: ( + {"existing-key-1": "value-1", "existing-key-2": 2}, + "2024.11.1" + ) + # Build the web app. build_command.build_app(first_app_generated) From 7bd14813c132e0be454587c66892ef341d9a7b15 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Mon, 22 Sep 2025 17:19:04 +0800 Subject: [PATCH 048/105] Define HTML/CSS banners as module level constants --- src/briefcase/platforms/web/static.py | 32 ++++++++++++++------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index d1908dcac..bc12ce387 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -35,6 +35,21 @@ ) from briefcase.config import AppConfig +# Module level banner templates (Constants used in write_inserts) +HTML_BANNER = ( + "\n" + "{content}" +) + +CSS_BANNER = ( + "/**************************************************\n" + " * {package}\n" + " *************************************************/\n" + "{content}" +) + class StaticWebMixin: output_format = "static" @@ -135,27 +150,14 @@ def write_inserts( # Each insert slot and its package contributions are processed in sorted order for insert, pkg_contribs in sorted(inserts.items()): - html_banner = ( - "\n" - "{content}" - ) - css_banner = ( - "/**************************************************\n" - " * {package}\n" - " *************************************************/\n" - "{content}" - ) - # Build bodies from the same contributions html_body = "\n".join( - html_banner.format(package=pkg, content=text) + HTML_BANNER.format(package=pkg, content=text) for pkg, text in sorted(pkg_contribs.items()) if text ) css_body = "\n".join( - css_banner.format(package=pkg, content=text) + CSS_BANNER.format(package=pkg, content=text) for pkg, text in sorted(pkg_contribs.items()) if text ) From af516a36c38e8db1e83b8490b0b1689176114714 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 22 Sep 2025 17:39:46 +0800 Subject: [PATCH 049/105] convert write_pyscript_version to_write_pyscript_insert --- src/briefcase/platforms/web/static.py | 60 +++++++-------------------- 1 file changed, 16 insertions(+), 44 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index d1908dcac..e5b9db805 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -211,37 +211,26 @@ def write_inserts( # Save modified content target_path.write_text(file_text, encoding="utf-8") - def write_pyscript_version( + def _write_pyscript_insert( self, - app: AppConfig, filename: Path, pyscript_version: str, + inserts: dict[str, dict[str, dict[str, str]]], ): """Write pyscript version into an existing html file. - This function looks for markers in the named file and appends - pyscript definitions between the markers. - - * Start: - * End: + This function creates an insert for PyScript. - :param app: The application being written. :param filename: The html file whose pyscript version is to be written. :param pyscript_version: The pyscript version number to be inserted. + :param inserts: Nested dict of inserts keyed by target - insert - package. """ - # Load file content. Raise a warning if the file is not found - target_path = self.project_path(app) / filename - try: - file_text = target_path.read_text(encoding="utf-8") - except FileNotFoundError: - self.console.warning( - f"Target {filename} not found in {target_path}; skipping pyscript insertion." - "PyScript may not be configured correctly. This project may not work correctly." - ) + package_key = "briefcase" + target = filename + insert = "Python" # PyScript definitions for insertion: - content = f"""\ - + content = dedent(f"""\ - - """ + """) - # Replace content between markers with PyScript definition insertions. - marker = r"(^[ \t]*).*" - marker_found = re.search(marker, file_text, flags=re.DOTALL | re.MULTILINE) - - if marker_found: - indented_content = indent( - dedent(content), - marker_found.group(1), - ) - file_text = re.sub( - marker, indented_content, file_text, flags=re.DOTALL | re.MULTILINE - ) - # Warning if no markers were found. - # NOTE: this is likely due to using an older version of the Web Template. - else: - self.console.warning( - f"No pyscript markers found in {filename}; PyScript may not be configured correctly." - "Please ensure you are using the latest version of Briefcase Static Web Template" - ) - - target_path.write_text(file_text, encoding="utf-8") + pkg_map = inserts.setdefault(target, {}).setdefault( + insert, {} + ) + pkg_map[package_key] = content def _process_wheel( self, @@ -557,8 +528,6 @@ def build_app(self, app: AppConfig, **kwargs): tomli_w.dump(config, f) with self.console.wait_bar("Compiling static web content from wheels..."): - # Add pyscript_version to index.html - self.write_pyscript_version(app, Path("index.html"), pyscript_version) # Trim previously compiled content out of briefcase.css briefcase_css_path = self.project_path(app) / "static/css/briefcase.css" @@ -576,6 +545,9 @@ def build_app(self, app: AppConfig, **kwargs): inserts=inserts, ) + # Add pyscript insertion to inserts. + self._write_pyscript_insert("index.html", pyscript_version, inserts) + # Write inserts per target for target, target_inserts in sorted(inserts.items()): self.write_inserts(app, Path(target), target_inserts) From 1c79f9b0f707d786768f7ae612462ab7d671a1c9 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Mon, 22 Sep 2025 19:10:17 +0800 Subject: [PATCH 050/105] Use split(~) with validation for insert filenames in process_wheel --- src/briefcase/platforms/web/static.py | 52 +++++++++++++-------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index d878dc0d9..786b7dfe8 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -245,9 +245,7 @@ def _write_pyscript_insert( """) - pkg_map = inserts.setdefault(target, {}).setdefault( - insert, {} - ) + pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) pkg_map[package_key] = content def _process_wheel( @@ -328,34 +326,37 @@ def _process_wheel( f" {filename}: skipping, not a valid insert file." ) else: - try: - dot_idx = rel_inside.index("~") - target = rel_inside[:dot_idx] - insert = rel_inside[dot_idx + 1 :] - except ValueError: + if "~" not in rel_inside: self.console.debug( f" {filename}: skipping, filename must match '~'." ) else: - self.console.info( - f" {filename}: Adding {insert} insert for {target}" - ) try: - text = wheel.read(filename).decode("utf-8") - except UnicodeDecodeError as e: - raise BriefcaseCommandError( - f"{filename}: insert must be UTF-8 encoded" - ) from e - - contrib_key = f"{package_key} (deploy insert: {rel_inside} from {filename})" - - pkg_map = inserts.setdefault(target, {}).setdefault( - insert, {} - ) - if package_key in pkg_map and pkg_map[package_key]: - pkg_map[package_key] += "\n" + text + target, insert = rel_inside.split("~") + except ValueError: + self.console.debug( + f" {filename}: skipping, filename must match '~'." + ) else: - pkg_map[package_key] = text + self.console.info( + f" {filename}: Adding {insert} insert for {target}" + ) + try: + text = wheel.read(filename).decode("utf-8") + except UnicodeDecodeError as e: + raise BriefcaseCommandError( + f"{filename}: insert must be UTF-8 encoded" + ) from e + + contrib_key = f"{package_key} (deploy insert: {rel_inside} from {filename})" + + pkg_map = inserts.setdefault(target, {}).setdefault( + insert, {} + ) + if package_key in pkg_map and pkg_map[package_key]: + pkg_map[package_key] += "\n" + text + else: + pkg_map[package_key] = text else: self.console.debug( @@ -530,7 +531,6 @@ def build_app(self, app: AppConfig, **kwargs): tomli_w.dump(config, f) with self.console.wait_bar("Compiling static web content from wheels..."): - # Trim previously compiled content out of briefcase.css briefcase_css_path = self.project_path(app) / "static/css/briefcase.css" self._trim_file( From 181523e1a165f9fca4bf88e71d89b6ab14b2bed1 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Thu, 25 Sep 2025 13:08:13 +0800 Subject: [PATCH 051/105] Update fixtures with new insert markers and config handling in conftest.py --- tests/platforms/web/static/conftest.py | 27 +++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/tests/platforms/web/static/conftest.py b/tests/platforms/web/static/conftest.py index 1a87029dc..f7981933d 100644 --- a/tests/platforms/web/static/conftest.py +++ b/tests/platforms/web/static/conftest.py @@ -16,13 +16,34 @@ def first_app_generated(first_app_config, tmp_path): """, ) - # Create index.html - create_file(bundle_path / "www/index.html", "") + # Create index.html with insert markers + create_file( + bundle_path / "www/index.html", + """ + + + + + + + + +
- # Create the initial briefcase.css + + + + +""", + ) + + # Create the initial briefcase.css with CSS insert markers create_file( bundle_path / "www/static/css/briefcase.css", """ +/*@@ CSS:start @@*/ +/*@@ CSS:end @@*/ + #pyconsole { display: None; } From 8b3a4b2c8839e544a3a1b7c107173f0c971b5b7b Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Sun, 28 Sep 2025 15:16:51 +0800 Subject: [PATCH 052/105] Conftest.py css markers moved below sentinel banner --- tests/platforms/web/static/conftest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/platforms/web/static/conftest.py b/tests/platforms/web/static/conftest.py index f7981933d..43eb5c914 100644 --- a/tests/platforms/web/static/conftest.py +++ b/tests/platforms/web/static/conftest.py @@ -41,14 +41,14 @@ def first_app_generated(first_app_config, tmp_path): create_file( bundle_path / "www/static/css/briefcase.css", """ -/*@@ CSS:start @@*/ -/*@@ CSS:end @@*/ - #pyconsole { display: None; } /******************************************************************* - ******************** Wheel contributed styles ********************/ +******************** Wheel contributed styles ********************/ +/*@@ CSS:start @@*/ +/*@@ CSS:end @@*/ + """, ) From fa2eb1e8945b8049904f2198624678108b96452e Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 29 Sep 2025 11:43:10 +0800 Subject: [PATCH 053/105] check pyscript insertions in index.html for test_build_app --- tests/platforms/web/static/test_build.py | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index 7d533faab..1f9a5e56e 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -148,6 +148,46 @@ def mock_run(*args, **kwargs): ], } + # index.html has insertions + with (bundle_path / "www/index.html").open(encoding="utf-8") as f: + assert ( + f.read() + == "\n".join( + [ + "", + "", + " ", + " ", + " ", + " ", + " ", + " ", + "", + " ", + " ", + " ", + " ", + " ", + "
", + "", + " ", + " ", + " ", + "", + ] + ) + + "\n" + ) + + # briefcase.css has been appended with (bundle_path / "www/static/css/briefcase.css").open(encoding="utf-8") as f: assert ( From c9cfe8c4d7bf23b5319fa79a651eb4050831f37c Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 29 Sep 2025 12:11:41 +0800 Subject: [PATCH 054/105] Add test_build_app_no_config """An app with no config.toml supplied by a wheel gets a basic config.""" --- tests/platforms/web/static/test_build.py | 54 ++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index 1f9a5e56e..0f2676494 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -224,6 +224,60 @@ def mock_run(*args, **kwargs): ) +def test_build_app_no_config(build_command, first_app_generated, tmp_path): + """An app with no config.toml supplied by a wheel gets a basic config.""" + + bundle_path = tmp_path / "base_path/build/first-app/web/static" + + # Mock some wheels without a config.toml + def mock_run(*args, **kwargs): + if args[0][5] == "wheel": + create_wheel( + bundle_path / "www/static/wheels", + "first_app", + extra_content=[ + ("dependency/static/style.css", "span { margin: 10px; }\n"), + ], + ) + elif args[0][5] == "pip": + create_wheel( + bundle_path / "www/static/wheels", + "dependency", + extra_content=[ + ("dependency/static/style.css", "div { margin: 10px; }\n"), + ], + ) + create_wheel( + bundle_path / "www/static/wheels", + "other", + extra_content=[ + ("other/static/style.css", "div { padding: 10px; }\n"), + ], + ) + else: + raise ValueError("Unknown command") + + build_command.tools.subprocess.run.side_effect = mock_run + + # 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": [ + "/static/wheels/dependency-1.2.3-py3-none-any.whl", + "/static/wheels/first_app-1.2.3-py3-none-any.whl", + "/static/wheels/other-1.2.3-py3-none-any.whl", + ], + } + + def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_path): """An app with extra pyscript.toml content can be written.""" bundle_path = tmp_path / "base_path/build/first-app/web/static" From 4bfea454d3dd9d99b17c8b2f3349c684166ad954 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 29 Sep 2025 12:23:53 +0800 Subject: [PATCH 055/105] test_build_app_multiple_config """An app with multiple config.toml supplied by wheels fails to build.""" --- tests/platforms/web/static/test_build.py | 64 ++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index 0f2676494..865b1e4c1 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -277,6 +277,70 @@ def mock_run(*args, **kwargs): ], } +def test_build_app_multiple_config(build_command, first_app_generated, tmp_path): + """An app with multiple config.toml supplied by wheels fails to build.""" + + bundle_path = tmp_path / "base_path/build/first-app/web/static" + + # Mock some wheels without a config.toml + def mock_run(*args, **kwargs): + if args[0][5] == "wheel": + create_wheel( + bundle_path / "www/static/wheels", + "first_app", + extra_content=[ + ("dependency/static/style.css", "span { margin: 10px; }\n"), + ( + "dependency/deploy/config.toml", + """ +backend = "pyscript" + +[pyscript] +version = "2024.11.1" +""" + ), + ], + ) + elif args[0][5] == "pip": + create_wheel( + bundle_path / "www/static/wheels", + "dependency", + extra_content=[ + ("dependency/static/style.css", "div { margin: 10px; }\n"), + ( + "dependency/deploy/config.toml", + """ +backend = "pyscript" + +[pyscript] +version = "2024.10.1" +""" + ), + ], + ) + create_wheel( + bundle_path / "www/static/wheels", + "other", + extra_content=[ + ("other/static/style.css", "div { padding: 10px; }\n"), + ], + ) + else: + raise ValueError("Unknown command") + + build_command.tools.subprocess.run.side_effect = mock_run + + # 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. + with pytest.raises( + BriefcaseConfigError, + match=r"Only 1 backend configuration file can be supplied.", + ): + build_command.build_app(first_app_generated) def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_path): """An app with extra pyscript.toml content can be written.""" From 550e45fdcc5e0716a47ef9f98598c47ba172d417 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 29 Sep 2025 14:51:23 +0800 Subject: [PATCH 056/105] test_build_app_config_no_backend """An app cannot be built with a config.toml containing no "backend" value.""" --- tests/platforms/web/static/test_build.py | 51 ++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index 865b1e4c1..17214e941 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -277,6 +277,7 @@ def mock_run(*args, **kwargs): ], } + def test_build_app_multiple_config(build_command, first_app_generated, tmp_path): """An app with multiple config.toml supplied by wheels fails to build.""" @@ -342,6 +343,56 @@ def mock_run(*args, **kwargs): ): build_command.build_app(first_app_generated) + +def test_build_app_config_no_backend(build_command, first_app_generated, tmp_path): + """An app cannot be built with a config.toml containing no "backend" value.""" + + bundle_path = tmp_path / "base_path/build/first-app/web/static" + + # Mock some wheels without a config.toml + def mock_run(*args, **kwargs): + if args[0][5] == "wheel": + create_wheel( + bundle_path / "www/static/wheels", + "first_app", + extra_content=[ + ("dependency/static/style.css", "span { margin: 10px; }\n"), + ("dependency/deploy/config.toml", ""), + ], + ) + elif args[0][5] == "pip": + create_wheel( + bundle_path / "www/static/wheels", + "dependency", + extra_content=[ + ("dependency/static/style.css", "div { margin: 10px; }\n"), + ], + ) + create_wheel( + bundle_path / "www/static/wheels", + "other", + extra_content=[ + ("other/static/style.css", "div { padding: 10px; }\n"), + ], + ) + else: + raise ValueError("Unknown command") + + build_command.tools.subprocess.run.side_effect = mock_run + + # 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. + with pytest.raises( + BriefcaseConfigError, + match=r"No backend was provided in config.toml file.", + ): + build_command.build_app(first_app_generated) + + def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_path): """An app with extra pyscript.toml content can be written.""" bundle_path = tmp_path / "base_path/build/first-app/web/static" From e57819dc402ec6125cbf0801ce6225f88afe6385 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 29 Sep 2025 15:10:41 +0800 Subject: [PATCH 057/105] no pyscript.toml supplies basic config and test_build_app_no_wheel_pyscript_toml """An app with no pyscript.toml supplied by a wheel gets a basic config.""" matches original template logic --- src/briefcase/platforms/web/static.py | 6 ++- tests/platforms/web/static/test_build.py | 65 +++++++++++++++++++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 786b7dfe8..b045fdf17 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -432,9 +432,11 @@ def extract_backend_config(self, wheels): with wheel.open(pyscript_path) as pyscript_file: pyscript_config = tomllib.load(pyscript_file) except KeyError: - raise BriefcaseConfigError( - f"Pyscript configuration file not found in package: {config_package_list[0]}" + self.console.info( + f"Pyscript configuration file not found in package: {config_package_list[0]}\n" + "Using default configuration." ) + pyscript_config = {} return pyscript_config, pyscript_version diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index 17214e941..dc04d9b11 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -349,7 +349,7 @@ def test_build_app_config_no_backend(build_command, first_app_generated, tmp_pat bundle_path = tmp_path / "base_path/build/first-app/web/static" - # Mock some wheels without a config.toml + # Mock some wheels with a single config.toml containing no backend value. def mock_run(*args, **kwargs): if args[0][5] == "wheel": create_wheel( @@ -393,6 +393,69 @@ def mock_run(*args, **kwargs): build_command.build_app(first_app_generated) +def test_build_app_no_wheel_pyscript_toml(build_command, first_app_generated, tmp_path): + """An app with no pyscript.toml supplied by a wheel gets a basic config.""" + + bundle_path = tmp_path / "base_path/build/first-app/web/static" + + # Mock some wheels without a pyscript.toml + def mock_run(*args, **kwargs): + if args[0][5] == "wheel": + create_wheel( + bundle_path / "www/static/wheels", + "first_app", + extra_content=[ + ("dependency/static/style.css", "span { margin: 10px; }\n"), + ( + "dependency/deploy/config.toml", + """ +backend = "pyscript" + +[pyscript] +version = "2024.11.1" +""" + ), + ], + ) + elif args[0][5] == "pip": + create_wheel( + bundle_path / "www/static/wheels", + "dependency", + extra_content=[ + ("dependency/static/style.css", "div { margin: 10px; }\n"), + ], + ) + create_wheel( + bundle_path / "www/static/wheels", + "other", + extra_content=[ + ("other/static/style.css", "div { padding: 10px; }\n"), + ], + ) + else: + raise ValueError("Unknown command") + + build_command.tools.subprocess.run.side_effect = mock_run + + # 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": [ + "/static/wheels/dependency-1.2.3-py3-none-any.whl", + "/static/wheels/first_app-1.2.3-py3-none-any.whl", + "/static/wheels/other-1.2.3-py3-none-any.whl", + ], + } + + def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_path): """An app with extra pyscript.toml content can be written.""" bundle_path = tmp_path / "base_path/build/first-app/web/static" From f5703759e0033a40f953f3889a9ec3bdf78be567 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 29 Sep 2025 15:25:23 +0800 Subject: [PATCH 058/105] Invalid pyscript.toml check and test_build_app_invalid_wheel_pyscript_toml """An app with an invalid pyscript.toml raises an error.""" --- src/briefcase/platforms/web/static.py | 4 ++ tests/platforms/web/static/test_build.py | 65 ++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index b045fdf17..274d6f82c 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -431,6 +431,10 @@ def extract_backend_config(self, wheels): try: with wheel.open(pyscript_path) as pyscript_file: pyscript_config = tomllib.load(pyscript_file) + except tomllib.TOMLDecodeError as e: + raise BriefcaseConfigError( + f"pyscript.toml content isn't valid TOML: {e}" + ) from e except KeyError: self.console.info( f"Pyscript configuration file not found in package: {config_package_list[0]}\n" diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index dc04d9b11..5755dc8fb 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -456,6 +456,71 @@ def mock_run(*args, **kwargs): } +def test_build_app_invalid_wheel_pyscript_toml( + build_command, first_app_generated, tmp_path +): + """An app with an invalid pyscript.toml raises an error.""" + + bundle_path = tmp_path / "base_path/build/first-app/web/static" + + # Invoking build will create wheels as a side effect. + def mock_run(*args, **kwargs): + if args[0][5] == "wheel": + create_wheel( + bundle_path / "www/static/wheels", + "first_app", + extra_content=[ + ("dependency/static/style.css", "span { margin: 10px; }\n"), + ( + "dependency/deploy/config.toml", + """ +backend = "pyscript" + +[pyscript] +version = "2024.11.1" +""" + ), + ( + "dependency/deploy/pyscript.toml", + """ +This is not valid toml. +""" + ), + ], + ) + elif args[0][5] == "pip": + create_wheel( + bundle_path / "www/static/wheels", + "dependency", + extra_content=[ + ("dependency/static/style.css", "div { margin: 10px; }\n"), + ], + ) + create_wheel( + bundle_path / "www/static/wheels", + "other", + extra_content=[ + ("other/static/style.css", "div { padding: 10px; }\n"), + ], + ) + else: + raise ValueError("Unknown command") + + build_command.tools.subprocess.run.side_effect = mock_run + + # 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_custom_pyscript_toml(build_command, first_app_generated, tmp_path): """An app with extra pyscript.toml content can be written.""" bundle_path = tmp_path / "base_path/build/first-app/web/static" From 032b18cb661abaeb98767bdb77f7bcb517bfe292 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Mon, 29 Sep 2025 15:58:46 +0800 Subject: [PATCH 059/105] Remove pyscript.toml checks in test_app_package_fail and test_dependency_fail --- tests/platforms/web/static/test_build.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index 5755dc8fb..f5654dbe6 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -814,13 +814,6 @@ 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 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): """If dependencies can't be downloaded, an error is raised.""" @@ -890,9 +883,3 @@ 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 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, - } From e2a9a385136f17903612d0520d6e5348cd1bb477 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Wed, 1 Oct 2025 13:37:01 +0800 Subject: [PATCH 060/105] Update test_build__process_wheel.py Updat test_process_wheel and test_process_wheel_no_content to match the new functionality --- .../web/static/test_build__process_wheel.py | 68 +++++++------------ 1 file changed, 23 insertions(+), 45 deletions(-) diff --git a/tests/platforms/web/static/test_build__process_wheel.py b/tests/platforms/web/static/test_build__process_wheel.py index 10f344073..cdc0eb65c 100644 --- a/tests/platforms/web/static/test_build__process_wheel.py +++ b/tests/platforms/web/static/test_build__process_wheel.py @@ -1,5 +1,3 @@ -from io import StringIO - import pytest from briefcase.console import Console @@ -39,44 +37,26 @@ def test_process_wheel(build_command, tmp_path): ], ) - # Create a dummy css file - css_file = StringIO() - - build_command._process_wheel(wheel_filename, css_file=css_file) - - assert ( - css_file.getvalue() - == "\n".join( - [ - "", - "/*******************************************************", - " * dummy 1.2.3::first.css", - " *******************************************************/", - "", - "span {", - " font-color: red;", - " font-size: larger", - "}", - "", - "/*******************************************************", - " * dummy 1.2.3::second.css", - " *******************************************************/", - "", - "div {", - " padding: 10px", - "}", - "", - "/*******************************************************", - " * dummy 1.2.3::deep/third.css", - " *******************************************************/", - "", - "p {", - " color: red", - "}", - ] - ) - + "\n" - ) + # Collect into inserts dict + inserts = {} + build_command._process_wheel(wheelfile=wheel_filename, inserts=inserts) + + # Legacy CSS should be collected into the briefcase.css "CSS" insert slot + assert inserts == { + "static/css/briefcase.css": { + "CSS": { + "dummy 1.2.3 (legacy static CSS: first.css)": ( + "span {\n font-color: red;\n font-size: larger\n}\n" + ), + "dummy 1.2.3 (legacy static CSS: second.css)": ( + "div {\n padding: 10px\n}\n" + ), + "dummy 1.2.3 (legacy static CSS: deep/third.css)": ( + "p {\n color: red\n}\n" + ), + } + } + } def test_process_wheel_no_content(build_command, tmp_path): @@ -94,9 +74,7 @@ def test_process_wheel_no_content(build_command, tmp_path): ], ) - # Create a dummy css file - css_file = StringIO() - - build_command._process_wheel(wheel_filename, css_file=css_file) + inserts = {} + build_command._process_wheel(wheelfile=wheel_filename, inserts=inserts) - assert css_file.getvalue() == "" + assert inserts == {} From eb71d789eb404cb3f922e2fdaecb26e28b2b5b69 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Wed, 1 Oct 2025 14:20:50 +0800 Subject: [PATCH 061/105] Expand _process_wheel test coverage Collect deploy inserts, skip invalid insert names, warn for legacy /static CSS --- .../web/static/test_build__process_wheel.py | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/platforms/web/static/test_build__process_wheel.py b/tests/platforms/web/static/test_build__process_wheel.py index cdc0eb65c..2f9f85cb0 100644 --- a/tests/platforms/web/static/test_build__process_wheel.py +++ b/tests/platforms/web/static/test_build__process_wheel.py @@ -78,3 +78,80 @@ def test_process_wheel_no_content(build_command, tmp_path): build_command._process_wheel(wheelfile=wheel_filename, inserts=inserts) assert inserts == {} + + +def test_process_wheel_deploy_inserts(build_command, tmp_path): + """Deploy inserts are collected into the correct insert slot.""" + wheel_filename = create_wheel( + tmp_path, + extra_content=[ + ("dummy/deploy/inserts/index.html~header", ""), + ( + "dummy/deploy/inserts/static/css/briefcase.css~CSS", + "body { margin: 0; }", + ), + ], + ) + + inserts = {} + build_command._process_wheel(wheelfile=wheel_filename, inserts=inserts) + + # The index.html header insert exists + assert "index.html" in inserts + assert "header" in inserts["index.html"] + contribs = inserts["index.html"]["header"] + assert any("", "", - " ", - " ", - " ", + ' ', + ' ', + " ", " ", " ", - "
", + '
', "", " ", " ", @@ -187,7 +187,6 @@ def mock_run(*args, **kwargs): + "\n" ) - # briefcase.css has been appended with (bundle_path / "www/static/css/briefcase.css").open(encoding="utf-8") as f: assert ( @@ -199,25 +198,25 @@ def mock_run(*args, **kwargs): " display: None;", "}", "/*******************************************************************", - " ******************** Wheel contributed styles ********************/", - "", - "/*******************************************************", - " * dependency 1.2.3::style.css", - " *******************************************************/", + "******************** Wheel contributed styles ********************/", + "/*@@ css:start @@*/", + "/**************************************************", + " * dependency 1.2.3 (legacy static CSS: style.css)", + " *************************************************/", "", "div { margin: 10px; }", "", - "/*******************************************************", + "/**************************************************", " * first_app 1.2.3::style.css", - " *******************************************************/", + " *************************************************/", "", "span { margin: 10px; }", "", - "/*******************************************************", - " * other 1.2.3::style.css", - " *******************************************************/", - "", + "/**************************************************", + " * other 1.2.3 (legacy static CSS: style.css)", + " *************************************************/", "div { padding: 10px; }", + "/*@@ css:end @@*/" ] ) + "\n" @@ -298,7 +297,7 @@ def mock_run(*args, **kwargs): [pyscript] version = "2024.11.1" -""" +""", ), ], ) @@ -315,7 +314,7 @@ def mock_run(*args, **kwargs): [pyscript] version = "2024.10.1" -""" +""", ), ], ) @@ -413,7 +412,7 @@ def mock_run(*args, **kwargs): [pyscript] version = "2024.11.1" -""" +""", ), ], ) @@ -478,13 +477,13 @@ def mock_run(*args, **kwargs): [pyscript] version = "2024.11.1" -""" +""", ), ( "dependency/deploy/pyscript.toml", """ This is not valid toml. -""" +""", ), ], ) @@ -551,7 +550,7 @@ def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_ # Mock extracting pyscript.toml from a wheel. build_command.extract_backend_config = lambda _: ( {"existing-key-1": "value-1", "existing-key-2": 2}, - "2024.11.1" + "2024.11.1", ) # Build the web app. @@ -682,7 +681,7 @@ def mock_run(*args, **kwargs): # Mock extracting pyscript.toml from a wheel. build_command.extract_backend_config = lambda _: ( {"existing-key-1": "value-1", "existing-key-2": 2}, - "2024.11.1" + "2024.11.1", ) # Build the web app. @@ -751,13 +750,13 @@ def mock_run(*args, **kwargs): " display: None;", "}", "/*******************************************************************", - " ******************** Wheel contributed styles ********************/", - "", - "/*******************************************************", - " * first_app 1.2.3::style.css", - " *******************************************************/", - "", + "******************** Wheel contributed styles ********************/", + "/*@@ css:start @@*/", + "/**************************************************", + " * first_app 1.2.3 (legacy static CSS: style.css)", + " *************************************************/", "span { margin: 10px; }", + "/*@@ css:end @@*/", ] ) + "\n" @@ -882,4 +881,3 @@ def test_dependency_fail(build_command, first_app_generated, tmp_path): # Wheels folder still exists assert (bundle_path / "www/static/wheels").is_dir() - From 5c698cb531d6db89493db1d1d7b82778ac3c8c99 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Fri, 3 Oct 2025 12:18:57 +0800 Subject: [PATCH 077/105] test_build_app_config_backend_warning """Briefcase raises a warning if "backend" value is not pyscript.""" --- src/briefcase/platforms/web/static.py | 2 +- tests/platforms/web/static/test_build.py | 73 ++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index f43ee5873..f45e36caf 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -435,7 +435,7 @@ def extract_backend_config(self, wheels): # Currently, only pyscript is supported. Warn if another backend is found. elif backend != "pyscript": self.console.warning( - "Only 'pyscript' backend is currently supported for web static builds." + "Only 'pyscript' backend is currently supported for web static builds. " "This project may not work correctly." ) diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index 102f07fe4..ab8bfdecf 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -203,20 +203,19 @@ def mock_run(*args, **kwargs): "/**************************************************", " * dependency 1.2.3 (legacy static CSS: style.css)", " *************************************************/", - "", "div { margin: 10px; }", "", "/**************************************************", - " * first_app 1.2.3::style.css", + " * first_app 1.2.3 (legacy static CSS: style.css)", " *************************************************/", - "", "span { margin: 10px; }", "", "/**************************************************", " * other 1.2.3 (legacy static CSS: style.css)", " *************************************************/", "div { padding: 10px; }", - "/*@@ css:end @@*/" + "/*@@ css:end @@*/", + "" ] ) + "\n" @@ -338,7 +337,7 @@ def mock_run(*args, **kwargs): # Build the web app. with pytest.raises( BriefcaseConfigError, - match=r"Only 1 backend configuration file can be supplied.", + match=r"Only one backend configuration file can be supplied.", ): build_command.build_app(first_app_generated) @@ -391,6 +390,69 @@ def mock_run(*args, **kwargs): ): build_command.build_app(first_app_generated) +def test_build_app_config_backend_warning(build_command, first_app_generated, tmp_path, capsys): + """Briefcase raises a warning if "backend" value is not pyscript.""" + + bundle_path = tmp_path / "base_path/build/first-app/web/static" + + # Mock some wheels with a single config.toml containing no backend value. + def mock_run(*args, **kwargs): + if args[0][5] == "wheel": + create_wheel( + bundle_path / "www/static/wheels", + "first_app", + extra_content=[ + ("dependency/static/style.css", "span { margin: 10px; }\n"), + ( + "dependency/deploy/config.toml", + """ +backend = "something-else" +""", + ), + ], + ) + elif args[0][5] == "pip": + create_wheel( + bundle_path / "www/static/wheels", + "dependency", + extra_content=[ + ("dependency/static/style.css", "div { margin: 10px; }\n"), + ], + ) + create_wheel( + bundle_path / "www/static/wheels", + "other", + extra_content=[ + ("other/static/style.css", "div { padding: 10px; }\n"), + ], + ) + else: + raise ValueError("Unknown command") + + build_command.tools.subprocess.run.side_effect = mock_run + + # Mock the side effect of invoking shutil + build_command.tools.shutil.rmtree.side_effect = lambda *args: shutil.rmtree( + bundle_path / "www/static/wheels" + ) + + build_command.build_app(first_app_generated) + + # Capture output and assert warning present. + captured = capsys.readouterr() + assert ( + "Only 'pyscript' backend is currently supported for web static builds." in captured.out + ) + + # Check pyscript.toml was created and has correct packages. + with (bundle_path / "www/pyscript.toml").open("rb") as f: + assert tomllib.load(f) == { + "packages": [ + "/static/wheels/dependency-1.2.3-py3-none-any.whl", + "/static/wheels/first_app-1.2.3-py3-none-any.whl", + "/static/wheels/other-1.2.3-py3-none-any.whl", + ], + } def test_build_app_no_wheel_pyscript_toml(build_command, first_app_generated, tmp_path): """An app with no pyscript.toml supplied by a wheel gets a basic config.""" @@ -757,6 +819,7 @@ def mock_run(*args, **kwargs): " *************************************************/", "span { margin: 10px; }", "/*@@ css:end @@*/", + "" ] ) + "\n" From 79239a2647a47bd315222a44a0a818e65d969050 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Fri, 3 Oct 2025 13:13:06 +0800 Subject: [PATCH 078/105] add enconding to readtext calls --- .../web/static/test_build_write_inserts.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/platforms/web/static/test_build_write_inserts.py b/tests/platforms/web/static/test_build_write_inserts.py index ee8910ffa..e8a7eb452 100644 --- a/tests/platforms/web/static/test_build_write_inserts.py +++ b/tests/platforms/web/static/test_build_write_inserts.py @@ -47,7 +47,7 @@ def test_write_insert_warn_if_slot_missing(build_command, app_config, monkeypatc # Ensure warning was raised and file is unchanged assert any("markers not found" in w for w in warnings) - assert target.read_text() == file_text + assert target.read_text(encoding="utf-8") == file_text def test_write_insert_warn_if_file_missing(build_command, app_config, monkeypatch): @@ -83,11 +83,11 @@ def test_write_insert_is_idempotent(build_command, app_config): # Apply insert once inserts = {"header": {"pkg": "

Hello

"}} build_command.write_inserts(app_config, Path("index.html"), inserts) - once = target.read_text() + once = target.read_text(encoding="utf-8") # Apply insert again build_command.write_inserts(app_config, Path("index.html"), inserts) - twice = target.read_text() + twice = target.read_text(encoding="utf-8") # Ensure result did not change assert once == twice @@ -113,7 +113,7 @@ def test_write_insert_slot_name_regex_escaped(build_command, app_config): build_command.write_inserts(app_config, Path("index.html"), inserts) # Ensure content was inserted correctly - out = target.read_text() + out = target.read_text(encoding="utf-8") assert "works" in out @@ -128,7 +128,7 @@ def test_write_insert_css_packages_sorted(build_command, app_config): inserts = {"css": {"b": "b{}", "a": "a{}"}} build_command.write_inserts(app_config, Path("static/css/briefcase.css"), inserts) - out = target.read_text() + out = target.read_text(encoding="utf-8") a_block = CSS_BANNER.format(package="a", content="a{}") b_block = CSS_BANNER.format(package="b", content="b{}") @@ -151,7 +151,7 @@ def test_write_insert_replaces_all_matches(build_command, app_config): inserts = {"h": {"pkg": "Z"}} build_command.write_inserts(app_config, Path("index.html"), inserts) - out = target.read_text() + out = target.read_text(encoding="utf-8") # Both occurrences replaced assert "X" not in out and "Y" not in out @@ -180,7 +180,7 @@ def test_write_insert_handles_html_and_css_markers(build_command, app_config): inserts = {"assets": {"pkgA": "", "pkgB": "h1{}"}} build_command.write_inserts(app_config, Path("index.html"), inserts) - out = target.read_text() + out = target.read_text(encoding="utf-8") # Ensure both banners appear, new content is inserted, and old content removed assert " and ` * CSS/JS: `/*@@ insert:start @@*/ and /*@@ insert:end @@*/` - Inserts and package contributions are processed in sorted order to ensure deterministic builds. + Inserts and package contributions are processed in sorted order to ensure + deterministic builds. :param app: The application whose `pyscript.toml` is being written. :param filename: The file whose insert is to be written. @@ -233,7 +211,8 @@ def _append_pyscript_insert( insert = "python" # PyScript definitions for insertion: - content = dedent(f"""\ + content = dedent( + f"""\ - """) + """ + ) pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) pkg_map[package_key] = content @@ -253,32 +233,24 @@ def _handle_legacy_css( self, wheel: ZipFile, path: Path, - filename: str, package_key: str, inserts: dict[str, dict[str, dict[str, str]]], - legacy_css_warning: bool, - ) -> bool: + ) -> None: """Handle legacy CSS under /static/*.css and add to briefcase.css. :param wheel: Open wheel ZipFile being processed. :param path: Path object of the file inside the wheel. - :param filename: Filename string inside the wheel. :param package_key: Provenance label (e.g. "name version"). :param inserts: Nested dict of inserts keyed by target - insert - package. - :param legacy_css_warning: Whether the warning has already been shown. - :return: Updated legacy_css_warning flag. """ - self.console.info(f" Found {filename}") - # Show deprecation warning once per wheel - if not legacy_css_warning: - self.console.warning( - f" {Path(wheel.filename).name}: legacy '/static' CSS detected, " - "treating as insert into briefcase.css, this legacy handling will be removed in the future." - ) - legacy_css_warning = True + self.console.warning( + f" {Path(wheel.filename).name}: legacy '/static' CSS file {path} detected.\n" + " Static file handling has been deprecated; this file should be " + "converted into an insert." + ) - css_text = wheel.read(filename).decode("utf-8") + css_text = wheel.read(str(path)).decode("utf-8") rel_inside = "/".join(path.parts[2:]) contrib_key = f"{package_key} (legacy static CSS: {rel_inside})" @@ -292,8 +264,6 @@ def _handle_legacy_css( else: pkg_map[contrib_key] = css_text - return legacy_css_warning - def _handle_insert( self, wheel: ZipFile, @@ -354,9 +324,6 @@ def _process_wheel( name_parts = wheelfile.name.split("-") package_key = f"{name_parts[0]} {name_parts[1]}" - # Warning flag for legacy CSS - legacy_css_warning = False - with ZipFile(wheelfile) as wheel: for filename in sorted(wheel.namelist()): # Skip directories and shallow paths @@ -369,21 +336,14 @@ def _process_wheel( and parts[1] == "static" and path.suffix.lower() == ".css" ): - legacy_css_warning = self._handle_legacy_css( - wheel, path, filename, package_key, inserts, legacy_css_warning - ) + self._handle_legacy_css(wheel, path, package_key, inserts) # New deploy/inserts handling elif len(parts) >= 3 and parts[1] == "deploy" and parts[2] == "inserts": self._handle_insert(wheel, parts, filename, package_key, inserts) - else: - self.console.debug( - f" {filename}: skipping, not a supported insert." - ) - - def extract_backend_config(self, wheels): - """Processes multiple wheels to gather a config.toml and a base pyscript.toml + def extract_pyscript_config(self, wheels): + """Process multiple wheels to gather a config.toml and a base pyscript.toml file. :param wheels: A list of wheel files to be scanned. @@ -414,54 +374,54 @@ def extract_backend_config(self, wheels): # Raise an error if more than one configuration file is supplied. elif len(config_package_list) > 1: raise BriefcaseConfigError( - f"""Only one backend configuration file can be supplied. - Initial config.toml found in package: {config_package} - Duplicate config.toml found in package: {wheel.filename}""" + "Only one deployment configuration file can be supplied. " + f"Initial config.toml found in package: {config_package}; " + f"Duplicate config.toml found in package: {wheel.filename}" ) - # Gather a backend configuration file from the package. - # For now, this is a pyscript.toml as no other backend is currently supported. + # Gather a deployment configuration file from the package. For now, this is a + # pyscript.toml as no other backend is currently supported. else: - with ZipFile(config_package_list[0]) as wheel: - with wheel.open(config_filename) as config_file: - config_data = tomllib.load(config_file) - - # Gather backend type from config.toml - backend = config_data.get("backend", None) - - # Fail if no backend is present in config.toml - if backend is None: - raise BriefcaseConfigError( - "No backend was provided in config.toml file." - ) - # Currently, only pyscript is supported. Warn if another backend is found. - elif backend != "pyscript": - self.console.warning( - "Only 'pyscript' backend is currently supported for web static builds. " - "This project may not work correctly." - ) - - # Get pyscript version from config.toml. Use default if not present. - pyscript_version = config_data.get("pyscript", {}).get( - "version", pyscript_version + with ( + ZipFile(config_package_list[0]) as wheel, + wheel.open(config_filename) as config_file, + ): + config_data = tomllib.load(config_file) + + # Gather implementation from config.toml + implementation = config_data.get("implementation", None) + + # Currently, only pyscript is supported. Warn if another implementation is found, + # but fail safe to using pyscript with a warning. + if implementation is None: + self.console.warning( + "No web implementation specified. Defaulting to 'pyscript'." ) + elif implementation != "pyscript": + self.console.warning( + "At present, 'pyscript' is the only supported web implementation. " + "This project may not work correctly." + ) + + # Get pyscript version from config.toml. Use default if not present. + pyscript_version = config_data.get("pyscript", {}).get( + "version", pyscript_version + ) - # Extract pyscript.toml - pyscript_path = config_filename.replace( - "config.toml", "pyscript.toml" + # Extract pyscript.toml + pyscript_path = config_filename.replace("config.toml", "pyscript.toml") + try: + with wheel.open(pyscript_path) as pyscript_file: + pyscript_config = tomllib.load(pyscript_file) + except tomllib.TOMLDecodeError as e: + raise BriefcaseConfigError( + f"pyscript.toml content isn't valid TOML: {e}" + ) from e + except (KeyError, FileNotFoundError): + self.console.info( + f"Pyscript configuration file not found in {config_package_list[0]}. " + "Using default configuration." ) - try: - with wheel.open(pyscript_path) as pyscript_file: - pyscript_config = tomllib.load(pyscript_file) - except tomllib.TOMLDecodeError as e: - raise BriefcaseConfigError( - f"pyscript.toml content isn't valid TOML: {e}" - ) from e - except (KeyError, FileNotFoundError): - self.console.info( - f"Pyscript configuration file not found in package: {config_package_list[0]}\n" - "Using default configuration." - ) - pyscript_config = {} + pyscript_config = {} return pyscript_config, pyscript_version @@ -529,7 +489,7 @@ def build_app(self, app: AppConfig, **kwargs): with self.console.wait_bar("Writing Pyscript configuration file..."): # Load any pre-existing pyscript.toml provided by a GUI Toolkit. If the file # doesn't exist, assume an empty pyscript.toml as a starting point. - config, pyscript_version = self.extract_backend_config( + config, pyscript_version = self.extract_pyscript_config( self.wheel_path(app).glob("*.whl") ) @@ -558,13 +518,6 @@ def build_app(self, app: AppConfig, **kwargs): tomli_w.dump(config, f) with self.console.wait_bar("Compiling static web content from wheels..."): - # Trim previously compiled content out of briefcase.css - briefcase_css_path = self.project_path(app) / "static/css/briefcase.css" - self._trim_file( - briefcase_css_path, - sentinel=" ******************* Wheel contributed styles **********************/", - ) - inserts: dict[str, dict[str, dict[str, str]]] = {} for wheelfile in sorted(self.wheel_path(app).glob("*.whl")): diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index dd6f342ab..07dc9789c 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -50,7 +50,7 @@ def mock_run(*args, **kwargs): ( "dependency/deploy/config.toml", """ -backend = "pyscript" +implementation = "pyscript" [pyscript] version = "2024.11.1" @@ -250,7 +250,7 @@ def test_build_app_custom_pyscript_toml(build_command, first_app_generated, tmp_ ) # Mock extracting pyscript.toml from a wheel. - build_command.extract_backend_config = lambda _: ( + build_command.extract_pyscript_config = lambda _: ( {"existing-key-1": "value-1", "existing-key-2": 2}, "2024.11.1", ) @@ -381,7 +381,7 @@ def mock_run(*args, **kwargs): ) # Mock extracting pyscript.toml from a wheel. - build_command.extract_backend_config = lambda _: ( + build_command.extract_pyscript_config = lambda _: ( {"existing-key-1": "value-1", "existing-key-2": 2}, "2024.11.1", ) diff --git a/tests/platforms/web/static/test_build__process_wheel.py b/tests/platforms/web/static/test_build__process_wheel.py index d0cd126d7..29007ad23 100644 --- a/tests/platforms/web/static/test_build__process_wheel.py +++ b/tests/platforms/web/static/test_build__process_wheel.py @@ -117,7 +117,7 @@ def test_process_wheel_deploy_inserts(build_command, tmp_path): assert any("body { margin: 0; }" in v for v in css_contribs.values()) -def test_process_wheel_legacy_css_warning_once(build_command, tmp_path, monkeypatch): +def test_process_wheel_legacy_css_warning_once(build_command, tmp_path, capsys): """Legacy CSS files trigger a single deprecation warning.""" wheel_filename = create_wheel( tmp_path, @@ -127,17 +127,18 @@ def test_process_wheel_legacy_css_warning_once(build_command, tmp_path, monkeypa ], ) - # Check on console.warning to count legacy warnings - warnings = [] - monkeypatch.setattr( - build_command.console, "warning", lambda msg: warnings.append(msg) - ) - inserts = {} build_command._process_wheel(wheelfile=wheel_filename, inserts=inserts) - legacy_msgs = [m for m in warnings if "legacy '/static' CSS detected" in m] - assert len(legacy_msgs) == 1 + output = capsys.readouterr().out + assert ( + "dummy-1.2.3-py3-none-any.whl: legacy '/static' CSS file dummy/static/one.css detected." + in output + ) + assert ( + "dummy-1.2.3-py3-none-any.whl: legacy '/static' CSS file dummy/static/two.css detected." + in output + ) def test_process_wheel_non_utf8_insert(build_command, tmp_path): @@ -164,28 +165,20 @@ def test_process_wheel_non_utf8_legacy_css(build_command, tmp_path): build_command._process_wheel(wheelfile=wheel_path, inserts=inserts) -def test_handle_legacy_css_warning_once_and_append( - build_command, tmp_path, monkeypatch -): +def test_handle_legacy_css_warning_once_and_append(build_command, tmp_path, capsys): """Legacy CSS warns once and appends to existing contrib key.""" wheel_path = Path(tmp_path) / "dummy-1.2.3.whl" with zipfile.ZipFile(wheel_path, "w") as zf: zf.writestr("dummy/static/one.css", "h1 { x:1; }") - # Count warnings - warnings = [] - monkeypatch.setattr(build_command.console, "warning", warnings.append) - inserts = {} with zipfile.ZipFile(wheel_path) as zf: # First call - warn - warned = build_command._handle_legacy_css( + build_command._handle_legacy_css( wheel=zf, path=Path("dummy/static/one.css"), - filename="dummy/static/one.css", package_key="pkg 1.0", inserts=inserts, - legacy_css_warning=False, ) # Pre-seed same contrib key to force append on second call target_map = inserts.setdefault("static/css/briefcase.css", {}).setdefault( @@ -194,18 +187,19 @@ def test_handle_legacy_css_warning_once_and_append( key = "pkg 1.0 (legacy static CSS: one.css)" target_map[key] = target_map[key] + "\n/*extra*/" # Second call - no additional warning, content appended - warned = build_command._handle_legacy_css( + build_command._handle_legacy_css( wheel=zf, path=Path("dummy/static/one.css"), - filename="dummy/static/one.css", package_key="pkg 1.0", inserts=inserts, - legacy_css_warning=warned, ) - # One warning only - legacy_msgs = [m for m in warnings if "legacy '/static' CSS detected" in m] - assert len(legacy_msgs) == 1 + # One warning for each file + output = capsys.readouterr().out + assert ( + "dummy-1.2.3.whl: legacy '/static' CSS file dummy/static/one.css detected." + in output + ) # Content appended out = inserts["static/css/briefcase.css"]["css"][key] @@ -240,7 +234,11 @@ def test_handle_insert_register_valid_file(build_command, tmp_path): ], ) def test_handle_insert_skip_dir_entries( - build_command, tmp_path, monkeypatch, entry, expected_skip + build_command, + tmp_path, + monkeypatch, + entry, + expected_skip, ): """Deploy/inserts directory entries are skipped with a debug log. @@ -277,7 +275,10 @@ def test_handle_insert_skip_dir_entries( @pytest.mark.parametrize("mode", ["integration", "unit"]) def missing_tilde_under_deploy_inserts_skipped( - build_command, tmp_path, monkeypatch, mode + build_command, + tmp_path, + monkeypatch, + mode, ): """Files under deploy/inserts without '~' are skipped with a debug log. diff --git a/tests/platforms/web/static/test_build__trim_file.py b/tests/platforms/web/static/test_build__trim_file.py deleted file mode 100644 index fbb92b382..000000000 --- a/tests/platforms/web/static/test_build__trim_file.py +++ /dev/null @@ -1,118 +0,0 @@ -import pytest - -from briefcase.platforms.web.static import StaticWebBuildCommand - -from ....utils import create_file - - -@pytest.fixture -def build_command(dummy_console, tmp_path): - return StaticWebBuildCommand( - console=dummy_console, - base_path=tmp_path / "base_path", - data_path=tmp_path / "briefcase", - ) - - -def test_trim_file(build_command, tmp_path): - """A file can be trimmed at a sentinel.""" - filename = tmp_path / "dummy.txt" - content = [ - "This is before the sentinel.", - "This is also before the sentinel.", - " ** This is the sentinel ** ", - "This is after the sentinel.", - "This is also after the sentinel.", - ] - - create_file(filename, "\n".join(content)) - - # Trim the file at the sentinel - build_command._trim_file(filename, sentinel=" ** This is the sentinel ** ") - - # The file contains everything up to and including the sentinel. - with filename.open(encoding="utf-8") as f: - assert f.read() == "\n".join(content[:3]) + "\n" - - -def test_trim_no_sentinel(build_command, tmp_path): - """A file that doesn't contain the sentinel is returned as-is.""" - filename = tmp_path / "dummy.txt" - content = [ - "This is before the sentinel.", - "This is also before the sentinel.", - "NO SENTINEL HERE", - "This is after the sentinel.", - "This is also after the sentinel.", - ] - - create_file(filename, "\n".join(content)) - - # Trim the file at a sentinel - build_command._trim_file(filename, sentinel=" ** This is the sentinel ** ") - - # The file is unmodified. - with filename.open(encoding="utf-8") as f: - assert f.read() == "\n".join(content) - - -def test_trim_file_multiple_sentinels(build_command, tmp_path): - """A file with multiple sentinels is trimmed at the first one.""" - filename = tmp_path / "dummy.txt" - content = [ - "This is before the sentinel.", - "This is also before the sentinel.", - " ** This is the sentinel ** ", - "This is after the first sentinel.", - "This is also after the first sentinel.", - " ** This is the sentinel ** ", - "This is after the second sentinel.", - "This is also after the second sentinel.", - ] - - create_file(filename, "\n".join(content)) - - # Trim the file at the sentinel - build_command._trim_file(filename, sentinel=" ** This is the sentinel ** ") - - # The file contains everything up to and including the sentinel. - with filename.open(encoding="utf-8") as f: - assert f.read() == "\n".join(content[:3]) + "\n" - - -def test_trim_sentinel_last_line(build_command, tmp_path): - """A file with the sentinel as the last full line isn't a problem.""" - filename = tmp_path / "dummy.txt" - content = [ - "This is before the sentinel.", - "This is also before the sentinel.", - " ** This is the sentinel ** ", - ] - - create_file(filename, "\n".join(content) + "\n") - - # Trim the file at a sentinel - build_command._trim_file(filename, sentinel=" ** This is the sentinel ** ") - - # The file is unmodified. - with filename.open(encoding="utf-8") as f: - assert f.read() == "\n".join(content) + "\n" - - -def test_trim_sentinel_EOF(build_command, tmp_path): - """A file with the sentinel at EOF isn't a problem.""" - filename = tmp_path / "dummy.txt" - content = [ - "This is before the sentinel.", - "This is also before the sentinel.", - " ** This is the sentinel ** ", - ] - - create_file(filename, "\n".join(content)) - - # Trim the file at a sentinel - build_command._trim_file(filename, sentinel=" ** This is the sentinel ** ") - - # The file is unmodified. - with filename.open(encoding="utf-8") as f: - assert f.read() == "\n".join(content) diff --git a/tests/platforms/web/static/test_build_extract_backend_config.py b/tests/platforms/web/static/test_build_extract_pyscript_config.py similarity index 69% rename from tests/platforms/web/static/test_build_extract_backend_config.py rename to tests/platforms/web/static/test_build_extract_pyscript_config.py index 5906c2fc6..8eb51a2d3 100644 --- a/tests/platforms/web/static/test_build_extract_backend_config.py +++ b/tests/platforms/web/static/test_build_extract_pyscript_config.py @@ -38,12 +38,12 @@ def _mock_wheel(tmp_path, wheel_name, files): return wheel_path -def test_extract_backend_config(build_command, tmp_path): +def test_extract_pyscript_config(build_command, tmp_path): """Test function works correctly with both config.toml and pyscript.toml.""" # Mock a wheel with files files = { "dependency/deploy/config.toml": """ -backend = "pyscript" +implementation = "pyscript" [pyscript] version = "2024.10.1" @@ -57,7 +57,7 @@ def test_extract_backend_config(build_command, tmp_path): wheel_path = _mock_wheel(tmp_path=tmp_path, wheel_name="dependency", files=files) # Run the function. - pyscript_config, pyscript_version = build_command.extract_backend_config( + pyscript_config, pyscript_version = build_command.extract_pyscript_config( [wheel_path] ) @@ -69,7 +69,7 @@ def test_extract_backend_config(build_command, tmp_path): assert pyscript_version == "2024.10.1" -def test_extract_backend_config_no_config(build_command, tmp_path): +def test_extract_pyscript_config_no_config(build_command, tmp_path): """If no config.toml supplied by wheels, functions returns a basic config.""" # Mock a wheel without the needed files files = {"dependency/deploy/not-the-files-you-are-looking-for.toml": ""} @@ -77,7 +77,7 @@ def test_extract_backend_config_no_config(build_command, tmp_path): wheel_path = _mock_wheel(tmp_path=tmp_path, wheel_name="dependency", files=files) # Run the function. - pyscript_config, pyscript_version = build_command.extract_backend_config( + pyscript_config, pyscript_version = build_command.extract_pyscript_config( [wheel_path] ) @@ -86,13 +86,13 @@ def test_extract_backend_config_no_config(build_command, tmp_path): assert pyscript_version == "2024.11.1" -def test_extract_backend_config_multiple_config(build_command, tmp_path): +def test_extract_pyscript_config_multiple_config(build_command, tmp_path): """Multiple config.toml supplied by wheels fails.""" # Mock wheels that both contain config.toml file_set_1 = { "dependency/deploy/config.toml": """ -backend = "pyscript" +implementation = "pyscript" [pyscript] version = "2024.10.1" @@ -101,7 +101,7 @@ def test_extract_backend_config_multiple_config(build_command, tmp_path): file_set_2 = { "dependency/deploy/config.toml": """ -backend = "pyscript" +implementation = "pyscript" [pyscript] version = "2024.10.1" @@ -118,46 +118,55 @@ def test_extract_backend_config_multiple_config(build_command, tmp_path): # Run the function. with pytest.raises( BriefcaseConfigError, - match=r"Only one backend configuration file can be supplied.", + match=r"Only one deployment configuration file can be supplied.", ): - build_command.extract_backend_config([wheel_path_1, wheel_path_2]) + build_command.extract_pyscript_config([wheel_path_1, wheel_path_2]) -def test_extract_backend_config_no_backend(build_command, tmp_path): - """An app cannot be built with a config.toml containing no "backend" value.""" +def test_extract_pyscript_config_no_implementation(build_command, tmp_path, capsys): + """An app with no "implementation" value defaults to pyscript with a warning.""" # Mock a wheel with the needed files files = {"dependency/deploy/config.toml": ""} wheel_path = _mock_wheel(tmp_path=tmp_path, wheel_name="dependency", files=files) - # Build the web app. - with pytest.raises( - BriefcaseConfigError, - match=r"No backend was provided in config.toml file.", - ): - build_command.extract_backend_config([wheel_path]) + pyscript_config, pyscript_version = build_command.extract_pyscript_config( + [wheel_path] + ) + + # Capture output and assert warning present. + captured = capsys.readouterr() + assert "No web implementation specified. Defaulting to 'pyscript'." in captured.out + + # Check pyscript_config is empty and pyscript_version is the default. + assert pyscript_config == {} + assert pyscript_version == "2024.11.1" -def test_extract_backend_config_backend_warning(build_command, tmp_path, capsys): - """Briefcase raises a warning if "backend" value is not pyscript.""" +def test_extract_pyscript_config_implementation_warning( + build_command, + tmp_path, + capsys, +): + """Briefcase raises a warning if "implementation" value is not pyscript.""" files = { "dependency/deploy/config.toml": """ -backend = "something-else" +implementation = "something-else" """, } wheel_path = _mock_wheel(tmp_path=tmp_path, wheel_name="dependency", files=files) - pyscript_config, pyscript_version = build_command.extract_backend_config( + pyscript_config, pyscript_version = build_command.extract_pyscript_config( [wheel_path] ) # Capture output and assert warning present. captured = capsys.readouterr() assert ( - "Only 'pyscript' backend is currently supported for web static builds." + "At present, 'pyscript' is the only supported web implementation." in captured.out ) @@ -166,37 +175,37 @@ def test_extract_backend_config_backend_warning(build_command, tmp_path, capsys) assert pyscript_version == "2024.11.1" -def test_extract_backend_config_no_pyscript_toml(build_command, tmp_path, capsys): +def test_extract_pyscript_config_no_pyscript_toml(build_command, tmp_path, capsys): """If no pyscript.toml is supplied by a wheel, function returns a basic config.""" # Create wheel with no pyscript.toml files = { "dependency/deploy/config.toml": """ -backend = "pyscript" +implementation = "pyscript" """, } wheel_path = _mock_wheel(tmp_path=tmp_path, wheel_name="dependency", files=files) - pyscript_config, pyscript_version = build_command.extract_backend_config( + pyscript_config, pyscript_version = build_command.extract_pyscript_config( [wheel_path] ) # Capture output and assert console info is present. captured = capsys.readouterr() - assert "Pyscript configuration file not found in package:" in captured.out + assert "Pyscript configuration file not found in" in captured.out # Check pyscript_config is empty and pyscript_version is the default. assert pyscript_config == {} assert pyscript_version == "2024.11.1" -def test_extract_backend_config_invalid_wheel_pyscript_toml(build_command, tmp_path): +def test_extract_pyscript_config_invalid_wheel_pyscript_toml(build_command, tmp_path): """A wheel with an invalid pyscript.toml raises an error.""" # Mock a wheel with files files = { "dependency/deploy/config.toml": """ -backend = "pyscript" +implementation = "pyscript" [pyscript] version = "2024.10.1" @@ -213,4 +222,4 @@ def test_extract_backend_config_invalid_wheel_pyscript_toml(build_command, tmp_p BriefcaseConfigError, match=r"Briefcase configuration error: pyscript.toml content isn't valid TOML: Expected", ): - build_command.extract_backend_config([wheel_path]) + build_command.extract_pyscript_config([wheel_path]) From b487898644589af066ef9f0e3d81a794b6411b34 Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Wed, 8 Oct 2025 17:15:00 +0800 Subject: [PATCH 095/105] add body-python content to _append_pyscript_insert --- src/briefcase/platforms/web/static.py | 25 ++++++++++++++++++++---- tests/platforms/web/static/conftest.py | 10 ++++++++-- tests/platforms/web/static/test_build.py | 16 +++++++++++++-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index f5a75ecc4..52c4b3f5f 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -194,6 +194,7 @@ def write_inserts( def _append_pyscript_insert( self, + app, filename: Path, pyscript_version: str, inserts: dict[str, dict[str, dict[str, str]]], @@ -202,16 +203,18 @@ def _append_pyscript_insert( This function creates an insert for PyScript. + :param app: The application whose html file is being written. :param filename: The html file whose pyscript version is to be written. :param pyscript_version: The pyscript version number to be inserted. :param inserts: Nested dict of inserts keyed by target - insert - package. """ package_key = "briefcase" target = filename - insert = "python" + head_insert = "head-python" + body_insert = "body-python" # PyScript definitions for insertion: - content = dedent( + head_content = dedent( f"""\ """ ) + body_content = dedent( + f"""\ + + """ + ) + + pkg_map_head = inserts.setdefault(target, {}).setdefault(head_insert, {}) + pkg_map_head[package_key] = head_content + + pkg_map_body = inserts.setdefault(target, {}).setdefault(body_insert, {}) + pkg_map_body[package_key] = body_content - pkg_map = inserts.setdefault(target, {}).setdefault(insert, {}) - pkg_map[package_key] = content def _handle_legacy_css( self, diff --git a/tests/platforms/web/static/conftest.py b/tests/platforms/web/static/conftest.py index 2126b9dc4..fa2d14153 100644 --- a/tests/platforms/web/static/conftest.py +++ b/tests/platforms/web/static/conftest.py @@ -24,12 +24,18 @@ def first_app_generated(first_app_config, tmp_path): - - + +
+ + + + + + diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index 07dc9789c..c67c2a42f 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -159,7 +159,7 @@ def mock_run(*args, **kwargs): " ", " ", " ", - " ", + " ", " ", @@ -173,11 +173,23 @@ def mock_run(*args, **kwargs): "", ' ', ' ', - " ", + " ", " ", " ", '
', "", + " ", + " ", + "", + " ", + ' ", + " ", + "", " ", " ", " ", From b8b0ccb014f069f0e02c2d5654604550e659694b Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Wed, 8 Oct 2025 17:17:00 +0800 Subject: [PATCH 096/105] minor fix to test_build.py --- tests/platforms/web/static/test_build.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index c67c2a42f..af2f8c420 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -182,6 +182,9 @@ def mock_run(*args, **kwargs): " ", "", " ", + " ", ' """ @@ -249,6 +249,7 @@ def _handle_legacy_css( self, wheel: ZipFile, path: Path, + filename: str, package_key: str, inserts: dict[str, dict[str, dict[str, str]]], ) -> None: @@ -258,6 +259,7 @@ def _handle_legacy_css( :param wheel: Open wheel ZipFile being processed. :param path: Path object of the file inside the wheel. + :param filename: Filename string inside the wheel. :param package_key: Provenance label (e.g. "name version"). :param inserts: Nested dict of inserts keyed by target - insert - package. """ @@ -268,7 +270,7 @@ def _handle_legacy_css( "converted into an insert." ) - css_text = wheel.read(str(path)).decode("utf-8") + css_text = wheel.read(filename).decode("utf-8") rel_inside = "/".join(path.parts[2:]) contrib_key = f"{package_key} (legacy static CSS: {rel_inside})" @@ -356,7 +358,7 @@ def _process_wheel( and parts[1] == "static" and path.suffix.lower() == ".css" ): - self._handle_legacy_css(wheel, path, package_key, inserts) + self._handle_legacy_css(wheel, path, filename, package_key, inserts) # New deploy/inserts handling elif len(parts) >= 3 and parts[1] == "deploy" and parts[2] == "inserts": @@ -548,7 +550,7 @@ def build_app(self, app: AppConfig, **kwargs): ) # Add pyscript insertion to inserts - self._append_pyscript_insert("index.html", pyscript_version, inserts) + self._append_pyscript_insert(app, "index.html", pyscript_version, inserts) # Write inserts per target for target, target_inserts in sorted(inserts.items()): diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index af2f8c420..1240bc51e 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -186,10 +186,10 @@ def mock_run(*args, **kwargs): " * briefcase", " -------------------------------------------------->", ' ", " ", "", From 48fad39da4f48ec1fe422cd63b4c83254b149052 Mon Sep 17 00:00:00 2001 From: caydnn Date: Sat, 11 Oct 2025 16:35:14 +0800 Subject: [PATCH 102/105] fixes for test_build__handle_legacy_css.py --- src/briefcase/platforms/web/static.py | 2 +- tests/platforms/web/static/test_build__handle_legacy_css.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index e5f6073f0..7f394dea9 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -265,7 +265,7 @@ def _handle_legacy_css( """ # Warn on every legacy usage self.console.warning( - f" {Path(wheel.filename).name}: legacy '/static' CSS file {path} detected.\n" + f" {Path(wheel.filename).name}: legacy '/static' CSS file {filename} detected.\n" " Static file handling has been deprecated; this file should be " "converted into an insert." ) diff --git a/tests/platforms/web/static/test_build__handle_legacy_css.py b/tests/platforms/web/static/test_build__handle_legacy_css.py index 6666506c1..d90b3e107 100644 --- a/tests/platforms/web/static/test_build__handle_legacy_css.py +++ b/tests/platforms/web/static/test_build__handle_legacy_css.py @@ -20,7 +20,7 @@ def test_handle_legacy_css_warn_and_append( tmp_path, capsys, ): - """Legacy CSS warns and appends to existing contrib key.""" + """Legacy CSS warns and appends to new contrib key.""" wheel_path = Path(tmp_path) / "dummy-1.2.3.whl" with zipfile.ZipFile(wheel_path, "w") as zf: zf.writestr("dummy/static/one.css", "h1 { x:1; }") @@ -31,6 +31,7 @@ def test_handle_legacy_css_warn_and_append( build_command._handle_legacy_css( wheel=zf, path=Path("dummy/static/one.css"), + filename="dummy/static/one.css", package_key="pkg 1.0", inserts=inserts, ) @@ -44,6 +45,7 @@ def test_handle_legacy_css_warn_and_append( build_command._handle_legacy_css( wheel=zf, path=Path("dummy/static/one.css"), + filename="dummy/static/one.css", package_key="pkg 1.0", inserts=inserts, ) @@ -72,6 +74,7 @@ def test_handle_legacy_css_non_utf8_raise(build_command, tmp_path): build_command._handle_legacy_css( wheel=zf, path=Path("dummy/static/bad.css"), + filename="dummy/static/bad.css", package_key="pkg 1.0", inserts=inserts, ) From 89b7aec3ad7305a9d693f182caea2d456c1e7972 Mon Sep 17 00:00:00 2001 From: caydnn Date: Sat, 11 Oct 2025 16:48:24 +0800 Subject: [PATCH 103/105] remove monkeypatch from test_build__handle_insert.py debugs have been changed to warning, monkey patch no longer necessary --- .../web/static/test_build__handle_insert.py | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/platforms/web/static/test_build__handle_insert.py b/tests/platforms/web/static/test_build__handle_insert.py index 99000a865..aaf6c4bd0 100644 --- a/tests/platforms/web/static/test_build__handle_insert.py +++ b/tests/platforms/web/static/test_build__handle_insert.py @@ -46,7 +46,7 @@ def test_handle_insert_register_valid_file(build_command, tmp_path): def test_handle_insert_skip_dir_entries( build_command, tmp_path, - monkeypatch, + capsys, entry, expected_skip, ): @@ -59,10 +59,6 @@ def test_handle_insert_skip_dir_entries( with zipfile.ZipFile(wheel_path, "w") as zf: zf.writestr(entry, "") - # Capture debug messages - debugs = [] - monkeypatch.setattr(build_command.console, "debug", debugs.append) - # Run the handler against this entry inserts = {} with zipfile.ZipFile(wheel_path) as zf: @@ -74,19 +70,21 @@ def test_handle_insert_skip_dir_entries( inserts=inserts, ) + output = capsys.readouterr().out + # Directory entries should be ignored completely assert inserts == {} # And a debug message explaining the skip should be logged if expected_skip: - assert any("skipping, not a valid insert file" in m for m in debugs) + assert "skipping; not a valid insert file" in output else: - assert all("skipping, not a valid insert file" not in m for m in debugs) + assert "skipping; not a valid insert file" not in output def test_handle_insert_missing_tilde_skipped( build_command, tmp_path, - monkeypatch, + capsys, ): """Files under deploy/inserts without '~' are skipped with a debug log.""" # Create a dummy wheel containing an invalid insert file (no "~" in name) @@ -95,10 +93,6 @@ def test_handle_insert_missing_tilde_skipped( with zipfile.ZipFile(wheel_filename, "w") as zf: zf.writestr(missing_tilde, "
oops
") - # Capture debug messages - debugs = [] - monkeypatch.setattr(build_command.console, "debug", debugs.append) - inserts = {} # Run handler directly on the invalid file with zipfile.ZipFile(wheel_filename) as zf: @@ -110,10 +104,12 @@ def test_handle_insert_missing_tilde_skipped( inserts=inserts, ) + output = capsys.readouterr().out + # File should be ignored completely assert inserts == {} # And a debug message should clearly say why (missing "~") - assert any("must match '~'" in m for m in debugs) + assert "must match '~'" in output def test_handle_insert_append_existing_contrib(build_command, tmp_path): From cab03a9e40ee1eb99209ce08ba966f3dcd6106fb Mon Sep 17 00:00:00 2001 From: 19766375 <19766375@student.curtin.edu.au> Date: Sat, 11 Oct 2025 19:32:04 +0800 Subject: [PATCH 104/105] add quotes to pyscript body script --- src/briefcase/platforms/web/static.py | 2 +- tests/platforms/web/static/test_build.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/briefcase/platforms/web/static.py b/src/briefcase/platforms/web/static.py index 7f394dea9..a7ee828a4 100644 --- a/src/briefcase/platforms/web/static.py +++ b/src/briefcase/platforms/web/static.py @@ -233,7 +233,7 @@ def _append_pyscript_insert( """ diff --git a/tests/platforms/web/static/test_build.py b/tests/platforms/web/static/test_build.py index 1240bc51e..f34b0a696 100644 --- a/tests/platforms/web/static/test_build.py +++ b/tests/platforms/web/static/test_build.py @@ -188,7 +188,7 @@ def mock_run(*args, **kwargs): ' ", " ", From ea9c0da2fc9fb9c10426ffe3428e7d04c3da7639 Mon Sep 17 00:00:00 2001 From: kavi2du Date: Wed, 15 Oct 2025 11:28:27 +0800 Subject: [PATCH 105/105] Update static.rst documentation Update static.rst to reflect the new web platform insert system and deployment config --- docs/reference/platforms/web/static.rst | 42 +++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/reference/platforms/web/static.rst b/docs/reference/platforms/web/static.rst index 7bc9cdc7e..df81cd2cf 100644 --- a/docs/reference/platforms/web/static.rst +++ b/docs/reference/platforms/web/static.rst @@ -97,3 +97,45 @@ and change the default PyScript app name, you could use:: [[runtimes]] src = "https://example.com/custom/pyodide.js" """ + +Deployment Configuration +======================== + +Web packages can include deployment configuration to control how Briefcase builds +and deploys web applications. The deployment process relies on a ``config.toml`` +file and associated deployment files included in Python packages. + +config.toml +----------- + +A package can specify deployment settings through a ``config.toml`` file located +at ``/deploy/config.toml``. The deployment configuration file accepts the +following information: + +* ``implementation``: The web implementation to use (currently only ``"pyscript"`` + is supported). If not specified, defaults to ``"pyscript"``. +* ``pyscript.version``: The PyScript version to use (e.g., ``"2024.11.1"``). If not + specified, Briefcase will use its default PyScript version. + +Example ``config.toml``:: + + implementation = "pyscript" + + [pyscript] + version = "2024.11.1" + +pyscript.toml +------------- + +A package can include a base ``pyscript.toml`` file located at +``/deploy/pyscript.toml``. The base configuration in this file serves as +the foundation for Briefcase to generate the final ``pyscript.toml`` file. +Briefcase performs the following operations when processing this file: + +1. The system reads the base ``pyscript.toml`` file from the package directory. +2. The system adds the ``packages`` list which contains all wheel files to the configuration. +3. Merge in any :attr:`extra_pyscript_toml_content` from ``pyproject.toml`` + +A Briefcase project must have only one package that defines deployment configuration +through a ``config.toml`` file. The system will produce an error when multiple packages +in the dependencies attempt to define deployment settings.