Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Manage stub binaries as external resources #1849

Merged
merged 6 commits into from
Jun 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/933.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Templates that use pre-compiled stub binaries can now manage that artefact as an independent resource, rather than needing to include the binary in the template repository. This significantly reduces the size of the macOS and Windows app templates.
12 changes: 11 additions & 1 deletion docs/reference/commands/build.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ running:
$ briefcase build

``--update-support``
----------------------
--------------------

Update application support package before building. Equivalent to running:

Expand All @@ -84,6 +84,16 @@ Update application support package before building. Equivalent to running:
$ briefcase update --update-resources
$ briefcase build

``--update-stub``
-----------------

Update stub binary before building. Equivalent to running:

.. code-block:: console

$ briefcase update --update-stub
$ briefcase build

``--test``
----------

Expand Down
13 changes: 12 additions & 1 deletion docs/reference/commands/run.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ running:
$ briefcase run

``--update-support``
----------------------
--------------------

Update application support package before running. Equivalent to running:

Expand All @@ -112,6 +112,17 @@ Update application support package before running. Equivalent to running:
$ briefcase build
$ briefcase run

``--update-stub``
-----------------

Update stub binary before running. Equivalent to running:

.. code-block:: console

$ briefcase update --update-stub
$ briefcase build
$ briefcase run

``--test``
----------

Expand Down
7 changes: 6 additions & 1 deletion docs/reference/commands/update.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ Update application requirements.
Update application resources such as icons.

``--update-support``
----------------------
--------------------

Update application support package.

``--update-stub``
-----------------

Update stub binary.
20 changes: 20 additions & 0 deletions docs/reference/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,26 @@ color for splash screens.
If the platform output format does not use a splash screen, this setting is
ignored.

``stub_binary``
~~~~~~~~~~~~~~~

A file path or URL pointing at a pre-compiled binary (or a zip/tarball of a binary) that
can be used as an entry point for a bundled application.

If this setting is not provided, and a stub binary is required by the platform,
Briefcase will use the default stub binary for the platform.

``stub_binary_revision``
~~~~~~~~~~~~~~~~~~~~~~~~

The specific revision of the stub binary that should be used. By default, Briefcase will
use the stub binary revision nominated by the application template. If you specify a
stub binary revision, that will override the revision nominated by the application
template.

If you specify an explicit stub binary (using the ``stub_binary`` setting), this
argument is ignored.

``support_package``
~~~~~~~~~~~~~~~~~~~

Expand Down
29 changes: 29 additions & 0 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class BaseCommand(ABC):
cmd_line = "briefcase {command} {platform} {output_format}"
supported_host_os = {"Darwin", "Linux", "Windows"}
supported_host_os_reason = f"This command is not supported on {platform.system()}."

# defined by platform-specific subclasses
command: str
description: str
Expand Down Expand Up @@ -400,6 +401,20 @@ def binary_executable_path(self, app) -> Path:
"""
return self.binary_path(app)

def unbuilt_executable_path(self, app) -> Path:
"""The path to the unbuilt form of the binary object for the app.

The pre-built stub binary may need to undergo some manipulation before it can be
used; to mark that this manipulation is required, the "unbuilt" binary has a
"raw" name that doesn't involve any app details. The build step moves the binary
to the final name.

:param app: The app config
"""
return self.binary_executable_path(app).parent / (
"Stub" + self.binary_executable_path(app).suffix
)

def briefcase_toml(self, app: AppConfig) -> dict[str, ...]:
"""Load the ``briefcase.toml`` file provided by the app template.

Expand Down Expand Up @@ -444,6 +459,14 @@ def template_target_version(self, app: AppConfig) -> str | None:
except KeyError:
return None

def stub_binary_revision(self, app: AppConfig) -> str:
"""Obtain the stub binary revision that the template requires.

:param app: The config object for the app
:return: The stub binary revision required by the template.
"""
return self.path_index(app, "stub_binary_revision")

