Example #1
0
def load(filestream: TextIO) -> Dict[str, Any]:
    """Load and parse a YAML-formatted file.

    :param filename: The YAML file to load.

    :raises SnapcraftError: if loading didn't succeed.
    :raises LegacyFallback: if the project's base is not core22.
    """
    try:
        data = yaml.safe_load(filestream)
        build_base = utils.get_effective_base(
            base=data.get("base"),
            build_base=data.get("build-base"),
            project_type=data.get("type"),
            name=data.get("name"),
        )

        if build_base is None:
            raise errors.LegacyFallback("no base defined")
        if build_base != "core22":
            raise errors.LegacyFallback("base is not core22")
    except yaml.error.YAMLError as err:
        raise errors.SnapcraftError(
            f"snapcraft.yaml parsing error: {err!s}") from err

    filestream.seek(0)

    try:
        return yaml.load(filestream, Loader=_SafeLoader)
    except yaml.error.YAMLError as err:
        raise errors.SnapcraftError(
            f"snapcraft.yaml parsing error: {err!s}") from err
Example #2
0
    def request(self, *args, **kwargs) -> requests.Response:
        """Request using the BaseClient and wrap responses that require action.

        Actionable items are those that could prompt a login or registration.
        """
        try:
            return self.store_client.request(*args, **kwargs)
        except craft_store.errors.StoreServerError as store_error:
            if (store_error.response.status_code == requests.codes.unauthorized  # pylint: disable=no-member
                ):
                if os.getenv(constants.ENVIRONMENT_STORE_CREDENTIALS):
                    raise errors.SnapcraftError(
                        "Provided credentials are no longer valid for the Snap Store.",
                        resolution="Regenerate them and try again.",
                    ) from store_error

                emit.message("You are required to re-login before continuing")
                self.store_client.logout()
            else:
                raise
        except craft_store.errors.CredentialsUnavailable:
            emit.message("You are required to login before continuing")

        self.login()
        return self.store_client.request(*args, **kwargs)
Example #3
0
def get_snap_tool(command_name: str) -> str:
    """Return the path of a command found in the snap.

    If snapcraft is not running as a snap, shutil.which() is used
    to resolve the command using PATH.

    :param command_name: the name of the command to resolve a path for.
    :return: Path to command

    :raises SnapcraftError: if command_name was not found.
    """
    if os.environ.get("SNAP_NAME") != "snapcraft":
        return get_host_tool(command_name)

    snap_path = os.getenv("SNAP")
    if snap_path is None:
        raise RuntimeError(
            "The SNAP environment variable is not defined, but SNAP_NAME is?")

    command_path = _find_command_path_in_root(snap_path, command_name)

    if command_path is None:
        raise errors.SnapcraftError(
            f"Cannot find snap tool {command_name!r}",
            resolution="Please report this error to the Snapcraft maintainers.",
        )

    return command_path
Example #4
0
def _get_dom(path: str) -> lxml.etree.ElementTree:
    try:
        return lxml.etree.parse(path)
    except OSError as err:
        raise errors.SnapcraftError(str(err)) from err
    except lxml.etree.ParseError as err:
        raise errors.MetadataExtractionError(path, str(err)) from err
Example #5
0
    def version_id(self) -> str:
        """Return the OS version ID.

        :raises SnapcraftError: If no version ID can be determined.
        """
        with contextlib.suppress(KeyError):
            return self._os_release["VERSION_ID"]

        raise errors.SnapcraftError("Unable to determine host OS version ID")
Example #6
0
    def name(self) -> str:
        """Return the OS name.

        :raises SnapcraftError: If no name can be determined.
        """
        with contextlib.suppress(KeyError):
            return self._os_release["NAME"]

        raise errors.SnapcraftError("Unable to determine host OS name")
