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 Apr 4, 2024
1 parent a147aaf commit 3af40d1
Show file tree
Hide file tree
Showing 19 changed files with 368 additions and 243 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.tracking_save()
logger.save_log_to_file(command)

return result
Expand Down
140 changes: 121 additions & 19 deletions src/briefcase/commands/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@
import textwrap
from abc import ABC, abstractmethod
from argparse import RawDescriptionHelpFormatter
from collections.abc import Iterable
from pathlib import Path
from typing import Any

import tomli_w
from cookiecutter import exceptions as cookiecutter_exceptions
from cookiecutter.repository import is_repo_url
from platformdirs import PlatformDirs
Expand Down Expand Up @@ -153,11 +155,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,
tracking: dict[AppConfig, dict[str, ...]] = None,
):
"""Base for all Commands.
Expand All @@ -171,10 +174,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 +194,9 @@ def __init__(

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

@property
def logger(self):
Expand Down Expand Up @@ -319,6 +322,7 @@ def _command_factory(self, command_name: str):
console=self.input,
tools=self.tools,
is_clone=True,
tracking=self._tracking,
)
command.clone_options(self)
return command
Expand Down Expand Up @@ -389,6 +393,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 +406,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 @@ -487,11 +494,11 @@ def app_module_path(self, app: AppConfig) -> Path:
"""Find the path for the application module for an app.
:param app: The config object for the app
:returns: The Path to the dist-info folder.
:returns: The Path to the app module
"""
app_home = [
path.split("/")
for path in app.sources
for path in app.sources()
if path.rsplit("/", 1)[-1] == app.module_name
]

Expand All @@ -500,14 +507,114 @@ def app_module_path(self, app: AppConfig) -> Path:
f"Unable to find code for application {app.app_name!r}"
)
elif len(app_home) == 1:
path = Path(str(self.base_path), *app_home[0])
path = Path(self.base_path, *app_home[0])
else:
raise BriefcaseCommandError(
f"Multiple paths in sources found for application {app.app_name!r}"
)

return path

def dist_info_path(self, app: AppConfig) -> Path:
"""Path to dist-info for the app in the output format build."""
return self.app_path(app) / f"{app.module_name}-{app.version}.dist-info"

def tracking_path(self, app: AppConfig) -> Path:
return self.bundle_path(app) / f".tracking.{app.module_name}.toml"

def tracking(self, app: AppConfig) -> dict[str, ...]:
"""Load the build tracking information for the app.
:param app: The config object for the app
:return: build tracking cache
"""
try:
return self._tracking[app]
except KeyError:
try:
toml = self.tracking_path(app).read_text(encoding="utf-8")
except (OSError, AttributeError):
toml = ""

self._tracking[app] = tomllib.loads(toml)
return self._tracking[app]

def tracking_save(self) -> None:
"""Update the persistent build tracking information."""
for app in self.apps.values():
try:
content = tomli_w.dumps(self._tracking[app])
except KeyError:
pass
else:
try:
with self.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 tracking_set(self, app: AppConfig, key: str, value: object) -> None:
"""Update a build tracking key/value pair."""
self.tracking(app)[key] = value

def tracking_add_requirements(
self,
app: AppConfig,
requires: Iterable[str],
) -> None:
"""Update the building tracking for the app's requirements."""
self.tracking_set(app, key="requires", value=requires)

def tracking_is_requirements_updated(
self,
app: AppConfig,
requires: Iterable[str],
) -> bool:
"""Has the app's requirements changed since last run?"""
try:
return self.tracking(app)["requires"] != requires
except KeyError:
return True

def tracking_source_modified_time(
self,
sources: Iterable[str | os.PathLike],
) -> 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 sources
for dir_path, _, files in self.tools.os.walk(Path.cwd() / src)
)

def tracking_add_source_modified_time(
self,
app: AppConfig,
sources: Iterable[str | os.PathLike],
) -> None:
"""Update build tracking for the app's source code's last modified datetime."""
self.tracking_set(
app,
key="src_last_modified",
value=self.tracking_source_modified_time(sources),
)

def tracking_is_source_modified(
self,
app: AppConfig,
sources: Iterable[str | os.PathLike],
) -> bool:
"""Has the app's source been modified since last run?"""
try:
tracked_time = self.tracking(app)["src_last_modified"]
return tracked_time < self.tracking_source_modified_time(sources)
except KeyError:
return True

@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 +862,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
Loading

0 comments on commit 3af40d1

Please sign in to comment.