aboutsummaryrefslogtreecommitdiff
blob: a5b984ce2e0f1ebd5288806434fbc2f32694c4c3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
"""
A package is a set of files, situated in a root directory.
A package can be installed into a repository.
A package is supposed to be created by a package source from a set of files.
A package is supposed to know, where it came from.
"""

from os import path, walk, makedirs
from shutil import copy2

import subprocess

from patch import PatchSet

from pomu.util.fs import strip_prefix
from pomu.util.misc import list_add
from pomu.util.result import Result

class Package():
    def __init__(self, name, root, backend=None, category=None, version=None, slot='0', d_path=None, files=None, filemap=None, patches=[]):
        """
        Parameters:
            backend - specific source module object/class
            name - name of the package
            root - root path of the repository (if applicable)
            d_path - a subdirectory of the root path, which would be sourced recursively.
                could be a relative or an absolute path
            files - a set of files to build a package from
            filemap - a mapping from destination files to files in the filesystem
            category, version, slot - self-descriptive
        """
        self.backend = backend
        self.name = name
        self.root = root
        self.category = category
        self.version = version
        self.slot = slot
        self.filemap = {}
        self.patches = patches
        if d_path is None and files is None and filemap is None:
            self.d_path = None
            self.read_path(self.root)
        elif d_path:
            self.d_path = self.strip_root(d_path)
            self.read_path(path.join(self.root, self.d_path))
        elif files:
            for f in files:
                dst = self.strip_root(f)
                self.filemap[dst] = path.join(self.root, dst)
        elif filemap:
            self.filemap = filemap
        else:
            raise ValueError('You should specify either d_path, files or filemap')

    @property
    def files(self):
        res = []
        for k in self.filemap:
            res.append(path.split(k))
        return res

    @property
    def ebuild_path(self):
        return path.join(self.category, self.name, '{}-{}.ebuild'.format(self.name, self.version))

    def strip_root(self, d_path):
        """Strip the root component of d_path"""
        # the path should be either relative, or a child of root
        if d_path.startswith('/'):
            if path.commonprefix([d_path, self.root]) != self.root:
                raise ValueError('Path should be a subdirectory of root')
            return strip_prefix(strip_prefix(d_path, self.root), '/')
        return d_path

    def read_path(self, d_path):
        """Recursively add files from a subtree (specified by d_path)"""
        for wd, dirs, files in walk(d_path):
            wd = self.strip_root(wd)
            self.filemap.update({path.join(wd, f): path.join(self.root, wd, f) for f in files})

    def merge_into(self, dst):
        """Merges contents of the package into a specified directory (dst)"""
        for trg, src in self.filemap.items():
            wd, filename = path.split(trg)
            dest = path.join(dst, wd)
            try:
                makedirs(dest, exist_ok=True)
                if isinstance(src, bytes):
                    with open(path.join(dest, filename), 'wb') as f:
                        f.write(src)
                else:
                    copy2(src, dest)
            except PermissionError:
                return Result.Err('You do not have enough permissions')
        return Result.Ok().and_(self.apply_patches())

    def patch(self, patch):
        if patch:
            list_add(self.patches, patch)

    def apply_patches(self, revert=False):
        """Applies a sequence of patches at the root (after merging)"""
        ps = PatchSet()
        for p in self.patches:
            ps.parse(open(p, 'r'))
        for patch in ps:
            if '.ebuild' in patch.target:
                patch.source = self.ebuild_path
                patch.target = self.ebuild_path
            elif '/files/' in patch.target:
                comps = patch.target.split('/')
                comps = [self.category, self.name] + comps[comps.index('files'):]
                patch.target = '/'.join(comps)
                if not patch.source.split('/'[-2:] == ['dev', 'null']):
                    patch.source = '/'.join(comps)
            else:
                pass

        if revert:
            ps.revert(root=self.root)
        else:
            ps.apply(root=self.root)
        return Result.Ok()

    def gen_manifests(self, dst):
        """
        Generate manifests for the installed package (in the dst directory).
        TODO: use portage APIs instead of calling repoman.
        """
        dirs = [wd for wd, f in self.files if f.endswith('.ebuild')]
        dirs = list(set(dirs))
        res = []
        for d_ in dirs:
            d = path.join(dst, d_)
            ret = subprocess.run(['repoman', 'manifest'],
                    stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                    cwd=d)
            if ret.returncode != 0:
                return Result.Err('Failed to generate manifest at ' + d)
            if path.exists(path.join(d, 'Manifest')):
                res.append(path.join(d, 'Manifest'))
        return Result.Ok(res)


    def __str__(self):
        s = ''
        if self.category:
            s = self.category + '/'
        s += self.name
        if self.version:
            s += '-' + self.version
        if self.slot != '0':
            s += self.slot
        return s

class PatchList():
    """A class to represent a sequence of patches (can be merged in the repo)"""
    def __init__(self, category, name, version, patches, slot='0'):
        self.category = category
        self.name = name
        self.version = version
        self.slot = slot
        self.patches = patches