Exemplo n.º 1
0
def test_lifecycle_run_command_clean(snapcraft_yaml, project_vars, new_dir,
                                     mocker):
    """Clean provider project when called without parts."""
    project = Project.unmarshal(snapcraft_yaml(base="core22"))
    clean_mock = mocker.patch(
        "snapcraft.providers.LXDProvider.clean_project_environments",
        return_value=["instance-name"],
    )

    parts_lifecycle._run_command(
        "clean",
        project=project,
        parse_info={},
        assets_dir=Path(),
        start_time=datetime.now(),
        parallel_build_count=8,
        parsed_args=argparse.Namespace(
            directory=None,
            output=None,
            destructive_mode=False,
            use_lxd=False,
            parts=None,
        ),
    )

    assert clean_mock.mock_calls == [
        call(
            project_name="mytest",
            project_path=new_dir,
            build_on=get_host_architecture(),
            build_for=get_host_architecture(),
        )
    ]
Exemplo n.º 2
0
def _run_in_provider(project: Project, command_name: str,
                     parsed_args: "argparse.Namespace") -> None:
    """Pack image in provider instance."""
    emit.debug("Checking build provider availability")
    provider_name = "lxd" if parsed_args.use_lxd else None
    provider = providers.get_provider(provider_name)
    provider.ensure_provider_is_available()

    cmd = ["snapcraft", command_name]

    if hasattr(parsed_args, "parts"):
        cmd.extend(parsed_args.parts)

    if getattr(parsed_args, "output", None):
        cmd.extend(["--output", parsed_args.output])

    if emit.get_mode() == EmitterMode.VERBOSE:
        cmd.append("--verbose")
    elif emit.get_mode() == EmitterMode.QUIET:
        cmd.append("--quiet")
    elif emit.get_mode() == EmitterMode.DEBUG:
        cmd.append("--verbosity=debug")
    elif emit.get_mode() == EmitterMode.TRACE:
        cmd.append("--verbosity=trace")

    if parsed_args.debug:
        cmd.append("--debug")
    if getattr(parsed_args, "shell", False):
        cmd.append("--shell")
    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...")
    with provider.launched_environment(
            project_name=project.name,
            project_path=Path().absolute(),
            base=project.get_effective_base(),
            bind_ssh=parsed_args.bind_ssh,
            build_on=get_host_architecture(),
            build_for=get_host_architecture(),
    ) as instance:
        try:
            with emit.pause():
                instance.execute_run(cmd, check=True, cwd=output_dir)
            capture_logs_from_instance(instance)
        except subprocess.CalledProcessError as err:
            capture_logs_from_instance(instance)
            raise providers.ProviderError(
                f"Failed to execute {command_name} in instance.") from err
Exemplo n.º 3
0
 def run(self, parsed_args):
     snap_project = get_snap_project()
     yaml_data = process_yaml(snap_project.project_file)
     expanded_yaml_data = extensions.apply_extensions(
         yaml_data,
         arch=get_host_architecture(),
         target_arch=get_host_architecture(),
     )
     Project.unmarshal(expanded_yaml_data)
     emit.message(
         yaml.safe_dump(expanded_yaml_data, indent=4, sort_keys=False))
Exemplo n.º 4
0
def _expand_environment(snapcraft_yaml: Dict[str, Any], *,
                        parallel_build_count: int, target_arch: str) -> None:
    """Expand global variables in the provided dictionary values.

    :param snapcraft_yaml: A dictionary containing the contents of the
        snapcraft.yaml project file.
    """
    if utils.is_managed_mode():
        work_dir = utils.get_managed_environment_home_path()
    else:
        work_dir = Path.cwd()

    project_vars = {
        "version": snapcraft_yaml.get("version", ""),
        "grade": snapcraft_yaml.get("grade", ""),
    }

    if target_arch == "all":
        target_arch = get_host_architecture()

    dirs = craft_parts.ProjectDirs(work_dir=work_dir)
    info = craft_parts.ProjectInfo(
        application_name="snapcraft",  # not used in environment expansion
        cache_dir=Path(),  # not used in environment expansion
        arch=convert_architecture_deb_to_platform(target_arch),
        parallel_build_count=parallel_build_count,
        project_name=snapcraft_yaml.get("name", ""),
        project_dirs=dirs,
        project_vars=project_vars,
    )
    _set_global_environment(info)

    craft_parts.expand_environment(snapcraft_yaml,
                                   info=info,
                                   skip=["name", "version"])
