Skip to content

Commit

Permalink
fix(remotebuild): parse '--build-for' and '--platform'
Browse files Browse the repository at this point in the history
* '--build-for' and '--platform' can only be used when 'platforms' and
  'architectures' are not defined the project metadata
* '--platform' cannot be used for core22 snaps

Signed-off-by: Callahan Kovacs <callahan.kovacs@canonical.com>
  • Loading branch information
mr-cal committed Aug 21, 2024
1 parent 35c7d97 commit 2666a99
Show file tree
Hide file tree
Showing 7 changed files with 283 additions and 152 deletions.
1 change: 1 addition & 0 deletions snapcraft/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
source_ignore_patterns=["*.snap"],
project_variables=["version", "grade"],
mandatory_adoptable_fields=["version", "summary", "description"],
docs_url="https://canonical-snapcraft.readthedocs-hosted.com/en/{version}",
)


Expand Down
113 changes: 81 additions & 32 deletions snapcraft/commands/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@

import lazr.restfulclient.errors
from craft_application import errors
from craft_application.application import filter_plan
from craft_application.commands import ExtensibleCommand
from craft_application.errors import RemoteBuildError
from craft_application.launchpad.models import Build, BuildState
from craft_application.remote.utils import get_build_id
from craft_application.util import humanize_list
from craft_cli import emit
from overrides import overrides

Expand Down Expand Up @@ -106,13 +106,19 @@ def _fill_parser(self, parser: argparse.ArgumentParser) -> None:
metavar="name",
default=os.getenv("CRAFT_PLATFORM"),
help="Set platform to build for",
# '--platform' needs to be handled differently since remote-build can
# build for an architecture that is not in the project metadata
dest="remote_build_platform",
)
group.add_argument(
"--build-for",
type=str,
metavar="arch",
default=os.getenv("CRAFT_BUILD_FOR"),
help="Set architecture to build for",
# '--build-for' needs to be handled differently since remote-build can
# build for architecture that is not in the project metadata
dest="remote_build_build_for",
)
parser.add_argument(
"--project", help="upload to the specified Launchpad project"
Expand Down Expand Up @@ -143,8 +149,56 @@ def _validate(self, parsed_args: argparse.Namespace) -> None:
"In non-interactive runs, please use the option "
"`--launchpad-accept-public-upload`."
),
doc_slug="/explanation/remote-build.html",
reportable=False,
retcode=77,
retcode=os.EX_NOPERM,
)

build_for = parsed_args.remote_build_build_for or None
platform = parsed_args.remote_build_platform or None
parameter = "--build-for" if build_for else "--platform" if platform else None
keyword = (
"architectures"
if self.project._architectures_in_yaml
else "platforms" if self.project.platforms else None
)

if keyword and parameter:
raise errors.RemoteBuildError(
f"{parameter!r} cannot be used when {keyword!r} is in the snapcraft.yaml.",
resolution=f"Remove {parameter!r} from the command line or remove {keyword!r} in the snapcraft.yaml.",
doc_slug="/explanation/remote-build.html",
retcode=os.EX_CONFIG,
)

if platform:
if self.project.get_effective_base() == "core22":
raise errors.RemoteBuildError(
"'--platform' cannot be used for core22 snaps.",
resolution="Use '--build-for' instead.",
doc_slug="/explanation/remote-build.html",
retcode=os.EX_CONFIG,
)
if platform not in SUPPORTED_ARCHS:
raise errors.RemoteBuildError(
f"Unsupported platform {parsed_args.remote_build_platform!r}.",
resolution=(
"Use a supported debian architecture. Supported "
f"architectures are: {humanize_list(SUPPORTED_ARCHS, 'and')}"
),
doc_slug="/explanation/remote-build.html",
retcode=os.EX_CONFIG,
)

if build_for and build_for not in SUPPORTED_ARCHS:
raise errors.RemoteBuildError(
f"Unsupported build-for architecture {parsed_args.remote_build_build_for!r}.",
resolution=(
"Use a supported debian architecture. Supported "
f"architectures are: {humanize_list(SUPPORTED_ARCHS, 'and')}"
),
doc_slug="/explanation/remote-build.html",
retcode=os.EX_CONFIG,
)