Example #7
0
def pack_snap(
    directory: Path,
    *,
    output: Optional[str],
    compression: Optional[str] = None,
    name: Optional[str] = None,
    version: Optional[str] = None,
    target_arch: Optional[str] = None,
) -> str:
    """Pack snap contents with `snap pack`.

    `output` may either be a directory, a file path, or just a file name.
      - directory: write snap to directory with default snap name
      - file path: write snap to specified directory with specified snap name
      - file name: write snap to cwd with specified snap name

    If name, version, and target architecture are not specified, then snap
    will use its default naming convention.

    :param directory: Directory to pack.
    :param output: Snap file name or directory.
    :param compression: Compression type to use, None for defaults.
    :param name: Name of snap project.
    :param version: Version of snap project.
    :param target_arch: Target architecture the snap project is built to.
    """
    emit.debug(f"pack_snap: output={output!r}, compression={compression!r}")

    # TODO remove workaround once LP: #1950465 is fixed
    _verify_snap(directory)

    # create command formatted as `snap pack <options> <snap-dir> <output-dir>`
    command: List[Union[str, Path]] = ["snap", "pack"]
    output_file = _get_filename(output, name, version, target_arch)
    if output_file is not None:
        command.extend(["--filename", output_file])
    if compression is not None:
        command.extend(["--compression", compression])
    command.append(directory)
    command.append(_get_directory(output))

    emit.progress("Creating snap package...")
    emit.debug(f"Pack command: {command}")
    try:
        proc = subprocess.run(command,
                              capture_output=True,
                              check=True,
                              universal_newlines=True)
    except subprocess.CalledProcessError as err:
        msg = f"Cannot pack snap file: {err!s}"
        if err.stderr:
            msg += f" ({err.stderr.strip()!s})"
        raise errors.SnapcraftError(msg)

    snap_filename = Path(str(proc.stdout).partition(":")[2].strip()).name
    return snap_filename
Example #8
0
def _update_project_variables(project: Project, project_vars: Dict[str, str]):
    """Update project fields with values set during lifecycle processing."""
    try:
        if project_vars["version"]:
            project.version = project_vars["version"]
        if project_vars["grade"]:
            project.grade = project_vars["grade"]  # type: ignore
    except pydantic.ValidationError as err:
        _raise_formatted_validation_error(err)
        raise errors.SnapcraftError(f"error setting variable: {err}")
Example #9
0
def update_project_metadata(
    project: Project,
    *,
    project_vars: Dict[str, str],
    metadata_list: List[ExtractedMetadata],
    assets_dir: Path,
    prime_dir: Path,
) -> None:
    """Set project fields using corresponding adopted entries.

    Fields are validated on assignment by pydantic.

    :param project: The project to update.
    :param project_vars: The variables updated during lifecycle execution.
    :param metadata_list: List containing parsed information from metadata files.

    :raises SnapcraftError: If project update failed.
    """
    _update_project_variables(project, project_vars)

    for metadata in metadata_list:
        # Data specified in the project yaml has precedence over extracted data
        if metadata.title and not project.title:
            project.title = metadata.title

        if metadata.summary and not project.summary:
            project.summary = metadata.summary

        if metadata.description and not project.description:
            project.description = metadata.description

        if metadata.version and not project.version:
            project.version = metadata.version

        if metadata.grade and not project.grade:
            project.grade = metadata.grade  # type: ignore

        emit.debug(f"project icon: {project.icon!r}")
        emit.debug(f"metadata icon: {metadata.icon!r}")

        if not project.icon:
            _update_project_icon(project,
                                 metadata=metadata,
                                 assets_dir=assets_dir)

        _update_project_app_desktop_file(project,
                                         metadata=metadata,
                                         assets_dir=assets_dir,
                                         prime_dir=prime_dir)

    # Fields that must not end empty
    for field in MANDATORY_ADOPTABLE_FIELDS:
        if not getattr(project, field):
            raise errors.SnapcraftError(
                f"Field {field!r} was not adopted from metadata")
Example #10
0
def run_project_checks(project: Project, *, assets_dir: Path) -> None:
    """Execute consistency checks for project and project files.

    The checks done here are meant to be light, and not rely on the
    build environment.
    """
    # Assets dir shouldn't contain unexpected files.
    if assets_dir.is_dir():
        _check_snap_dir(assets_dir)

    # Icon should refer to project file, verify it exists.
    if project.icon and not Path(project.icon).exists():
        raise errors.SnapcraftError(
            f"Specified icon {project.icon!r} does not exist.")
Example #11
0
 def login(
     self,
     *,
     permissions: Sequence[str],
     description: str,
     ttl: int,
     packages: Optional[Sequence[craft_store.endpoints.Package]] = None,
     channels: Optional[Sequence[str]] = None,
     **kwargs,
 ) -> str:
     raise errors.SnapcraftError(
         "Cannot login with existing legacy credentials in use",
         resolution="Run 'snapcraft logout' first to clear them",
     )
Example #12
0
def get_snap_project() -> _SnapProject:
    """Find the snapcraft.yaml to load.

    :raises SnapcraftError: if the project yaml file cannot be found.
    """
    for snap_project in _SNAP_PROJECT_FILES:
        if snap_project.project_file.exists():
            return snap_project

    raise errors.SnapcraftError(
        "Could not find snap/snapcraft.yaml. Are you sure you are in the "
        "right directory?",
        resolution="To start a new project, use `snapcraft init`",
    )
