Source code for craft_parts.sources.sources

# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2015-2021,2024 Canonical Ltd.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Source handle utilities.

Unless the part plugin overrides this behaviour, a part can use these
'source' keys in its definition. They tell Craft Parts where to pull source
code for that part, and how to unpack it if necessary.

  - source: url-or-path

    A URL or path to some source tree to build. It can be local
    ('./src/foo') or remote ('https://foo.org/...'), and can refer to a
    directory tree or a tarball or a revision control repository
    ('git:...').

  - source-type: git, tar, deb, rpm, or zip

    In some cases the source string is not enough to identify the version
    control system or compression algorithm. The source-type key can tell
    Craft Parts exactly how to treat that content.

  - source-checksum: <algorithm>/<digest>

    Craft Parts will use the digest specified to verify the integrity of the
    source. The source-type needs to be a file (tar, zip, deb or rpm) and
    the algorithm either md5, sha1, sha224, sha256, sha384, sha512, sha3_256,
    sha3_384 or sha3_512.

  - source-depth: <integer>

    By default clones or branches with full history, specifying a depth
    will truncate the history to the specified number of commits.

  - source-branch: <branch-name>

    Craft Parts will checkout a specific branch from the source tree. This
    only works on multi-branch repositories from git and hg (mercurial).

  - source-commit: <commit>

    Craft Parts will checkout the specific commit from the source tree revision
    control system.

  - source-tag: <tag>

    Craft Parts will checkout the specific tag from the source tree revision
    control system.

  - source-subdir: path

    When building, Snapcraft will set the working directory to be this
    subdirectory within the source.

  - source-submodules: <list-of-submodules>

    Configure which submodules to fetch from the source tree.
    If source-submodules in defined and empty, no submodules are fetched.
    If source-submodules is not defined, all submodules are fetched (default
    behavior).

Note that plugins might well define their own semantics for the 'source'
keywords, because they handle specific build systems, and many languages
have their own built-in packaging systems (think CPAN, PyPI, NPM). In those
cases you want to refer to the documentation for the specific plugin.
"""

import os
import re
from pathlib import Path
from typing import TYPE_CHECKING

import pydantic_core

from craft_parts.dirs import ProjectDirs

from . import errors
from .base import BaseSourceModel, SourceHandler
from .deb_source import DebSource
from .file_source import FileSource
from .git_source import GitSource
from .local_source import LocalSource
from .rpm_source import RpmSource
from .sevenzip_source import SevenzipSource
from .snap_source import SnapSource
from .tar_source import TarSource
from .zip_source import ZipSource

if TYPE_CHECKING:
    from craft_parts.parts import Part

SourceHandlerType = type[SourceHandler]

_MANDATORY_SOURCES: dict[str, SourceHandlerType] = {
    "local": LocalSource,
    "tar": TarSource,
    "git": GitSource,
    "snap": SnapSource,
    "zip": ZipSource,
    "deb": DebSource,
    "file": FileSource,
    "rpm": RpmSource,
    "7z": SevenzipSource,
}

_SOURCES: dict[str, SourceHandlerType] = {}


def _get_type_name_from_model(model: type[BaseSourceModel]) -> str:
    source_type = model.model_fields["source_type"]
    if (default := source_type.get_default()) is not pydantic_core.PydanticUndefined:
        return str(default)
    if source_type.annotation is None:
        raise TypeError("Source type needs an annotation.")
    return str(source_type.annotation.__args__[0])


[docs] def register(source: SourceHandlerType, /) -> None: """Register source handlers. :param source: a SourceHandler class to register. :raises: ValueError if the source handler overrides a built-in source type. """ source_name = _get_type_name_from_model(source.source_model) if source_name in _MANDATORY_SOURCES: raise ValueError(f"Built-in source types cannot be overridden: {source_name!r}") _SOURCES[source_name] = source
[docs] def unregister(source: str, /) -> None: """Unregister a source handler by name.""" if source in _MANDATORY_SOURCES: raise ValueError(f"Built-in source types cannot be unregistered: {source!r}") try: del _SOURCES[source] except KeyError: raise ValueError(f"Source type not registered: {source!r}") from None
[docs] def get_source_handler( cache_dir: Path, part: "Part", project_dirs: ProjectDirs, ignore_patterns: list[str] | None = None, ) -> SourceHandler | None: """Return the appropriate handler for the given source. :param application_name: The name of the application using Craft Parts. :param part: The part to get a source handler for. :param project_dirs: The project's work directories. """ source_handler = None if part.spec.source: handler_class = _get_source_handler_class( part.spec.source, source_type=part.spec.source_type, ) source_handler = handler_class( cache_dir=cache_dir, source=part.spec.source, part_src_dir=part.part_src_dir, source_checksum=part.spec.source_checksum, source_branch=part.spec.source_branch, source_tag=part.spec.source_tag, source_depth=part.spec.source_depth, source_commit=part.spec.source_commit, source_submodules=part.spec.source_submodules, project_dirs=project_dirs, ignore_patterns=ignore_patterns, ) return source_handler
def _get_source_handler_class( source: str, *, source_type: str = "" ) -> SourceHandlerType: """Return the appropriate handler class for the given source. :param source: The source specification. :param source_type: The source type to use. If not specified, the type will be inferred from the source specification. """ if not source_type: source_type = get_source_type_from_uri(source) if source_type in _MANDATORY_SOURCES: return _MANDATORY_SOURCES[source_type] if source_type in _SOURCES: return _SOURCES[source_type] raise errors.InvalidSourceType(source, source_type=source_type)
[docs] def get_source_type_from_uri( source: str, ignore_errors: bool = False # noqa: FBT001, FBT002 ) -> str: """Return the source type based on the given source URI. :param source: The source specification. :param ignore_errors: Don't raise InvalidSourceType if the source type could not be determined. :returns: a string matching the registered source type. :raise InvalidSourceType: If the source type is unknown. """ for source_cls in (*_MANDATORY_SOURCES.values(), *_SOURCES.values()): source_model = source_cls.source_model if source_model.pattern and re.search(source_model.pattern, source): return _get_type_name_from_model(source_model) # Special case for the "local" source for backwards compatibility. if os.path.isdir(source): return "local" if ignore_errors: return "" raise errors.InvalidSourceType(source)