def _run( # noqa: PLR0915 [too-many-statements]
Expand All @@ -159,14 +213,13 @@ def _run( # noqa: PLR0915 [too-many-statements]
if parsed_args.project:
self._services.remote_build.set_project(parsed_args.project)

self._validate(parsed_args)
emit.progress(
"remote-build is experimental and is subject to change. Use with caution.",
permanent=True,
)

builder = self._services.remote_build
project = cast(models.Project, self._services.project)
self.project = cast(models.Project, self._services.project)
config = cast(dict[str, Any], self.config)
project_dir = (
Path(config.get("global_args", {}).get("project_dir") or ".")
Expand All @@ -175,41 +228,22 @@ def _run( # noqa: PLR0915 [too-many-statements]
)

emit.trace(f"Project directory: {project_dir}")
self._validate(parsed_args)

possible_build_plan = filter_plan(
self._app.BuildPlannerClass.unmarshal(project.marshal()).get_build_plan(),
platform=parsed_args.platform,
build_for=parsed_args.build_for,
host_arch=None,
)

architectures: list[str] | None = list(
{build_info.build_for for build_info in possible_build_plan}
)

if parsed_args.platform and not architectures:
emit.progress(
f"platform '{parsed_args.platform}' is not present in the build plan.",
permanent=True,
)
return 78 # Configuration error

if parsed_args.build_for and not architectures:
if parsed_args.build_for not in SUPPORTED_ARCHS:
raise errors.RemoteBuildError(
f"build-for '{parsed_args.build_for}' is not supported.", retcode=78
)
# Allow the user to build for a single architecture if snapcraft.yaml
# doesn't define architectures.
architectures = [parsed_args.build_for]
if parsed_args.remote_build_build_for:
architectures = [parsed_args.remote_build_build_for]
elif parsed_args.remote_build_platform:
architectures = [parsed_args.remote_build_platform]
else:
architectures = self._get_project_build_fors()

emit.debug(f"Architectures to build: {architectures}")
emit.debug(f"Architectures to build for: {architectures}")

if parsed_args.launchpad_timeout:
emit.debug(f"Setting timeout to {parsed_args.launchpad_timeout} seconds")
builder.set_timeout(parsed_args.launchpad_timeout)

build_id = get_build_id(self._app.name, project.name, project_dir)
build_id = get_build_id(self._app.name, self.project.name, project_dir)
if parsed_args.recover:
emit.progress(f"Recovering build {build_id}")
builds = builder.resume_builds(build_id)
Expand Down Expand Up @@ -325,3 +359,18 @@ def _monitor_and_complete( # noqa: PLR0912, PLR0915
f"Artifacts: {', '.join(artifact_names)}"
)
return return_code

def _get_project_build_fors(self) -> list[str]:
"""Get a unique list of build-for architectures from the project.
:returns: A list of architectures.
"""
build_plan = self._app.BuildPlannerClass.unmarshal(
self.project.marshal()
).get_build_plan()
emit.debug(f"Build plan: {build_plan}")

build_fors = set({build_info.build_for for build_info in build_plan})
emit.debug(f"Parsed build-for architectures from build plan: {build_fors}")

return list(build_fors)
11 changes: 9 additions & 2 deletions snapcraft/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,7 @@ class Project(models.Project):
) = None
grade: Literal["stable", "devel"] | None = None
architectures: list[str | Architecture] | None = None
_architectures_in_yaml: bool | None = None
platforms: dict[str, Platform] | None = None # type: ignore[assignment,reportIncompatibleVariableOverride]
assumes: UniqueList[str] = pydantic.Field(default_factory=list)
hooks: dict[str, Hook] | None = None
Expand Down Expand Up @@ -759,8 +760,14 @@ def _validate_platforms_and_architectures(self) -> Self:
f"'platforms' keyword is not supported for base {base!r}. "
"Use 'architectures' keyword instead."
)
# set default value
if not self.architectures:

# this is a one-shot - the value should not change when re-validating
if self.architectures and self._architectures_in_yaml is None:
# record if architectures are defined in the yaml for remote-build (#4881)
self._architectures_in_yaml = True
elif not self.architectures:
self._architectures_in_yaml = False
# set default value
self.architectures = [
Architecture(
build_on=[get_host_architecture()],
Expand Down
5 changes: 2 additions & 3 deletions tests/spread/core24/remote-build/task.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ systems:
environment:
LAUNCHPAD_TOKEN: "$(HOST: echo ${LAUNCHPAD_TOKEN})"
SNAP: no-platforms
# launchpad can't parse `platforms` (https://github.com/canonical/snapcraft/issues/4858)
#SNAP/all: all
#SNAP/platforms: platforms
SNAP/all: all
SNAP/platforms: platforms
SNAP/no_platforms: no-platforms
CREDENTIALS_FILE: "$HOME/.local/share/snapcraft/launchpad-credentials"
CREDENTIALS_FILE/new_credentials: "$HOME/.local/share/snapcraft/launchpad-credentials"
Expand Down
Loading

0 comments on commit 2666a99

Please sign in to comment.