Exemplo n.º 5
0
def test_get_host_architecture(platform_machine, platform_architecture, mocker,
                               deb_arch):
    """Test all platform mappings in addition to unknown."""
    mocker.patch("platform.machine", return_value=platform_machine)
    mocker.patch("platform.architecture", return_value=platform_architecture)

    assert utils.get_host_architecture() == deb_arch
Exemplo n.º 6
0
class ArchitectureProject(ProjectModel, extra=pydantic.Extra.ignore):
    """Project definition containing only architecture data."""

    architectures: List[Union[str, Architecture]] = [get_host_architecture()]

    @pydantic.validator("architectures", always=True)
    @classmethod
    def _validate_architecture_data(cls, architectures):
        """Validate architecture data."""
        return _validate_architectures(architectures)

    @classmethod
    def unmarshal(cls, data: Dict[str, Any]) -> "ArchitectureProject":
        """Create and populate a new ``Project`` object from dictionary data.

        The unmarshal method validates entries in the input dictionary, populating
        the corresponding fields in the data object.

        :param data: The dictionary data to unmarshal.

        :return: The newly created object.

        :raise TypeError: If data is not a dictionary.
        """
        if not isinstance(data, dict):
            raise TypeError("Project data is not a dictionary")

        try:
            architectures = ArchitectureProject(**data)
        except pydantic.ValidationError as err:
            raise ProjectValidationError(_format_pydantic_errors(
                err.errors())) from err

        return architectures
Exemplo n.º 7
0
def _construct_deb822_source(
    *,
    architectures: Optional[List[str]] = None,
    components: Optional[List[str]] = None,
    formats: Optional[List[str]] = None,
    suites: List[str],
    url: str,
) -> str:
    """Construct deb-822 formatted sources.list config string."""
    with io.StringIO() as deb822:
        if formats:
            type_text = " ".join(formats)
        else:
            type_text = "deb"

        print(f"Types: {type_text}", file=deb822)

        print(f"URIs: {url}", file=deb822)

        suites_text = " ".join(suites)
        print(f"Suites: {suites_text}", file=deb822)

        if components:
            components_text = " ".join(components)
            print(f"Components: {components_text}", file=deb822)

        if architectures:
            arch_text = " ".join(architectures)
        else:
            arch_text = utils.get_host_architecture()

        print(f"Architectures: {arch_text}", file=deb822)

        return deb822.getvalue()
Exemplo n.º 8
0
def _clean_provider(project: Project,
                    parsed_args: "argparse.Namespace") -> None:
    """Clean the provider environment.

    :param project: The project to clean.
    """
    emit.debug("Clean build provider")
    provider_name = "lxd" if parsed_args.use_lxd else None
    provider = providers.get_provider(provider_name)
    instance_names = provider.clean_project_environments(
        project_name=project.name,
        project_path=Path().absolute(),
        build_on=get_host_architecture(),
        build_for=get_host_architecture(),
    )
    if instance_names:
        emit.message(f"Removed instance: {', '.join(instance_names)}")
    else:
        emit.message("No instances to remove")