Example #13
0
def _verify_snap(directory: Path) -> None:
    emit.debug("pack_snap: check skeleton")
    try:
        subprocess.run(
            ["snap", "pack", "--check-skeleton", directory],
            capture_output=True,
            check=True,
            universal_newlines=True,
        )
    except subprocess.CalledProcessError as err:
        msg = f"Cannot pack snap file: {err!s}"
        if err.stderr:
            msg += f" ({err.stderr.strip()!s})"
        raise errors.SnapcraftError(msg)
Example #14
0
def _raise_formatted_validation_error(err: pydantic.ValidationError):
    error_list = err.errors()
    if len(error_list) != 1:
        return

    error = error_list[0]
    loc = error.get("loc")
    msg = error.get("msg")

    if not (loc and msg) or not isinstance(loc, tuple):
        return

    varname = ".".join((x for x in loc if isinstance(x, str)))
    raise errors.SnapcraftError(f"error setting {varname}: {msg}")
Example #15
0
def process_yaml(project_file: Path) -> Dict[str, Any]:
    """Process the yaml from project file.

    :raises SnapcraftError: if the project yaml file cannot be loaded.
    """
    try:
        with open(project_file, encoding="utf-8") as yaml_file:
            yaml_data = yaml_utils.load(yaml_file)
    except OSError as err:
        msg = err.strerror
        if err.filename:
            msg = f"{msg}: {err.filename!r}."
        raise errors.SnapcraftError(msg) from err

    return apply_yaml(yaml_data)
Example #16
0
def read(prime_dir: Path) -> SnapMetadata:
    """Read snap metadata file.

    :param prime_dir: The directory containing the snap payload.
    :return: The populated snap metadata.
    """
    snap_yaml = prime_dir / "meta" / "snap.yaml"
    try:
        with snap_yaml.open(encoding="utf-8") as file:
            data = yaml.safe_load(file)
    except OSError as error:
        raise errors.SnapcraftError(
            f"Cannot read snap metadata: {error}") from error

    return SnapMetadata.unmarshal(data)
Example #17
0
def _validate_command_chain(
    command_chain: List[str], *, name: str, prime_dir: Path
) -> None:
    """Verify if each item in the command chain is executble."""
    for item in command_chain:
        executable_path = prime_dir / item

        # command-chain entries must always be relative to the root of
        # the snap, i.e. PATH is not used.
        if not _is_executable(executable_path):
            raise errors.SnapcraftError(
                f"Failed to generate snap metadata: The command-chain item {item!r} "
                f"defined in {name} does not exist or is not executable.",
                resolution=f"Ensure that {item!r} is relative to the prime directory.",
            )
Example #18
0
    def version_codename(self) -> str:
        """Return the OS version codename.

        This first tries to use the VERSION_CODENAME. If that's missing, it
        tries to use the VERSION_ID to figure out the codename on its own.

        :raises SnapcraftError: If no version codename can be determined.
        """
        with contextlib.suppress(KeyError):
            return self._os_release["VERSION_CODENAME"]

        with contextlib.suppress(KeyError):
            return _ID_TO_UBUNTU_CODENAME[self._os_release["VERSION_ID"]]

        raise errors.SnapcraftError(
            "Unable to determine host OS version codename")
Example #19
0
def get_host_tool(command_name: str) -> str:
    """Return the full path of the given host tool.

    :param command_name: the name of the command to resolve a path for.
    :return: Path to command

    :raises SnapcraftError: if command_name was not found.
    """
    tool = shutil.which(command_name)
    if not tool:
        raise errors.SnapcraftError(
            f"A tool snapcraft depends on could not be found: {command_name!r}",
            resolution=
            "Ensure the tool is installed and available, and try again.",
        )
    return tool
