conan-io/conan

View on GitHub
conans/model/manifest.py

Summary

Maintainability
B
6 hrs
Test Coverage
import os

from conans.errors import ConanException
from conans.paths import CONAN_MANIFEST, EXPORT_SOURCES_TGZ_NAME, EXPORT_TGZ_NAME, PACKAGE_TGZ_NAME
from conans.util.dates import timestamp_now, timestamp_to_str
from conans.util.env_reader import get_env
from conans.util.files import load, md5, md5sum, save, walk


def discarded_file(filename, keep_python):
    """
    # The __conan pattern is to be prepared for the future, in case we want to manage our
    own files that shouldn't be uploaded
    """
    if not keep_python:
        return (filename == ".DS_Store" or filename.endswith(".pyc") or
                filename.endswith(".pyo") or filename == "__pycache__" or
                filename.startswith("__conan"))
    else:
        return filename == ".DS_Store"


def gather_files(folder):
    file_dict = {}
    symlinks = {}
    keep_python = get_env("CONAN_KEEP_PYTHON_FILES", False)
    for root, dirs, files in walk(folder):
        if not keep_python:
            dirs[:] = [d for d in dirs if d != "__pycache__"]  # Avoid recursing pycache
        for d in dirs:
            abs_path = os.path.join(root, d)
            if os.path.islink(abs_path):
                rel_path = abs_path[len(folder) + 1:].replace("\\", "/")
                symlinks[rel_path] = os.readlink(abs_path)
        for f in files:
            if discarded_file(f, keep_python):
                continue
            abs_path = os.path.join(root, f)
            rel_path = abs_path[len(folder) + 1:].replace("\\", "/")
            if os.path.exists(abs_path):
                file_dict[rel_path] = abs_path
            else:
                if not get_env("CONAN_SKIP_BROKEN_SYMLINKS_CHECK", False):
                    raise ConanException("The file is a broken symlink, verify that "
                                         "you are packaging the needed destination files: '%s'."
                                         "You can skip this check adjusting the "
                                         "'general.skip_broken_symlinks_check' at the conan.conf "
                                         "file."
                                         % abs_path)
    return file_dict, symlinks


class FileTreeManifest(object):

    def __init__(self, the_time, file_sums):
        """file_sums is a dict with filepaths and md5's: {filepath/to/file.txt: md5}"""
        self.time = the_time
        self.file_sums = file_sums

    def files(self):
        return self.file_sums.keys()

    @property
    def summary_hash(self):
        s = ["%s: %s" % (f, fmd5) for f, fmd5 in sorted(self.file_sums.items())]
        s.append("")
        return md5("\n".join(s))

    @property
    def time_str(self):
        return timestamp_to_str(self.time)

    @staticmethod
    def loads(text):
        """ parses a string representation, generated with __repr__
        """
        tokens = text.split("\n")
        the_time = int(tokens[0])
        file_sums = {}
        keep_python = get_env("CONAN_KEEP_PYTHON_FILES", False)
        for md5line in tokens[1:]:
            if md5line:
                filename, file_md5 = md5line.rsplit(": ", 1)
                # FIXME: This is weird, it should never happen, maybe remove?
                if not discarded_file(filename, keep_python):
                    file_sums[filename] = file_md5
        return FileTreeManifest(the_time, file_sums)

    @staticmethod
    def load(folder):
        text = load(os.path.join(folder, CONAN_MANIFEST))
        return FileTreeManifest.loads(text)

    def __repr__(self):
        # Used for serialization and saving it to disk
        ret = ["%s" % self.time]
        for file_path, file_md5 in sorted(self.file_sums.items()):
            ret.append("%s: %s" % (file_path, file_md5))
        ret.append("")
        content = "\n".join(ret)
        return content

    def __str__(self):
        """  Used for displaying the manifest in user readable format in Uploader, when the server
        manifest is newer than the cache one (and not force)
        """
        ret = ["Time: %s" % timestamp_to_str(self.time)]
        for file_path, file_md5 in sorted(self.file_sums.items()):
            ret.append("%s, MD5: %s" % (file_path, file_md5))
        ret.append("")
        content = "\n".join(ret)
        return content

    def save(self, folder, filename=CONAN_MANIFEST):
        path = os.path.join(folder, filename)
        save(path, repr(self))

    @classmethod
    def create(cls, folder, exports_sources_folder=None):
        """ Walks a folder and create a FileTreeManifest for it, reading file contents
        from disk, and capturing current time
        """
        files, _ = gather_files(folder)
        for f in (PACKAGE_TGZ_NAME, EXPORT_TGZ_NAME, CONAN_MANIFEST, EXPORT_SOURCES_TGZ_NAME):
            files.pop(f, None)

        file_dict = {}
        for name, filepath in files.items():
            file_dict[name] = md5sum(filepath)

        if exports_sources_folder:
            export_files, _ = gather_files(exports_sources_folder)
            for name, filepath in export_files.items():
                file_dict["export_source/%s" % name] = md5sum(filepath)

        date = timestamp_now()

        return cls(date, file_dict)

    def __eq__(self, other):
        """ Two manifests are equal if file_sums
        """
        return self.file_sums == other.file_sums

    def __ne__(self, other):
        return not self.__eq__(other)

    def difference(self, other):
        result = {}
        for f, h in self.file_sums.items():
            h2 = other.file_sums.get(f)
            if h != h2:
                result[f] = h, h2
        for f, h in other.file_sums.items():
            h2 = self.file_sums.get(f)
            if h != h2:
                result[f] = h2, h
        return result