def support_path(self, app: AppConfig) -> Path:
"""Obtain the path into which the support package should be unpacked.

Expand Down Expand Up @@ -799,6 +822,12 @@ def _add_update_options(
help=f"Update support package for the app{context_label}",
)

parser.add_argument(
"--update-stub",
action="store_true",
help=f"Update stub binary for the app{context_label}",
)

parser.add_argument(
"--update-resources",
action="store_true",
Expand Down
11 changes: 11 additions & 0 deletions src/briefcase/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def _build_app(
update_requirements: bool,
update_resources: bool,
update_support: bool,
update_stub: bool,
no_update: bool,
test_mode: bool,
**options,
Expand All @@ -43,6 +44,7 @@ def _build_app(
:param update_resources: Should the application resources be updated before
building?
:param update_support: Should the application support be updated?
:param update_stub: Should the stub binary be updated?
:param no_update: Should automated updates be disabled?
:param test_mode: Is the app being build in test mode?
"""
Expand All @@ -53,6 +55,7 @@ def _build_app(
or update_requirements # An explicit update of requirements has been requested
or update_resources # An explicit update of resources has been requested
or update_support # An explicit update of app support has been requested
or update_stub # An explicit update of the stub binary has been requested
or (
test_mode and not no_update
) # Test mode, but updates have not been disabled
Expand All @@ -62,6 +65,7 @@ def _build_app(
update_requirements=update_requirements,
update_resources=update_resources,
update_support=update_support,
update_stub=update_stub,
test_mode=test_mode,
**options,
)
Expand All @@ -86,6 +90,7 @@ def __call__(
update_requirements: bool = False,
update_resources: bool = False,
update_support: bool = False,
update_stub: bool = False,
no_update: bool = False,
test_mode: bool = False,
**options,
Expand All @@ -109,6 +114,10 @@ def __call__(
raise BriefcaseCommandError(
"Cannot specify both --update-support and --no-update"
)
if update_stub:
raise BriefcaseCommandError(
"Cannot specify both --update-stub and --no-update"
)

# Confirm host compatibility, that all required tools are available,
# and that the app configuration is finalized.
Expand All @@ -121,6 +130,7 @@ def __call__(
update_requirements=update_requirements,
update_resources=update_resources,
update_support=update_support,
update_stub=update_stub,
no_update=no_update,
test_mode=test_mode,
**options,
Expand All @@ -134,6 +144,7 @@ def __call__(
update_requirements=update_requirements,
update_resources=update_resources,
update_support=update_support,
update_stub=update_stub,
no_update=no_update,
test_mode=test_mode,
**full_options(state, options),
Expand Down
129 changes: 129 additions & 0 deletions src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
InvalidSupportPackage,
MissingAppSources,
MissingNetworkResourceError,
MissingStubBinary,
MissingSupportPackage,
RequirementsInstallError,
UnsupportedPlatform,
Expand Down Expand Up @@ -100,6 +101,18 @@ def support_package_url(self, support_revision):
+ self.support_package_filename(support_revision)
)

def stub_binary_filename(self, support_revision, is_console_app):
"""The filename for the stub binary."""
stub_type = "Console" if is_console_app else "GUI"
return f"{stub_type}-Stub-{self.python_version_tag}-b{support_revision}.zip"

def stub_binary_url(self, support_revision, is_console_app):
"""The URL of the stub binary to use for apps of this type."""
return (
f"https://briefcase-support.s3.amazonaws.com/python/{self.python_version_tag}/{self.platform}/"
+ self.stub_binary_filename(support_revision, is_console_app)
)

def icon_targets(self, app: AppConfig):
"""Obtain the dictionary of icon targets that the template requires.

Expand Down Expand Up @@ -382,6 +395,111 @@ def _download_support_package(self, app: AppConfig):
is_32bit=self.tools.is_32bit_python,
) from e

def cleanup_stub_binary(self, app: AppConfig):
"""Clean up an existing application support package.

:param app: The config object for the app
"""
with self.input.wait_bar("Removing existing stub binary..."):
binary_executable_path = self.binary_executable_path(app)
if binary_executable_path.exists():
binary_executable_path.unlink()

unbuilt_executable_path = self.unbuilt_executable_path(app)
if unbuilt_executable_path.exists():
unbuilt_executable_path.unlink()

def install_stub_binary(self, app: AppConfig):
"""Install the application stub binary into the "unbuilt" location.

:param app: The config object for the app
"""
unbuilt_executable_path = self.unbuilt_executable_path(app)
stub_binary_path = self._download_stub_binary(app)

with self.input.wait_bar("Installing stub binary..."):
# Ensure the folder for the stub binary exists
unbuilt_executable_path.parent.mkdir(exist_ok=True, parents=True)
# Install the stub binary into the unbuilt location. Allow for both raw
# and compressed artefacts.
if stub_binary_path.suffix in {".zip", ".tar.gz", ".tgz"}:
self.tools.shutil.unpack_archive(
stub_binary_path,
extract_dir=unbuilt_executable_path.parent,
)
else:
self.tools.shutil.copyfile(stub_binary_path, unbuilt_executable_path)
# Ensure the binary is executable
self.tools.os.chmod(unbuilt_executable_path, 0o755)

def _download_stub_binary(self, app: AppConfig):
try:
# Work out if the app defines a custom override for
# the support package URL.
try:
stub_binary_url = app.stub_binary
custom_stub_binary = True
self.logger.info(f"Using custom stub binary {stub_binary_url}")
try:
# If the app has a custom stub binary *and* a support revision,
# that's an error.
app.stub_binary_revision
self.logger.warning(
"App specifies both a stub binary and a stub binary revision; "
"stub binary revision will be ignored."
)
except AttributeError:
pass
except AttributeError:
# If the app specifies a support revision, use it; otherwise, use the
# support revision named by the template. This value *must* exist, as
# stub binary handling won't be triggered at all unless it is present.
try:
stub_binary_revision = app.stub_binary_revision
except AttributeError:
stub_binary_revision = self.stub_binary_revision(app)

stub_binary_url = self.stub_binary_url(
stub_binary_revision, app.console_app
)
custom_stub_binary = False
self.logger.info(f"Using stub binary {stub_binary_url}")

if stub_binary_url.startswith(("https://", "http://")):
if custom_stub_binary:
# If the support package is custom, cache it using a hash of
# the download URL. This is needed to differentiate to support
# packages with the same filename, served at different URLs.
# (or a custom package that collides with an official package name)
download_path = (
self.data_path
/ "stub"
/ hashlib.sha256(stub_binary_url.encode("utf-8")).hexdigest()
)
else:
download_path = self.data_path / "stub"

# Download the stub binary, caching the result
# in the user's briefcase stub cache directory.
return self.tools.download.file(
url=stub_binary_url,
download_path=download_path,
role="stub binary",
)
else:
return Path(stub_binary_url)
except MissingNetworkResourceError as e:
# If there is a custom support package, report the missing resource as-is.
if custom_stub_binary:
raise
else:
raise MissingStubBinary(
python_version_tag=self.python_version_tag,
platform=self.platform,
host_arch=self.tools.host_arch,
is_32bit=self.tools.is_32bit_python,
) from e

def _write_requirements_file(
self,
app: AppConfig,
Expand Down Expand Up @@ -770,6 +888,17 @@ def create_app(self, app: AppConfig, test_mode: bool = False, **options):
self.logger.info("Installing support package...", prefix=app.app_name)
self.install_app_support_package(app=app)

try:
# If the platform uses a stub binary, the template will define a binary
# revision. If this template configuration item doesn't exist, no stub
# binary is required.
self.stub_binary_revision(app)
except KeyError:
pass
else:
self.logger.info("Installing stub binary...", prefix=app.app_name)
self.install_stub_binary(app=app)

# Verify the app after the app template and support package
# are in place since the app tools may be dependent on them.
self.verify_app(app)
Expand Down
3 changes: 3 additions & 0 deletions src/briefcase/commands/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ def __call__(
update_requirements: bool = False,
update_resources: bool = False,
update_support: bool = False,
update_stub: bool = False,
no_update: bool = False,
test_mode: bool = False,
passthrough: list[str] | None = None,
Expand Down Expand Up @@ -299,6 +300,7 @@ def __call__(
or update_requirements # An explicit update of requirements has been requested
or update_resources # An explicit update of resources has been requested
or update_support # An explicit update of support files has been requested
or update_stub # An explicit update of the stub binary has been requested
or (not exec_file.exists()) # Executable binary doesn't exist yet
or (
test_mode and not no_update
Expand All @@ -310,6 +312,7 @@ def __call__(
update_requirements=update_requirements,
update_resources=update_resources,
update_support=update_support,
update_stub=update_stub,
no_update=no_update,
test_mode=test_mode,
**options,
Expand Down
Loading