Example #20
0
def run(command_name: str, parsed_args: "argparse.Namespace") -> None:
    """Run the parts lifecycle.

    :raises SnapcraftError: if the step name is invalid, or the project
        yaml file cannot be loaded.
    :raises LegacyFallback: if the project's base is not core22.
    """
    emit.debug(f"command: {command_name}, arguments: {parsed_args}")

    snap_project = get_snap_project()
    yaml_data = process_yaml(snap_project.project_file)
    start_time = datetime.now()
    build_plan = get_build_plan(yaml_data, parsed_args)

    if parsed_args.provider:
        raise errors.SnapcraftError("Option --provider is not supported.")

    # Register our own plugins and callbacks
    plugins.register()
    callbacks.register_prologue(_set_global_environment)
    callbacks.register_pre_step(_set_step_environment)

    build_count = utils.get_parallel_build_count()

    for build_on, build_for in build_plan:
        emit.verbose(f"Running on {build_on} for {build_for}")
        yaml_data_for_arch = apply_yaml(yaml_data, build_on, build_for)
        parse_info = _extract_parse_info(yaml_data_for_arch)
        _expand_environment(
            yaml_data_for_arch,
            parallel_build_count=build_count,
            target_arch=build_for,
        )
        project = Project.unmarshal(yaml_data_for_arch)

        try:
            _run_command(
                command_name,
                project=project,
                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:
            raise errors.FilePermissionError(err.filename, reason=err.strerror)
Example #21
0
def _get_architecture() -> str:
    snap_arch = os.getenv("SNAP_ARCH")
    # The first scenario is the general case as snapcraft will be running from the snap.
    if snap_arch is not None:
        try:
            miniconda_arch = _MINICONDA_ARCH_FROM_SNAP_ARCH[snap_arch]
        except KeyError as key_error:
            raise errors.SnapcraftError(
                f"Architecture not supported for conda plugin: {snap_arch!r}"
            ) from key_error
    # But there may be times when running from a virtualenv while doing development.
    else:
        machine = platform.machine()
        architecture = platform.architecture()[0]
        miniconda_arch = _MINICONDA_ARCH_FROM_PLATFORM[machine][architecture]

    return miniconda_arch
Example #22
0
def prompt(prompt_text: str, *, hide: bool = False) -> str:
    """Prompt and return the entered string.

    :param prompt_text: string used for the prompt.
    :param hide: hide user input if True.
    """
    if is_managed_mode():
        raise RuntimeError("prompting not yet supported in managed-mode")

    if not sys.stdin.isatty():
        raise errors.SnapcraftError("prompting not possible with no tty")

    if hide:
        method = getpass
    else:
        method = input  # type: ignore

    with emit.pause():
        return str(method(prompt_text))
Example #23
0
def process_yaml(project_file: Path) -> Dict[str, Any]:
    """Process yaml data from file into a dictionary.

    :param project_file: Path to project.

    :raises SnapcraftError: if the project yaml file cannot be loaded.

    :return: The processed YAML data.
    """
    try:
        with open(project_file, encoding="utf-8") as yaml_file:
            yaml_data = yaml_utils.load(yaml_file)
    except OSError as err:
        msg = err.strerror
        if err.filename:
            msg = f"{msg}: {err.filename!r}."
        raise errors.SnapcraftError(msg) from err

    return yaml_data
Example #24
0
    def run(self, parsed_args):
        client = store.StoreClientCLI()

        # Account info request to retrieve the snap-id
        account_info = client.get_account_info()
        try:
            snap_id = account_info["snaps"][store.constants.DEFAULT_SERIES][
                parsed_args.name]["snap-id"]
        except KeyError as key_error:
            emit.debug(f"{key_error!r} no found in {account_info!r}")
            raise errors.SnapcraftError(
                f"{parsed_args.name!r} not found or not owned by this account"
            ) from key_error

        client.close(
            snap_id=snap_id,
            channel=parsed_args.channel,
        )

        emit.message(
            f"Channel {parsed_args.channel!r} for {parsed_args.name!r} is now closed"
        )
Example #25
0
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 = utils.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=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)
Example #26
0
    def login(
        self,
        *,
        ttl: int = int(timedelta(days=365).total_seconds()),
        acls: Optional[Sequence[str]] = None,
        packages: Optional[Sequence[str]] = None,
        channels: Optional[Sequence[str]] = None,
    ) -> str:
        """Login to the Snap Store and prompt if required."""
        if os.getenv(constants.ENVIRONMENT_STORE_CREDENTIALS):
            raise errors.SnapcraftError(
                f"Cannot login with {constants.ENVIRONMENT_STORE_CREDENTIALS!r} set.",
                resolution=
                f"Unset {constants.ENVIRONMENT_STORE_CREDENTIALS!r} and try again.",
            )

        kwargs: Dict[str, Any] = {}
        if use_candid() is False:
            kwargs["email"], kwargs["password"] = _prompt_login()

        if packages is None:
            packages = []
        _packages = [
            craft_store.endpoints.Package(package_name=p, package_type="snap")
            for p in packages
        ]
        if acls is None:
            acls = [
                "package_access",
                "package_manage",
                "package_metrics",
                "package_push",
                "package_register",
                "package_release",
                "package_update",
            ]

        description = f"snapcraft@{_get_hostname()}"

        try:
            credentials = self.store_client.login(
                ttl=ttl,
                permissions=acls,
                channels=channels,
                packages=_packages,
                description=description,
                **kwargs,
            )
        except craft_store.errors.StoreServerError as store_error:
            if "twofactor-required" not in store_error.error_list:
                raise
            kwargs["otp"] = utils.prompt("Second-factor auth: ")

            credentials = self.store_client.login(
                ttl=ttl,
                permissions=acls,
                channels=channels,
                packages=_packages,
                description=description,
                **kwargs,
            )

        return credentials