Exemplo n.º 9
0
    def __init__(
        self,
        all_parts: Dict[str, Any],
        *,
        work_dir: pathlib.Path,
        assets_dir: pathlib.Path,
        base: str,
        package_repositories: List[Dict[str, Any]],
        parallel_build_count: int,
        part_names: Optional[List[str]],
        adopt_info: Optional[str],
        parse_info: Dict[str, List[str]],
        project_name: str,
        project_vars: Dict[str, str],
        extra_build_snaps: Optional[List[str]] = None,
        target_arch: str,
    ):
        self._work_dir = work_dir
        self._assets_dir = assets_dir
        self._package_repositories = package_repositories
        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")

        # set the cache dir for parts package management
        cache_dir = BaseDirectory.save_cache_path("snapcraft")

        if target_arch == "all":
            target_arch = get_host_architecture()

        platform_arch = convert_architecture_deb_to_platform(target_arch)

        try:
            self._lcm = craft_parts.LifecycleManager(
                {"parts": all_parts},
                application_name="snapcraft",
                work_dir=work_dir,
                cache_dir=cache_dir,
                arch=platform_arch,
                base=base,
                ignore_local_sources=["*.snap"],
                extra_build_snaps=extra_build_snaps,
                parallel_build_count=parallel_build_count,
                project_name=project_name,
                project_vars_part_name=adopt_info,
                project_vars=project_vars,
            )
        except craft_parts.PartsError as err:
            raise errors.PartsLifecycleError(str(err)) from err
Exemplo n.º 10
0
def test_lifecycle_pack_managed(cmd, snapcraft_yaml, project_vars, new_dir,
                                mocker):
    project = Project.unmarshal(snapcraft_yaml(base="core22"))
    run_in_provider_mock = mocker.patch(
        "snapcraft.parts.lifecycle._run_in_provider")
    run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run")
    pack_mock = mocker.patch("snapcraft.pack.pack_snap")
    mocker.patch("snapcraft.meta.snap_yaml.write")
    mocker.patch("snapcraft.utils.is_managed_mode", return_value=True)
    mocker.patch(
        "snapcraft.utils.get_managed_environment_home_path",
        return_value=new_dir / "home",
    )

    parts_lifecycle._run_command(
        cmd,
        project=project,
        parse_info={},
        assets_dir=Path(),
        start_time=datetime.now(),
        parallel_build_count=8,
        parsed_args=argparse.Namespace(
            directory=None,
            output=None,
            debug=False,
            bind_ssh=False,
            enable_manifest=False,
            manifest_image_information=None,
            destructive_mode=False,
            shell=False,
            shell_after=False,
            use_lxd=False,
            parts=[],
        ),
    )

    assert run_in_provider_mock.mock_calls == []
    assert run_mock.mock_calls == [
        call("prime", debug=False, shell=False, shell_after=False)
    ]
    assert pack_mock.mock_calls[:1] == [
        call(
            new_dir / "home/prime",
            output=None,
            compression="xz",
            name="mytest",
            version="0.1",
            target_arch=get_host_architecture(),
        )
    ]
Exemplo n.º 11
0
def get_build_plan(yaml_data: Dict[str, Any],
                   parsed_args: "argparse.Namespace") -> List[Tuple[str, str]]:
    """Get a list of all build_on->build_for architectures from the project file.

    Additionally, check for the command line argument `--build-for <architecture>`
    When defined, the build plan will only contain builds where `build-for`
    matches `SNAPCRAFT_BUILD_FOR`.
    Note: `--build-for` defaults to the environmental variable `SNAPCRAFT_BUILD_FOR`.

    :param yaml_data: The project YAML data.
    :param parsed_args: snapcraft's argument namespace

    :return: List of tuples of every valid build-on->build-for combination.
    """
    archs = ArchitectureProject.unmarshal(yaml_data).architectures

    host_arch = get_host_architecture()
    build_plan: List[Tuple[str, str]] = []

    # `isinstance()` calls are for mypy type checking and should not change logic
    for arch in [arch for arch in archs if isinstance(arch, Architecture)]:
        for build_on in arch.build_on:
            if build_on in host_arch and isinstance(arch.build_for, list):
                build_plan.append((host_arch, arch.build_for[0]))
            else:
                emit.verbose(
                    f"Skipping build-on: {build_on} build-for: {arch.build_for}"
                    f" because build-on doesn't match host arch: {host_arch}")

    # filter out builds not matching argument `--build_for` or env `SNAPCRAFT_BUILD_FOR`
    build_for_arg = parsed_args.build_for
    if build_for_arg is not None:
        build_plan = [
            build for build in build_plan if build[1] == build_for_arg
        ]

    if len(build_plan) == 0:
        emit.message("Could not make build plan:"
                     " build-on architectures in snapcraft.yaml"
                     f" does not match host architecture ({host_arch}).")
    else:
        log_output = "Created build plan:"
        for build in build_plan:
            log_output += f"\n  build-on: {build[0]} build-for: {build[1]}"
        emit.trace(log_output)

    return build_plan
