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

feat(remote-build): new command logic #4395

Merged
merged 12 commits into from
Oct 6, 2023
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.
sergiusens marked this conversation as resolved.
Show resolved Hide resolved
"""
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
Loading