Example #27
0
    def notify_upload(
        self,
        *,
        snap_name: str,
        upload_id: str,
        snap_file_size: int,
        built_at: Optional[str],
        channels: Optional[Sequence[str]],
    ) -> int:
        """Notify an upload to the Snap Store.

        :param snap_name: name of the snap
        :param upload_id: the upload_id to register with the Snap Store
        :param snap_file_size: the file size of the uploaded snap
        :param built_at: the build timestamp for this build
        :param channels: the channels to release to after being accepted into the Snap Store
        :returns: the snap's processed revision
        """
        data = {
            "name": snap_name,
            "series": constants.DEFAULT_SERIES,
            "updown_id": upload_id,
            "binary_filesize": snap_file_size,
            "source_uploaded": False,
        }
        if built_at is not None:
            data["built_at"] = built_at
        if channels is not None:
            data["channels"] = channels

        response = self.request(
            "POST",
            self._base_url + "/dev/api/snap-push/",
            json=data,
            headers={
                "Accept": "application/json",
            },
        )

        status_url = response.json()["status_details_url"]
        while True:
            response = self.request("GET", status_url)
            status = response.json()
            human_status = _HUMAN_STATUS.get(status["code"], status["code"])
            emit.progress(f"Status: {human_status}")

            if status.get("processed", False):
                if status.get("errors"):
                    error_messages = [
                        e["message"] for e in status["errors"]
                        if "message" in e
                    ]
                    error_string = "\n".join(
                        [f"- {e}" for e in error_messages])
                    raise errors.SnapcraftError(
                        f"Issues while processing snap:\n{error_string}")
                break

            time.sleep(_POLL_DELAY)

        return status["revision"]
Example #28
0
def setup_assets(
    project: Project, *, assets_dir: Path, project_dir: Path, prime_dir: Path
) -> None:
    """Copy assets to the appropriate locations in the snap filesystem.

    :param project: The snap project file.
    :param assets_dir: The directory containing snap project assets.
    :param project_dir: The project root directory.
    :param prime_dir: The directory containing the content to be snapped.
    """
    meta_dir = prime_dir / "meta"
    gui_dir = meta_dir / "gui"
    gui_dir.mkdir(parents=True, exist_ok=True)

    _write_snap_directory(assets_dir=assets_dir, prime_dir=prime_dir, meta_dir=meta_dir)

    if project.hooks:
        for hook_name, hook in project.hooks.items():
            if hook.command_chain:
                _validate_command_chain(
                    hook.command_chain, name=f"hook {hook_name!r}", prime_dir=prime_dir
                )
            ensure_hook(meta_dir / "hooks" / hook_name)

    if project.type == "gadget":
        gadget_yaml = project_dir / "gadget.yaml"
        if not gadget_yaml.exists():
            raise errors.SnapcraftError("gadget.yaml is required for gadget snaps")
        _copy_file(gadget_yaml, meta_dir / "gadget.yaml")

    if project.type == "kernel":
        kernel_yaml = project_dir / "kernel.yaml"
        if kernel_yaml.exists():
            _copy_file(kernel_yaml, meta_dir / "kernel.yaml")

    if not project.apps:
        return

    icon_path = _finalize_icon(
        project.icon, assets_dir=assets_dir, gui_dir=gui_dir, prime_dir=prime_dir
    )
    relative_icon_path: Optional[str] = None

    if icon_path is not None:
        if prime_dir in icon_path.parents:
            icon_path = icon_path.relative_to(prime_dir)
        relative_icon_path = str(icon_path)

    emit.debug(f"relative icon path: {relative_icon_path!r}")

    for app_name, app in project.apps.items():
        _validate_command_chain(
            app.command_chain, name=f"app {app_name!r}", prime_dir=prime_dir
        )

        if app.desktop:
            desktop_file = DesktopFile(
                snap_name=project.name,
                app_name=app_name,
                filename=app.desktop,
                prime_dir=prime_dir,
            )
            desktop_file.write(gui_dir=gui_dir, icon_path=relative_icon_path)