Exemplo n.º 12
0
def test_root_packages(minimal_yaml_data, key, value):
    minimal_yaml_data[key] = value
    arch = get_host_architecture()

    assert parts_lifecycle.apply_yaml(
        minimal_yaml_data, build_on=arch, build_for=arch
    ) == {
        "name": "name",
        "base": "core22",
        "confinement": "strict",
        "grade": "devel",
        "version": "1.0",
        "summary": "summary",
        "description": "description",
        "architectures": [Architecture(build_on=arch, build_for=arch)],
        "parts": {"nil": {}, "snapcraft/core": {"plugin": "nil", key: ["foo"]}},
    }
Exemplo n.º 13
0
def test_lifecycle_run_provider(cmd, snapcraft_yaml, new_dir, mocker):
    """Option --provider is not supported in core22."""
    snapcraft_yaml(base="core22")
    run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run")
    mocker.patch(
        "snapcraft.providers.Provider.is_base_available", return_value=(True, None)
    )

    with pytest.raises(errors.SnapcraftError) as raised:
        parts_lifecycle.run(
            cmd,
            parsed_args=argparse.Namespace(
                destructive_mode=False,
                use_lxd=False,
                provider="some",
                build_for=get_host_architecture(),
            ),
        )

    assert run_mock.mock_calls == []
    assert str(raised.value) == "Option --provider is not supported."
Exemplo n.º 14
0
def test_lifecycle_run_command_pack(cmd, snapcraft_yaml, project_vars, new_dir, mocker):
    project = Project.unmarshal(snapcraft_yaml(base="core22"))
    run_mock = mocker.patch("snapcraft.parts.PartsLifecycle.run")
    pack_mock = mocker.patch("snapcraft.pack.pack_snap")

    parts_lifecycle._run_command(
        cmd,
        project=project,
        parse_info={},
        assets_dir=Path(),
        start_time=datetime.now(),
        parallel_build_count=8,
        parsed_args=argparse.Namespace(
            directory=None,
            output=None,
            debug=False,
            destructive_mode=True,
            enable_manifest=False,
            shell=False,
            shell_after=False,
            use_lxd=False,
            parts=[],
        ),
    )

    assert run_mock.mock_calls == [
        call("prime", debug=False, shell=False, shell_after=False)
    ]
    assert pack_mock.mock_calls[:1] == [
        call(
            new_dir / "prime",
            output=None,
            compression="xz",
            name="mytest",
            version="0.1",
            target_arch=get_host_architecture(),
        )
    ]
