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

meta: create manifest file #3824

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ test-pydocstyle:
.PHONY: test-pylint
test-pylint:
pylint snapcraft
pylint tests/*.py tests/unit --disable=invalid-name,missing-module-docstring,missing-function-docstring,no-self-use,duplicate-code,protected-access,unspecified-encoding,too-many-public-methods,too-many-arguments
pylint tests/*.py tests/unit --disable=invalid-name,missing-module-docstring,missing-function-docstring,no-self-use,duplicate-code,protected-access,unspecified-encoding,too-many-public-methods,too-many-arguments,too-many-lines

.PHONY: test-pyright
test-pyright:
Expand Down
2 changes: 1 addition & 1 deletion requirements-devel.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ codespell==2.1.0
coverage==6.4.1
craft-cli==0.6.0
craft-grammar==1.1.1
craft-parts==1.7.2
craft-parts==1.8.0
craft-providers==1.3.1
craft-store==2.1.1
cryptography==3.4
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ charset-normalizer==2.0.12
click==8.1.3
craft-cli==0.6.0
craft-grammar==1.1.1
craft-parts==1.7.2
craft-parts==1.8.0
craft-providers==1.3.1
craft-store==2.1.1
cryptography==3.4
Expand Down
16 changes: 15 additions & 1 deletion snapcraft/commands/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@

import abc
import argparse
import os
import textwrap

from craft_cli import BaseCommand, emit
from overrides import overrides

from snapcraft import pack
from snapcraft import pack, utils
from snapcraft.parts import lifecycle as parts_lifecycle


Expand All @@ -48,6 +49,19 @@ def fill_parser(self, parser: "argparse.ArgumentParser") -> None:
action="store_true",
help="Shell into the environment if the build fails",
)
parser.add_argument(
"--enable-manifest",
action="store_true",
default=utils.strtobool(os.getenv("SNAPCRAFT_BUILD_INFO", "n")),
help="Generate snap manifest",
)
parser.add_argument(
"--manifest-image-information",
type=str,
metavar="image-info",
default=os.getenv("SNAPCRAFT_IMAGE_INFO"),
help="Set snap manifest image-info",
)

# --enable-experimental-extensions is only available in legacy
parser.add_argument(
Expand Down
133 changes: 133 additions & 0 deletions snapcraft/meta/manifest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2022 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Define and create the manifest file."""

import json
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional, cast

from pydantic_yaml import YamlModel

from snapcraft import __version__, errors, os_release
from snapcraft.projects import Project

from .snap_yaml import process_version


class Manifest(YamlModel):
"""Manifest file for snaps."""

# Snapcraft annotations
snapcraft_version: str
snapcraft_started_at: str
snapcraft_os_release_id: str
snapcraft_os_release_version_id: str

# Project fields
name: str
version: str
summary: str
description: str
base: str
grade: str
confinement: str
apps: Optional[Dict[str, Any]]
parts: Dict[str, Any]

# TODO: add assumes, environment, hooks, slots

# Architecture
architectures: List[str]

# Image info
image_info: Dict[str, Any]

# Build environment
build_packages: List[str]
build_snaps: List[str]
primed_stage_packages: List

class Config: # pylint: disable=too-few-public-methods
"""Pydantic model configuration."""

allow_population_by_field_name = True
alias_generator = lambda s: s.replace("_", "-") # noqa: E731


def write(
project: Project,
prime_dir: Path,
*,
arch: str,
parts: Dict[str, Any],
image_information: str,
start_time: datetime,
primed_stage_packages: List[str],
):
"""Create a manifest.yaml file."""
snap_dir = prime_dir / "snap"
snap_dir.mkdir(parents=True, exist_ok=True)

osrel = os_release.OsRelease()
version = process_version(project.version)

try:
image_info = json.loads(image_information)
except json.decoder.JSONDecodeError as err:
raise errors.SnapcraftError(
f"Image information decode error at {err.lineno}:{err.colno}: "
f"{err.doc!r}: {err.msg}"
) from err

manifest = Manifest(
# Snapcraft annotations
snapcraft_version=__version__,
snapcraft_started_at=start_time.isoformat("T") + "Z",
snapcraft_os_release_id=osrel.name().lower(),
snapcraft_os_release_version_id=osrel.version_id().lower(),
# Project fields
name=project.name,
version=version,
summary=project.summary, # type: ignore
description=project.description, # type: ignore
base=cast(str, project.base),
grade=project.grade or "stable",
confinement=project.confinement,
apps=project.apps,
parts=parts,
# Architecture
architectures=[arch],
# Image info
image_info=image_info,
# Build environment
build_packages=[],
build_snaps=[],
primed_stage_packages=primed_stage_packages,
)

yaml_data = manifest.yaml(
by_alias=True,
exclude_none=True,
exclude_unset=True,
allow_unicode=True,
sort_keys=False,
width=1000,
)

manifest_yaml = snap_dir / "manifest.yaml"
manifest_yaml.write_text(yaml_data)
4 changes: 2 additions & 2 deletions snapcraft/meta/snap_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ def write(project: Project, prime_dir: Path, *, arch: str, arch_triplet: str):
assumes.add("command-chain")

environment = _populate_environment(project.environment, prime_dir, arch_triplet)
version = _process_version(project.version)
version = process_version(project.version)

snap_metadata = SnapMetadata(
name=project.name,
Expand Down Expand Up @@ -257,7 +257,7 @@ def _populate_environment(
return None


def _process_version(version: Optional[str]) -> str:
def process_version(version: Optional[str]) -> str:
"""Handle special version strings."""
if version is None:
raise ValueError("version cannot be None")
Expand Down
58 changes: 57 additions & 1 deletion snapcraft/parts/lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

"""Parts lifecycle preparation and execution."""

import copy
import os
import shutil
import subprocess
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, List, Optional

Expand All @@ -27,7 +30,7 @@
from craft_parts import ProjectInfo, StepInfo, callbacks, infos

from snapcraft import errors, extensions, pack, providers, utils
from snapcraft.meta import snap_yaml
from snapcraft.meta import manifest, snap_yaml
from snapcraft.projects import GrammarAwareProject, Project
from snapcraft.providers import capture_logs_from_instance

Expand Down Expand Up @@ -134,6 +137,7 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None:
snap_project = get_snap_project()
yaml_data = process_yaml(snap_project.project_file)
parse_info = _extract_parse_info(yaml_data)
start_time = datetime.now()

if parsed_args.provider:
raise errors.SnapcraftError("Option --provider is not supported.")
Expand All @@ -155,6 +159,7 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None:
parse_info=parse_info,
parallel_build_count=build_count,
assets_dir=snap_project.assets_dir,
start_time=start_time,
parsed_args=parsed_args,
)
except PermissionError as err:
Expand All @@ -167,6 +172,7 @@ def _run_command(
project: Project,
parse_info: Dict[str, List[str]],
assets_dir: Path,
start_time: datetime,
parallel_build_count: int,
parsed_args: "argparse.Namespace",
) -> None:
Expand Down Expand Up @@ -264,6 +270,14 @@ def _run_command(
)
emit.message("Generated snap metadata", intermediate=True)

if parsed_args.enable_manifest:
_generate_manifest(
project,
lifecycle=lifecycle,
start_time=start_time,
parsed_args=parsed_args,
)

if command_name in ("pack", "snap"):
pack.pack_snap(
lifecycle.prime_dir,
Expand All @@ -272,6 +286,41 @@ def _run_command(
)


def _generate_manifest(
project: Project,
*,
lifecycle: PartsLifecycle,
start_time: datetime,
parsed_args: "argparse.Namespace",
) -> None:
"""Create and populate the manifest file."""
emit.progress("Generating snap manifest...")
image_information = parsed_args.manifest_image_information or "{}"

parts = copy.deepcopy(project.parts)
for name, part in parts.items():
assets = lifecycle.get_part_pull_assets(part_name=name)
if assets:
part["stage-packages"] = assets.get("stage-packages", [])
for key in ("stage", "prime", "stage-packages", "build-packages"):
part.setdefault(key, [])

manifest.write(
project,
lifecycle.prime_dir,
arch=lifecycle.target_arch,
parts=parts,
start_time=start_time,
image_information=image_information,
primed_stage_packages=lifecycle.get_primed_stage_packages(),
)
emit.message("Generated snap manifest", intermediate=True)

# Also copy the original snapcraft.yaml
snap_project = get_snap_project()
shutil.copy(snap_project.project_file, lifecycle.prime_dir / "snap")


def _clean_provider(project: Project, parsed_args: "argparse.Namespace") -> None:
"""Clean the provider environment.

Expand Down Expand Up @@ -320,6 +369,13 @@ def _run_in_provider(
if getattr(parsed_args, "shell_after", False):
cmd.append("--shell-after")

if getattr(parsed_args, "enable_manifest", False):
cmd.append("--enable-manifest")
build_information = getattr(parsed_args, "manifest_build_information", None)
if build_information:
cmd.append("--manifest-build-information")
cmd.append(build_information)

output_dir = utils.get_managed_environment_project_path()

emit.progress("Launching instance...")
Expand Down
18 changes: 17 additions & 1 deletion snapcraft/parts/parts.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import pathlib
import subprocess
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Set

import craft_parts
from craft_cli import emit
Expand Down Expand Up @@ -72,6 +72,7 @@ def __init__(
self._part_names = part_names
self._adopt_info = adopt_info
self._parse_info = parse_info
self._all_part_names = [*all_parts]

emit.progress("Initializing parts lifecycle")

Expand Down Expand Up @@ -243,6 +244,21 @@ def extract_metadata(self) -> List[ExtractedMetadata]:

return metadata_list

def get_primed_stage_packages(self) -> List[str]:
"""Obtain the list of primed stage packages from all parts."""
primed_stage_packages: Set[str] = set()
for name in self._all_part_names:
stage_packages = self._lcm.get_primed_stage_packages(part_name=name)
if stage_packages:
primed_stage_packages |= set(stage_packages)
package_list = list(primed_stage_packages)
package_list.sort()
return package_list

def get_part_pull_assets(self, *, part_name: str) -> Optional[Dict[str, Any]]:
"""Obtain the pull state assets."""
return self._lcm.get_pull_assets(part_name=part_name)


def _launch_shell(*, cwd: Optional[pathlib.Path] = None) -> None:
"""Launch a user shell for debugging environment.
Expand Down
2 changes: 2 additions & 0 deletions snapcraft/providers/_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ def get_command_environment() -> Dict[str, Optional[str]]:
"https_proxy",
"no_proxy",
"SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS",
"SNAPCRAFT_BUILD_INFO",
"SNAPCRAFT_IMAGE_INFO",
]:
if env_key in os.environ:
env[env_key] = os.environ[env_key]
Expand Down
5 changes: 5 additions & 0 deletions spread.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ suites:
systems:
- ubuntu-22.04*

tests/spread/core22/manifest/:
summary: core22 manifest tests
systems:
- ubuntu-22.04*

# General, core suite
tests/spread/general/:
summary: tests of snapcraft core functionality
Expand Down
Loading