Skip to content

Commit

Permalink
[POC] Implement persistent build tracking for more intuitive behavior
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Mar 29, 2024
1 parent a147aaf commit d7ebb95
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 55 deletions.
77 changes: 39 additions & 38 deletions src/briefcase/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,48 +15,49 @@
def main():
result = 0
command = None

printer = Printer()
console = Console(printer=printer)
logger = Log(printer=printer)
try:
Command, extra_cmdline = parse_cmdline(sys.argv[1:])
command = Command(logger=logger, console=console)
options, overrides = command.parse_options(extra=extra_cmdline)
command.parse_config(
Path.cwd() / "pyproject.toml",
overrides=overrides,
)
command(**options)
except HelpText as e:
logger.info()
logger.info(str(e))
result = e.error_code
except BriefcaseWarning as w:
# The case of something that hasn't gone right, but in an
# acceptable way.
logger.warning(str(w))
result = w.error_code
except BriefcaseTestSuiteFailure as e:
# Test suite status is logged when the test is executed.
# Set the return code, but don't log anything else.
result = e.error_code
except BriefcaseError as e:
logger.error()
logger.error(str(e))
result = e.error_code
logger.capture_stacktrace()
except Exception:
logger.capture_stacktrace()
raise
except KeyboardInterrupt:
logger.warning()
logger.warning("Aborted by user.")
logger.warning()
result = -42
if logger.save_log:

with suppress(KeyboardInterrupt):
try:
Command, extra_cmdline = parse_cmdline(sys.argv[1:])
command = Command(logger=logger, console=console)
options, overrides = command.parse_options(extra=extra_cmdline)
command.parse_config(Path.cwd() / "pyproject.toml", overrides=overrides)
command(**options)
except HelpText as e:
logger.info()
logger.info(str(e))
result = e.error_code
except BriefcaseWarning as w:
# The case of something that hasn't gone right, but in an
# acceptable way.
logger.warning(str(w))
result = w.error_code
except BriefcaseTestSuiteFailure as e:
# Test suite status is logged when the test is executed.
# Set the return code, but don't log anything else.
result = e.error_code
except BriefcaseError as e:
logger.error()
logger.error(str(e))
result = e.error_code
logger.capture_stacktrace()
except Exception:
logger.capture_stacktrace()
finally:
with suppress(KeyboardInterrupt):
raise
except KeyboardInterrupt:
logger.warning()
logger.warning("Aborted by user.")
logger.warning()
result = -42
if logger.save_log:
logger.capture_stacktrace()
finally:
if command is not None:
command.build_tracking_save()
logger.save_log_to_file(command)

return result
Expand Down
104 changes: 88 additions & 16 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import textwrap
from abc import ABC, abstractmethod
from argparse import RawDescriptionHelpFormatter
from json import dumps, loads
from pathlib import Path
from typing import Any

Expand Down Expand Up @@ -153,11 +154,12 @@ def __init__(
self,
logger: Log,
console: Console,
tools: ToolCache = None,
apps: dict = None,
base_path: Path = None,
data_path: Path = None,
tools: ToolCache | None = None,
apps: dict[str, AppConfig] | None = None,
base_path: Path | None = None,
data_path: Path | None = None,
is_clone: bool = False,
build_tracking: dict[AppConfig, dict[str, ...]] = None,
):
"""Base for all Commands.
Expand All @@ -171,10 +173,7 @@ def __init__(
Command; for instance, RunCommand can invoke UpdateCommand and/or
BuildCommand.
"""
if base_path is None:
self.base_path = Path.cwd()
else:
self.base_path = base_path
self.base_path = Path.cwd() if base_path is None else base_path
self.data_path = self.validate_data_path(data_path)
self.apps = {} if apps is None else apps
self.is_clone = is_clone
Expand All @@ -194,6 +193,9 @@ def __init__(

self.global_config = None
self._briefcase_toml: dict[AppConfig, dict[str, ...]] = {}
self._build_tracking: dict[AppConfig, dict[str, ...]] = (
{} if build_tracking is None else build_tracking
)

@property
def logger(self):
Expand Down Expand Up @@ -319,6 +321,7 @@ def _command_factory(self, command_name: str):
console=self.input,
tools=self.tools,
is_clone=True,
build_tracking=self._build_tracking,
)
command.clone_options(self)
return command
Expand Down Expand Up @@ -389,6 +392,9 @@ def binary_path(self, app) -> Path:
:param app: The app config
"""

def briefcase_toml_path(self, app: AppConfig) -> Path:
return self.bundle_path(app) / "briefcase.toml"

def briefcase_toml(self, app: AppConfig) -> dict[str, ...]:
"""Load the ``briefcase.toml`` file provided by the app template.
Expand All @@ -399,11 +405,11 @@ def briefcase_toml(self, app: AppConfig) -> dict[str, ...]:
return self._briefcase_toml[app]
except KeyError:
try:
with (self.bundle_path(app) / "briefcase.toml").open("rb") as f:
self._briefcase_toml[app] = tomllib.load(f)
toml = self.briefcase_toml_path(app).read_text(encoding="utf-8")
except OSError as e:
raise MissingAppMetadata(self.bundle_path(app)) from e
else:
self._briefcase_toml[app] = tomllib.loads(toml)
return self._briefcase_toml[app]