Exemplo n.º 15
0
class Project(ProjectModel):
    """Snapcraft project definition.

    See https://snapcraft.io/docs/snapcraft-yaml-reference

    XXX: Not implemented in this version
    - system-usernames
    """

    name: constr(max_length=40)  # type: ignore
    title: Optional[constr(max_length=40)]  # type: ignore
    base: Optional[str]
    build_base: Optional[str]
    compression: Literal["lzo", "xz"] = "xz"
    version: Optional[constr(max_length=32, strict=True)]  # type: ignore
    contact: Optional[Union[str, UniqueStrList]]
    donation: Optional[Union[str, UniqueStrList]]
    issues: Optional[Union[str, UniqueStrList]]
    source_code: Optional[str]
    website: Optional[str]
    summary: Optional[constr(max_length=78)]  # type: ignore
    description: Optional[str]
    type: Optional[Literal["app", "base", "gadget", "kernel", "snapd"]]
    icon: Optional[str]
    confinement: Literal["classic", "devmode", "strict"]
    layout: Optional[
        Dict[str, Dict[Literal["symlink", "bind", "bind-file", "type"], str]]
    ]
    license: Optional[str]
    grade: Optional[Literal["stable", "devel"]]
    architectures: List[Union[str, Architecture]] = [get_host_architecture()]
    assumes: UniqueStrList = []
    package_repositories: List[Dict[str, Any]] = []  # handled by repo
    hooks: Optional[Dict[str, Hook]]
    passthrough: Optional[Dict[str, Any]]
    apps: Optional[Dict[str, App]]
    plugs: Optional[Dict[str, Union[ContentPlug, Any]]]
    slots: Optional[Dict[str, Any]]
    parts: Dict[str, Any]  # parts are handled by craft-parts
    epoch: Optional[str]
    adopt_info: Optional[str]
    system_usernames: Optional[Dict[str, Any]]
    environment: Optional[Dict[str, Optional[str]]]
    build_packages: Optional[GrammarStrList]
    build_snaps: Optional[GrammarStrList]

    @pydantic.validator("plugs")
    @classmethod
    def _validate_plugs(cls, plugs):
        if plugs is not None:
            for plug_name, plug in plugs.items():
                if (
                    isinstance(plug, dict)
                    and plug.get("interface") == "content"
                    and not plug.get("target")
                ):
                    raise ValueError(
                        f"ContentPlug '{plug_name}' must have a 'target' parameter."
                    )
                if isinstance(plug, list):
                    raise ValueError(f"Plug '{plug_name}' cannot be a list.")

        return plugs

    @pydantic.root_validator(pre=True)
    @classmethod
    def _validate_adoptable_fields(cls, values):
        for field in MANDATORY_ADOPTABLE_FIELDS:
            if field not in values and "adopt-info" not in values:
                raise ValueError(f"Snap {field} is required if not using adopt-info")
        return values

    @pydantic.root_validator(pre=True)
    @classmethod
    def _validate_mandatory_base(cls, values):
        snap_type = values.get("type")
        base = values.get("base")
        if (base is not None) ^ (snap_type not in ["base", "kernel", "snapd"]):
            raise ValueError(
                "Snap base must be declared when type is not base, kernel or snapd"
            )
        return values

    @pydantic.validator("name")
    @classmethod
    def _validate_name(cls, name):
        if not re.match(r"^[a-z0-9-]*[a-z][a-z0-9-]*$", name):
            raise ValueError(
                "Snap names can only use ASCII lowercase letters, numbers, and hyphens, "
                "and must have at least one letter"
            )

        if name.startswith("-"):
            raise ValueError("Snap names cannot start with a hyphen")

        if name.endswith("-"):
            raise ValueError("Snap names cannot end with a hyphen")

        if "--" in name:
            raise ValueError("Snap names cannot have two hyphens in a row")

        return name

    @pydantic.validator("version")
    @classmethod
    def _validate_version(cls, version, values):
        if not version and "adopt_info" not in values:
            raise ValueError("Version must be declared if not adopting metadata")

        if version and not re.match(
            r"^[a-zA-Z0-9](?:[a-zA-Z0-9:.+~-]*[a-zA-Z0-9+~])?$", version
        ):
            raise ValueError(
                "Snap versions consist of upper- and lower-case alphanumeric characters, "
                "as well as periods, colons, plus signs, tildes, and hyphens. They cannot "
                "begin with a period, colon, plus sign, tilde, or hyphen. They cannot end "
                "with a period, colon, or hyphen"
            )

        return version

    @pydantic.validator("grade", "summary", "description")
    @classmethod
    def _validate_adoptable_field(cls, field_value, values, field):
        if not field_value and "adopt_info" not in values:
            raise ValueError(
                f"{field.name.capitalize()} must be declared if not adopting metadata"
            )
        return field_value

    @pydantic.validator("build_base", always=True)
    @classmethod
    def _validate_build_base(cls, build_base, values):
        """Build-base defaults to the base value if not specified."""
        if not build_base:
            build_base = values.get("base")
        return build_base

    @pydantic.validator("package_repositories", each_item=True)
    @classmethod
    def _validate_package_repositories(cls, item):
        """Ensure package-repositories format is correct."""
        repo.validate_repository(item)
        return item

    @pydantic.validator("parts", each_item=True)
    @classmethod
    def _validate_parts(cls, item):
        """Verify each part (craft-parts will re-validate this)."""
        parts_validation.validate_part(item)
        return item

    @pydantic.validator("epoch")
    @classmethod
    def _validate_epoch(cls, epoch):
        """Verify epoch format."""
        if epoch is not None and not re.match(r"^(?:0|[1-9][0-9]*[*]?)$", epoch):
            raise ValueError(
                "Epoch is a positive integer followed by an optional asterisk"
            )

        return epoch

    @pydantic.validator("architectures", always=True)
    @classmethod
    def _validate_architecture_data(cls, architectures):
        """Validate architecture data."""
        return _validate_architectures(architectures)

    @classmethod
    def unmarshal(cls, data: Dict[str, Any]) -> "Project":
        """Create and populate a new ``Project`` object from dictionary data.

        The unmarshal method validates entries in the input dictionary, populating
        the corresponding fields in the data object.

        :param data: The dictionary data to unmarshal.

        :return: The newly created object.

        :raise TypeError: If data is not a dictionary.
        """
        if not isinstance(data, dict):
            raise TypeError("Project data is not a dictionary")

        try:
            project = Project(**data)
        except pydantic.ValidationError as err:
            raise ProjectValidationError(_format_pydantic_errors(err.errors())) from err

        return project

    def _get_content_plugs(self) -> List[ContentPlug]:
        """Get list of content plugs."""
        if self.plugs is not None:
            return [
                plug for plug in self.plugs.values() if isinstance(plug, ContentPlug)
            ]
        return []

    def get_content_snaps(self) -> List[str]:
        """Get list of snaps from ContentPlug `default-provider` fields."""
        return [
            x.default_provider
            for x in self._get_content_plugs()
            if x.default_provider is not None
        ]

    def get_extra_build_snaps(self) -> List[str]:
        """Get list of extra snaps required to build."""
        # Build snaps defined by the user with channel stripped
        build_snaps: List[str] = []
        for part in self.parts.values():
            build_snaps.extend(part.get("build-snaps", []))
        part_build_snaps = {p.split("/")[0] for p in build_snaps}

        # Content snaps the project uses
        content_snaps = set(self.get_content_snaps())

        # Do not add the content snaps if provided by the user
        extra_build_snaps = list(content_snaps - part_build_snaps)

        # Always add the base as an extra build snap
        if self.base is not None:
            extra_build_snaps.append(self.base)
        extra_build_snaps.sort()

        return extra_build_snaps

    def get_effective_base(self) -> str:
        """Return the base to use to create the snap."""
        base = get_effective_base(
            base=self.base,
            build_base=self.build_base,
            project_type=self.type,
            name=self.name,
        )

        # will not happen after schema validation
        if base is None:
            raise RuntimeError("cannot determine build base")

        return base