Skip to content

Commit

Permalink
Add tooling to download stub binary as part of app creation.
Browse files Browse the repository at this point in the history
  • Loading branch information
freakboy3742 committed May 30, 2024
1 parent 96eb010 commit 59dd93f
Show file tree
Hide file tree
Showing 8 changed files with 228 additions and 7 deletions.
32 changes: 32 additions & 0 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,8 @@ 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()}."
exe_extension = ""

# defined by platform-specific subclasses
command: str
description: str
Expand Down Expand Up @@ -400,6 +402,22 @@ 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 / (
("Console" if app.console_app else "GUI")
+ "-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 +462,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 +825,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
138 changes: 138 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,120 @@ 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
try:
stub_binary_revision = app.stub_binary_revision
except AttributeError:
# No support revision specified; use the template-specified version
try:
stub_binary_revision = self.stub_binary_revision(app)
except KeyError:
# No template-specified stub binary
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,
)

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 +897,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
18 changes: 18 additions & 0 deletions src/briefcase/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def update_app(
update_requirements: bool,
update_resources: bool,
update_support: bool,
update_stub: bool,
test_mode: bool,
**options,
) -> dict | None:
Expand All @@ -29,6 +30,7 @@ def update_app(
:param update_requirements: Should requirements be updated?
:param update_resources: Should extra resources be updated?
:param update_support: Should app support be updated?
:param update_stub: Should stub binary be updated?
:param test_mode: Should the app be updated in test mode?
"""

Expand Down Expand Up @@ -56,6 +58,19 @@ def update_app(
self.cleanup_app_support_package(app=app)
self.install_app_support_package(app=app)

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

self.logger.info("Removing unneeded app content...", prefix=app.app_name)
self.cleanup_app_content(app=app)

Expand All @@ -67,6 +82,7 @@ def __call__(
update_requirements: bool = False,
update_resources: bool = False,
update_support: bool = False,
update_stub: bool = False,
test_mode: bool = False,
**options,
) -> dict | None:
Expand All @@ -80,6 +96,7 @@ def __call__(
update_requirements=update_requirements,
update_resources=update_resources,
update_support=update_support,
update_stub=update_stub,
test_mode=test_mode,
**options,
)
Expand All @@ -91,6 +108,7 @@ def __call__(
update_requirements=update_requirements,
update_resources=update_resources,
update_support=update_support,
update_stub=update_stub,
test_mode=test_mode,
**full_options(state, options),
)
Expand Down
Loading

0 comments on commit 59dd93f

Please sign in to comment.