def path_index(self, app: AppConfig, path_name: str) -> str | dict | list:
Expand Down Expand Up @@ -508,6 +514,77 @@ def app_module_path(self, app: AppConfig) -> Path:

return path

def build_tracking_path(self, app: AppConfig) -> Path:
return self.bundle_path(app) / ".build_tracking.json"

def build_tracking(self, app: AppConfig) -> dict[str, ...]:
"""Load the build tracking information for the app.
:param app: The config object for the app
:return: ConfigParser for build tracking
"""
try:
return self._build_tracking[app]
except KeyError:
try:
config = self.build_tracking_path(app).read_text(encoding="utf-8")
except OSError:
config = "{}"

self._build_tracking[app] = loads(config)
return self._build_tracking[app]

def build_tracking_save(self) -> None:
"""Update the persistent build tracking information."""
for app in self.apps.values():
try:
content = dumps(self._build_tracking[app], indent=4)
except KeyError:
pass
else:
try:
with self.build_tracking_path(app).open("w", encoding="utf-8") as f:
f.write(content)
except OSError as e:
self.logger.warning(
f"Failed to update build tracking for {app.app_name!r}: "
f"{type(e).__name__}: {e}"
)

def build_tracking_set(self, app: AppConfig, key: str, value: object) -> None:
"""Update a build tracking key/value pair."""
self.build_tracking(app)[key] = value

def build_tracking_add_requirements(self, app: AppConfig) -> None:
"""Update the building tracking for the app's requirements."""
self.build_tracking_set(app, key="requires", value=app.requires)

def build_tracking_is_requirements_updated(self, app: AppConfig) -> bool:
"""Have the app's requirements changed since last run?"""
return self.build_tracking(app).get("requires") != app.requires

def build_tracking_source_modified_time(self, app: AppConfig) -> float:
"""The epoch datetime of the most recently modified file in the app's
sources."""
return max(
max((Path(dir_path) / f).stat().st_mtime for f in files)
for src in app.sources
for dir_path, _, files in self.tools.os.walk(Path.cwd() / src)
)

def build_tracking_add_source_modified_time(self, app: AppConfig) -> None:
"""Update build tracking for the app's source code's last modified datetime."""
self.build_tracking_set(
app,
key="src_last_modified",
value=self.build_tracking_source_modified_time(app),
)

def build_tracking_is_source_modified(self, app: AppConfig) -> bool:
"""Has the app's source been modified since last run?"""
curr_modified_time = self.build_tracking_source_modified_time(app)
return self.build_tracking(app).get("src_last_modified") < curr_modified_time

@property
def briefcase_required_python_version(self):
"""The major.minor of the minimum Python version required by Briefcase itself.
Expand Down Expand Up @@ -755,12 +832,7 @@ def add_default_options(self, parser):
help="Save a detailed log to file. By default, this log file is only created for critical errors",
)

def _add_update_options(
self,
parser,
context_label="",
update=True,
):
def _add_update_options(self, parser, context_label="", update=True):
"""Internal utility method for adding common update options.
:param parser: The parser to which options should be added.
Expand Down
6 changes: 6 additions & 0 deletions src/briefcase/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ def _build_app(
:param no_update: Should automated updates be disabled?
:param test_mode: Is the app being build in test mode?
"""
if not update_requirements:
update_requirements = self.build_tracking_is_requirements_updated(app)

if not update:
update = self.build_tracking_is_source_modified(app)

if not self.bundle_path(app).exists():
state = self.create_command(app, test_mode=test_mode, **options)
elif (
Expand Down
5 changes: 4 additions & 1 deletion src/briefcase/commands/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ def _install_app_requirements(
:param requires: The list of requirements to install
:param app_packages_path: The full path of the app_packages folder into which
requirements should be installed.
:param progress_message: The waitbar progress message to display to the user.
:param progress_message: The Wait Bar progress message to display to the user.
:param pip_kwargs: Any additional keyword arguments to pass to the subprocess
when invoking pip.
"""
Expand Down Expand Up @@ -602,6 +602,7 @@ def install_app_requirements(self, app: AppConfig, test_mode: bool):
"Application path index file does not define "
"`app_requirements_path` or `app_packages_path`"
) from e
self.build_tracking_add_requirements(app)

def install_app_code(self, app: AppConfig, test_mode: bool):
"""Install the application code into the bundle.
Expand Down Expand Up @@ -636,6 +637,8 @@ def install_app_code(self, app: AppConfig, test_mode: bool):
else:
self.logger.info(f"No sources defined for {app.app_name}.")

self.build_tracking_add_source_modified_time(app)

# Write the dist-info folder for the application.
write_dist_info(
app=app,
Expand Down

0 comments on commit d7ebb95

Please sign in to comment.