Skip to content

Commit

Permalink
feat(remote-build): new command logic (#4395)
Browse files Browse the repository at this point in the history
- Migrate the command logic used to orchestrate a remote build.
    - Some of this code was moved to a new class, RemoteBuild
- Store a copy of the project directory in the local cache
- Clean up the cache after a build completes
- Typing and linting updates

Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com>
  • Loading branch information
mr-cal committed Oct 6, 2023
1 parent 5d19d28 commit 2e06475
Show file tree
Hide file tree
Showing 16 changed files with 1,095 additions and 162 deletions.
10 changes: 1 addition & 9 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ ignore_missing_imports = true
follow_imports = "silent"
exclude = [
"build",
# launchpadlib is not typed
"snapcraft/remote/launchpad.py",
"snapcraft_legacy",
"tests/spread",
"tests/legacy",
Expand All @@ -67,13 +65,7 @@ plugins = [

[tool.pyright]
include = ["snapcraft", "tests"]
exclude = [
"build",
# launchpadlib is not typed
"snapcraft/remote/launchpad.py",
"tests/legacy",
"tests/spread",
]
exclude = ["build", "tests/legacy", "tests/spread"]
pythonVersion = "3.10"

[tool.pytest.ini_options]
Expand Down
117 changes: 98 additions & 19 deletions snapcraft/commands/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import textwrap
from enum import Enum
from pathlib import Path
from typing import Optional
from typing import List, Optional

from craft_cli import BaseCommand, emit
from craft_cli.helptexts import HIDDEN
Expand All @@ -30,9 +30,8 @@
from snapcraft.errors import MaintenanceBase, SnapcraftError
from snapcraft.legacy_cli import run_legacy
from snapcraft.parts import yaml_utils
from snapcraft.remote import get_build_id, is_repo
from snapcraft.utils import confirm_with_user, humanize_list
from snapcraft_legacy.internal.remote_build.errors import AcceptPublicUploadError
from snapcraft.remote import AcceptPublicUploadError, RemoteBuilder, is_repo
from snapcraft.utils import confirm_with_user, get_host_architecture, humanize_list

_CONFIRMATION_PROMPT = (
"All data sent to remote builders will be publicly available. "
Expand Down Expand Up @@ -216,23 +215,53 @@ def _get_project_name(self) -> str:

def _run_new_remote_build(self) -> None:
"""Run new remote-build code."""
# the build-id will be passed to the new remote-build code as part of #4323
if self._parsed_args.build_id:
build_id = self._parsed_args.build_id
emit.debug(f"Using build ID {build_id!r} passed as a parameter.")
else:
build_id = get_build_id(
app_name="snapcraft",
project_name=self._get_project_name(),
project_path=Path(),
)
emit.debug(f"Using computed build ID {build_id!r}.")
emit.progress("Setting up launchpad environment.")
remote_builder = RemoteBuilder(
app_name="snapcraft",
build_id=self._parsed_args.build_id,
project_name=self._get_project_name(),
architectures=self._determine_architectures(),
project_dir=Path(),
)

if self._parsed_args.status:
remote_builder.print_status()
return

emit.progress("Looking for existing builds.")
has_outstanding_build = remote_builder.has_outstanding_build()
if self._parsed_args.recover and not has_outstanding_build:
emit.message("No build found.")
return

if has_outstanding_build:
emit.message("Found previously started build.")
remote_builder.print_status()

# TODO: use new remote-build code (#4323)
emit.debug(
"Running fallback remote-build because new remote-build is not available."
# If recovery specified, monitor build and exit.
if self._parsed_args.recover or confirm_with_user(
"Do you wish to recover this build?", default=True
):
emit.progress("Building")
remote_builder.monitor_build()
emit.progress("Cleaning")
remote_builder.clean_build()
return

# Otherwise clean running build before we start a new one.
emit.progress("Cleaning previously existing build.")
remote_builder.clean_build()

emit.message(
"If interrupted, resume with: 'snapcraft remote-build --recover "
f"--build-id {remote_builder.build_id}'."
)
run_legacy()
emit.progress("Starting build")
remote_builder.start_build()
emit.progress("Building")
remote_builder.monitor_build()
emit.progress("Cleaning")
remote_builder.clean_build()

def _get_build_strategy(self) -> Optional[_Strategies]:
"""Get the build strategy from the envvar `SNAPCRAFT_REMOTE_BUILD_STRATEGY`.
Expand Down Expand Up @@ -285,6 +314,56 @@ def _get_effective_base(self) -> str:

return base

def _get_project_build_on_architectures(self) -> List[str]:
"""Get a list of build-on architectures from the project's snapcraft.yaml.
:returns: A list of architectures.
"""
with open(self._snapcraft_yaml, encoding="utf-8") as file:
data = yaml_utils.safe_load(file)

project_archs = data.get("architectures")

archs = []
if project_archs:
for item in project_archs:
if "build-on" in item:
new_arch = item["build-on"]
if isinstance(new_arch, list):
archs.extend(new_arch)
else:
archs.append(new_arch)

return archs

def _determine_architectures(self) -> List[str]:
"""Determine architectures to build for.
The build architectures can be set via the `--build-on` parameter or determined
from the build-on architectures listed in the project's snapcraft.yaml.
:returns: A list of architectures.
:raises SnapcraftError: If `--build-on` was provided and architectures are
defined in the project's snapcraft.yaml.
"""
project_architectures = self._get_project_build_on_architectures()
if project_architectures and self._parsed_args.build_for:
raise SnapcraftError(
"Cannot use `--build-on` because architectures are already defined in "
"snapcraft.yaml."
)

if project_architectures:
archs = project_architectures
elif self._parsed_args.build_for:
archs = self._parsed_args.build_for
else:
# default to typical snapcraft behavior (build for host)
archs = [get_host_architecture()]

return archs


def _get_esm_warning_for_base(base: str) -> str:
"""Return a warning appropriate for the base under ESM."""
Expand Down
2 changes: 1 addition & 1 deletion snapcraft/legacy_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import snapcraft_legacy
from snapcraft_legacy.cli import legacy

_LIB_NAMES = ("craft_parts", "craft_providers", "craft_store")
_LIB_NAMES = ("craft_parts", "craft_providers", "craft_store", "snapcraft.remote")
_ORIGINAL_LIB_NAME_LOG_LEVEL: Dict[str, int] = {}


Expand Down
10 changes: 9 additions & 1 deletion snapcraft/remote/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,33 @@
"""Remote-build and related utilities."""

from .errors import (
AcceptPublicUploadError,
GitError,
LaunchpadHttpsError,
RemoteBuildError,
RemoteBuildTimeoutError,
UnsupportedArchitectureError,
)
from .git import GitRepo, is_repo
from .launchpad import LaunchpadClient
from .utils import get_build_id, rmtree
from .remote_builder import RemoteBuilder
from .utils import get_build_id, humanize_list, rmtree, validate_architectures
from .worktree import WorkTree

__all__ = [
"get_build_id",
"humanize_list",
"is_repo",
"rmtree",
"validate_architectures",
"AcceptPublicUploadError",
"GitError",
"GitRepo",
"LaunchpadClient",
"LaunchpadHttpsError",
"RemoteBuilder",
"RemoteBuildError",
"RemoteBuildTimeoutError",
"UnsupportedArchitectureError",
"WorkTree",
]
31 changes: 30 additions & 1 deletion snapcraft/remote/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"""Remote build errors."""

from dataclasses import dataclass
from typing import Optional
from typing import List, Optional


@dataclass(repr=True)
Expand Down Expand Up @@ -69,3 +69,32 @@ def __init__(self) -> None:
details = "Verify connectivity to https://api.launchpad.net and retry build."

super().__init__(brief=brief, details=details)


class UnsupportedArchitectureError(RemoteBuildError):
"""Unsupported architecture error."""

def __init__(self, architectures: List[str]) -> None:
brief = "Architecture not supported by the remote builder."
details = (
"The following architectures are not supported by the remote builder: "
f"{architectures}.\nPlease remove them from the "
"architecture list and try again."
)

super().__init__(brief=brief, details=details)


class AcceptPublicUploadError(RemoteBuildError):
"""Accept public upload error."""

def __init__(self) -> None:
brief = "Cannot upload data to build servers."
details = (
"Remote build needs explicit acknowledgement that data sent to build "
"servers is public.\n"
"In non-interactive runs, please use the option "
"`--launchpad-accept-public-upload`."
)

super().__init__(brief=brief, details=details)
2 changes: 1 addition & 1 deletion snapcraft/remote/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ def _init_repo(self) -> None:
:raises GitError: if the repo cannot be initialized
"""
logger.debug("Initializing git repository in {str(self.path)!r}")
logger.debug("Initializing git repository in %r", str(self.path))

try:
pygit2.init_repository(self.path)
Expand Down

0 comments on commit 2e06475

Please sign in to comment.