xref: /OK3568_Linux_fs/yocto/poky/meta/lib/oe/patch.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
1*4882a593Smuzhiyun#
2*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only
3*4882a593Smuzhiyun#
4*4882a593Smuzhiyun
5*4882a593Smuzhiyunimport oe.path
6*4882a593Smuzhiyunimport oe.types
7*4882a593Smuzhiyunimport subprocess
8*4882a593Smuzhiyun
9*4882a593Smuzhiyunclass NotFoundError(bb.BBHandledException):
10*4882a593Smuzhiyun    def __init__(self, path):
11*4882a593Smuzhiyun        self.path = path
12*4882a593Smuzhiyun
13*4882a593Smuzhiyun    def __str__(self):
14*4882a593Smuzhiyun        return "Error: %s not found." % self.path
15*4882a593Smuzhiyun
16*4882a593Smuzhiyunclass CmdError(bb.BBHandledException):
17*4882a593Smuzhiyun    def __init__(self, command, exitstatus, output):
18*4882a593Smuzhiyun        self.command = command
19*4882a593Smuzhiyun        self.status = exitstatus
20*4882a593Smuzhiyun        self.output = output
21*4882a593Smuzhiyun
22*4882a593Smuzhiyun    def __str__(self):
23*4882a593Smuzhiyun        return "Command Error: '%s' exited with %d  Output:\n%s" % \
24*4882a593Smuzhiyun                (self.command, self.status, self.output)
25*4882a593Smuzhiyun
26*4882a593Smuzhiyun
27*4882a593Smuzhiyundef runcmd(args, dir = None):
28*4882a593Smuzhiyun    import pipes
29*4882a593Smuzhiyun
30*4882a593Smuzhiyun    if dir:
31*4882a593Smuzhiyun        olddir = os.path.abspath(os.curdir)
32*4882a593Smuzhiyun        if not os.path.exists(dir):
33*4882a593Smuzhiyun            raise NotFoundError(dir)
34*4882a593Smuzhiyun        os.chdir(dir)
35*4882a593Smuzhiyun        # print("cwd: %s -> %s" % (olddir, dir))
36*4882a593Smuzhiyun
37*4882a593Smuzhiyun    try:
38*4882a593Smuzhiyun        args = [ pipes.quote(str(arg)) for arg in args ]
39*4882a593Smuzhiyun        cmd = " ".join(args)
40*4882a593Smuzhiyun        # print("cmd: %s" % cmd)
41*4882a593Smuzhiyun        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
42*4882a593Smuzhiyun        stdout, stderr = proc.communicate()
43*4882a593Smuzhiyun        stdout = stdout.decode('utf-8')
44*4882a593Smuzhiyun        stderr = stderr.decode('utf-8')
45*4882a593Smuzhiyun        exitstatus = proc.returncode
46*4882a593Smuzhiyun        if exitstatus != 0:
47*4882a593Smuzhiyun            raise CmdError(cmd, exitstatus >> 8, "stdout: %s\nstderr: %s" % (stdout, stderr))
48*4882a593Smuzhiyun        if " fuzz " in stdout and "Hunk " in stdout:
49*4882a593Smuzhiyun            # Drop patch fuzz info with header and footer to log file so
50*4882a593Smuzhiyun            # insane.bbclass can handle to throw error/warning
51*4882a593Smuzhiyun            bb.note("--- Patch fuzz start ---\n%s\n--- Patch fuzz end ---" % format(stdout))
52*4882a593Smuzhiyun
53*4882a593Smuzhiyun        return stdout
54*4882a593Smuzhiyun
55*4882a593Smuzhiyun    finally:
56*4882a593Smuzhiyun        if dir:
57*4882a593Smuzhiyun            os.chdir(olddir)
58*4882a593Smuzhiyun
59*4882a593Smuzhiyun
60*4882a593Smuzhiyunclass PatchError(Exception):
61*4882a593Smuzhiyun    def __init__(self, msg):
62*4882a593Smuzhiyun        self.msg = msg
63*4882a593Smuzhiyun
64*4882a593Smuzhiyun    def __str__(self):
65*4882a593Smuzhiyun        return "Patch Error: %s" % self.msg
66*4882a593Smuzhiyun
67*4882a593Smuzhiyunclass PatchSet(object):
68*4882a593Smuzhiyun    defaults = {
69*4882a593Smuzhiyun        "strippath": 1
70*4882a593Smuzhiyun    }
71*4882a593Smuzhiyun
72*4882a593Smuzhiyun    def __init__(self, dir, d):
73*4882a593Smuzhiyun        self.dir = dir
74*4882a593Smuzhiyun        self.d = d
75*4882a593Smuzhiyun        self.patches = []
76*4882a593Smuzhiyun        self._current = None
77*4882a593Smuzhiyun
78*4882a593Smuzhiyun    def current(self):
79*4882a593Smuzhiyun        return self._current
80*4882a593Smuzhiyun
81*4882a593Smuzhiyun    def Clean(self):
82*4882a593Smuzhiyun        """
83*4882a593Smuzhiyun        Clean out the patch set.  Generally includes unapplying all
84*4882a593Smuzhiyun        patches and wiping out all associated metadata.
85*4882a593Smuzhiyun        """
86*4882a593Smuzhiyun        raise NotImplementedError()
87*4882a593Smuzhiyun
88*4882a593Smuzhiyun    def Import(self, patch, force):
89*4882a593Smuzhiyun        if not patch.get("file"):
90*4882a593Smuzhiyun            if not patch.get("remote"):
91*4882a593Smuzhiyun                raise PatchError("Patch file must be specified in patch import.")
92*4882a593Smuzhiyun            else:
93*4882a593Smuzhiyun                patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
94*4882a593Smuzhiyun
95*4882a593Smuzhiyun        for param in PatchSet.defaults:
96*4882a593Smuzhiyun            if not patch.get(param):
97*4882a593Smuzhiyun                patch[param] = PatchSet.defaults[param]
98*4882a593Smuzhiyun
99*4882a593Smuzhiyun        if patch.get("remote"):
100*4882a593Smuzhiyun            patch["file"] = self.d.expand(bb.fetch2.localpath(patch["remote"], self.d))
101*4882a593Smuzhiyun
102*4882a593Smuzhiyun        patch["filemd5"] = bb.utils.md5_file(patch["file"])
103*4882a593Smuzhiyun
104*4882a593Smuzhiyun    def Push(self, force):
105*4882a593Smuzhiyun        raise NotImplementedError()
106*4882a593Smuzhiyun
107*4882a593Smuzhiyun    def Pop(self, force):
108*4882a593Smuzhiyun        raise NotImplementedError()
109*4882a593Smuzhiyun
110*4882a593Smuzhiyun    def Refresh(self, remote = None, all = None):
111*4882a593Smuzhiyun        raise NotImplementedError()
112*4882a593Smuzhiyun
113*4882a593Smuzhiyun    @staticmethod
114*4882a593Smuzhiyun    def getPatchedFiles(patchfile, striplevel, srcdir=None):
115*4882a593Smuzhiyun        """
116*4882a593Smuzhiyun        Read a patch file and determine which files it will modify.
117*4882a593Smuzhiyun        Params:
118*4882a593Smuzhiyun            patchfile: the patch file to read
119*4882a593Smuzhiyun            striplevel: the strip level at which the patch is going to be applied
120*4882a593Smuzhiyun            srcdir: optional path to join onto the patched file paths
121*4882a593Smuzhiyun        Returns:
122*4882a593Smuzhiyun            A list of tuples of file path and change mode ('A' for add,
123*4882a593Smuzhiyun            'D' for delete or 'M' for modify)
124*4882a593Smuzhiyun        """
125*4882a593Smuzhiyun
126*4882a593Smuzhiyun        def patchedpath(patchline):
127*4882a593Smuzhiyun            filepth = patchline.split()[1]
128*4882a593Smuzhiyun            if filepth.endswith('/dev/null'):
129*4882a593Smuzhiyun                return '/dev/null'
130*4882a593Smuzhiyun            filesplit = filepth.split(os.sep)
131*4882a593Smuzhiyun            if striplevel > len(filesplit):
132*4882a593Smuzhiyun                bb.error('Patch %s has invalid strip level %d' % (patchfile, striplevel))
133*4882a593Smuzhiyun                return None
134*4882a593Smuzhiyun            return os.sep.join(filesplit[striplevel:])
135*4882a593Smuzhiyun
136*4882a593Smuzhiyun        for encoding in ['utf-8', 'latin-1']:
137*4882a593Smuzhiyun            try:
138*4882a593Smuzhiyun                copiedmode = False
139*4882a593Smuzhiyun                filelist = []
140*4882a593Smuzhiyun                with open(patchfile) as f:
141*4882a593Smuzhiyun                    for line in f:
142*4882a593Smuzhiyun                        if line.startswith('--- '):
143*4882a593Smuzhiyun                            patchpth = patchedpath(line)
144*4882a593Smuzhiyun                            if not patchpth:
145*4882a593Smuzhiyun                                break
146*4882a593Smuzhiyun                            if copiedmode:
147*4882a593Smuzhiyun                                addedfile = patchpth
148*4882a593Smuzhiyun                            else:
149*4882a593Smuzhiyun                                removedfile = patchpth
150*4882a593Smuzhiyun                        elif line.startswith('+++ '):
151*4882a593Smuzhiyun                            addedfile = patchedpath(line)
152*4882a593Smuzhiyun                            if not addedfile:
153*4882a593Smuzhiyun                                break
154*4882a593Smuzhiyun                        elif line.startswith('*** '):
155*4882a593Smuzhiyun                            copiedmode = True
156*4882a593Smuzhiyun                            removedfile = patchedpath(line)
157*4882a593Smuzhiyun                            if not removedfile:
158*4882a593Smuzhiyun                                break
159*4882a593Smuzhiyun                        else:
160*4882a593Smuzhiyun                            removedfile = None
161*4882a593Smuzhiyun                            addedfile = None
162*4882a593Smuzhiyun
163*4882a593Smuzhiyun                        if addedfile and removedfile:
164*4882a593Smuzhiyun                            if removedfile == '/dev/null':
165*4882a593Smuzhiyun                                mode = 'A'
166*4882a593Smuzhiyun                            elif addedfile == '/dev/null':
167*4882a593Smuzhiyun                                mode = 'D'
168*4882a593Smuzhiyun                            else:
169*4882a593Smuzhiyun                                mode = 'M'
170*4882a593Smuzhiyun                            if srcdir:
171*4882a593Smuzhiyun                                fullpath = os.path.abspath(os.path.join(srcdir, addedfile))
172*4882a593Smuzhiyun                            else:
173*4882a593Smuzhiyun                                fullpath = addedfile
174*4882a593Smuzhiyun                            filelist.append((fullpath, mode))
175*4882a593Smuzhiyun            except UnicodeDecodeError:
176*4882a593Smuzhiyun                continue
177*4882a593Smuzhiyun            break
178*4882a593Smuzhiyun        else:
179*4882a593Smuzhiyun            raise PatchError('Unable to decode %s' % patchfile)
180*4882a593Smuzhiyun
181*4882a593Smuzhiyun        return filelist
182*4882a593Smuzhiyun
183*4882a593Smuzhiyun
184*4882a593Smuzhiyunclass PatchTree(PatchSet):
185*4882a593Smuzhiyun    def __init__(self, dir, d):
186*4882a593Smuzhiyun        PatchSet.__init__(self, dir, d)
187*4882a593Smuzhiyun        self.patchdir = os.path.join(self.dir, 'patches')
188*4882a593Smuzhiyun        self.seriespath = os.path.join(self.dir, 'patches', 'series')
189*4882a593Smuzhiyun        bb.utils.mkdirhier(self.patchdir)
190*4882a593Smuzhiyun
191*4882a593Smuzhiyun    def _appendPatchFile(self, patch, strippath):
192*4882a593Smuzhiyun        with open(self.seriespath, 'a') as f:
193*4882a593Smuzhiyun            f.write(os.path.basename(patch) + "," + strippath + "\n")
194*4882a593Smuzhiyun        shellcmd = ["cat", patch, ">" , self.patchdir + "/" + os.path.basename(patch)]
195*4882a593Smuzhiyun        runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
196*4882a593Smuzhiyun
197*4882a593Smuzhiyun    def _removePatch(self, p):
198*4882a593Smuzhiyun        patch = {}
199*4882a593Smuzhiyun        patch['file'] = p.split(",")[0]
200*4882a593Smuzhiyun        patch['strippath'] = p.split(",")[1]
201*4882a593Smuzhiyun        self._applypatch(patch, False, True)
202*4882a593Smuzhiyun
203*4882a593Smuzhiyun    def _removePatchFile(self, all = False):
204*4882a593Smuzhiyun        if not os.path.exists(self.seriespath):
205*4882a593Smuzhiyun            return
206*4882a593Smuzhiyun        with open(self.seriespath, 'r+') as f:
207*4882a593Smuzhiyun            patches = f.readlines()
208*4882a593Smuzhiyun        if all:
209*4882a593Smuzhiyun            for p in reversed(patches):
210*4882a593Smuzhiyun                self._removePatch(os.path.join(self.patchdir, p.strip()))
211*4882a593Smuzhiyun            patches = []
212*4882a593Smuzhiyun        else:
213*4882a593Smuzhiyun            self._removePatch(os.path.join(self.patchdir, patches[-1].strip()))
214*4882a593Smuzhiyun            patches.pop()
215*4882a593Smuzhiyun        with open(self.seriespath, 'w') as f:
216*4882a593Smuzhiyun            for p in patches:
217*4882a593Smuzhiyun                f.write(p)
218*4882a593Smuzhiyun
219*4882a593Smuzhiyun    def Import(self, patch, force = None):
220*4882a593Smuzhiyun        """"""
221*4882a593Smuzhiyun        PatchSet.Import(self, patch, force)
222*4882a593Smuzhiyun
223*4882a593Smuzhiyun        if self._current is not None:
224*4882a593Smuzhiyun            i = self._current + 1
225*4882a593Smuzhiyun        else:
226*4882a593Smuzhiyun            i = 0
227*4882a593Smuzhiyun        self.patches.insert(i, patch)
228*4882a593Smuzhiyun
229*4882a593Smuzhiyun    def _applypatch(self, patch, force = False, reverse = False, run = True):
230*4882a593Smuzhiyun        shellcmd = ["cat", patch['file'], "|", "patch", "--no-backup-if-mismatch", "-p", patch['strippath']]
231*4882a593Smuzhiyun        if reverse:
232*4882a593Smuzhiyun            shellcmd.append('-R')
233*4882a593Smuzhiyun
234*4882a593Smuzhiyun        if not run:
235*4882a593Smuzhiyun            return "sh" + "-c" + " ".join(shellcmd)
236*4882a593Smuzhiyun
237*4882a593Smuzhiyun        if not force:
238*4882a593Smuzhiyun            shellcmd.append('--dry-run')
239*4882a593Smuzhiyun
240*4882a593Smuzhiyun        try:
241*4882a593Smuzhiyun            output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
242*4882a593Smuzhiyun
243*4882a593Smuzhiyun            if force:
244*4882a593Smuzhiyun                return
245*4882a593Smuzhiyun
246*4882a593Smuzhiyun            shellcmd.pop(len(shellcmd) - 1)
247*4882a593Smuzhiyun            output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
248*4882a593Smuzhiyun        except CmdError as err:
249*4882a593Smuzhiyun            raise bb.BBHandledException("Applying '%s' failed:\n%s" %
250*4882a593Smuzhiyun                                        (os.path.basename(patch['file']), err.output))
251*4882a593Smuzhiyun
252*4882a593Smuzhiyun        if not reverse:
253*4882a593Smuzhiyun            self._appendPatchFile(patch['file'], patch['strippath'])
254*4882a593Smuzhiyun
255*4882a593Smuzhiyun        return output
256*4882a593Smuzhiyun
257*4882a593Smuzhiyun    def Push(self, force = False, all = False, run = True):
258*4882a593Smuzhiyun        bb.note("self._current is %s" % self._current)
259*4882a593Smuzhiyun        bb.note("patches is %s" % self.patches)
260*4882a593Smuzhiyun        if all:
261*4882a593Smuzhiyun            for i in self.patches:
262*4882a593Smuzhiyun                bb.note("applying patch %s" % i)
263*4882a593Smuzhiyun                self._applypatch(i, force)
264*4882a593Smuzhiyun                self._current = i
265*4882a593Smuzhiyun        else:
266*4882a593Smuzhiyun            if self._current is not None:
267*4882a593Smuzhiyun                next = self._current + 1
268*4882a593Smuzhiyun            else:
269*4882a593Smuzhiyun                next = 0
270*4882a593Smuzhiyun
271*4882a593Smuzhiyun            bb.note("applying patch %s" % self.patches[next])
272*4882a593Smuzhiyun            ret = self._applypatch(self.patches[next], force)
273*4882a593Smuzhiyun
274*4882a593Smuzhiyun            self._current = next
275*4882a593Smuzhiyun            return ret
276*4882a593Smuzhiyun
277*4882a593Smuzhiyun    def Pop(self, force = None, all = None):
278*4882a593Smuzhiyun        if all:
279*4882a593Smuzhiyun            self._removePatchFile(True)
280*4882a593Smuzhiyun            self._current = None
281*4882a593Smuzhiyun        else:
282*4882a593Smuzhiyun            self._removePatchFile(False)
283*4882a593Smuzhiyun
284*4882a593Smuzhiyun        if self._current == 0:
285*4882a593Smuzhiyun            self._current = None
286*4882a593Smuzhiyun
287*4882a593Smuzhiyun        if self._current is not None:
288*4882a593Smuzhiyun            self._current = self._current - 1
289*4882a593Smuzhiyun
290*4882a593Smuzhiyun    def Clean(self):
291*4882a593Smuzhiyun        """"""
292*4882a593Smuzhiyun        self.Pop(all=True)
293*4882a593Smuzhiyun
294*4882a593Smuzhiyunclass GitApplyTree(PatchTree):
295*4882a593Smuzhiyun    patch_line_prefix = '%% original patch'
296*4882a593Smuzhiyun    ignore_commit_prefix = '%% ignore'
297*4882a593Smuzhiyun
298*4882a593Smuzhiyun    def __init__(self, dir, d):
299*4882a593Smuzhiyun        PatchTree.__init__(self, dir, d)
300*4882a593Smuzhiyun        self.commituser = d.getVar('PATCH_GIT_USER_NAME')
301*4882a593Smuzhiyun        self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
302*4882a593Smuzhiyun        if not self._isInitialized(d):
303*4882a593Smuzhiyun            self._initRepo()
304*4882a593Smuzhiyun
305*4882a593Smuzhiyun    def _isInitialized(self, d):
306*4882a593Smuzhiyun        cmd = "git rev-parse --show-toplevel"
307*4882a593Smuzhiyun        try:
308*4882a593Smuzhiyun            output = runcmd(cmd.split(), self.dir).strip()
309*4882a593Smuzhiyun        except CmdError as err:
310*4882a593Smuzhiyun            ## runcmd returned non-zero which most likely means 128
311*4882a593Smuzhiyun            ## Not a git directory
312*4882a593Smuzhiyun            return False
313*4882a593Smuzhiyun        ## Make sure repo is in builddir to not break top-level git repos, or under workdir
314*4882a593Smuzhiyun        return os.path.samefile(output, self.dir) or oe.path.is_path_parent(d.getVar('WORKDIR'), output)
315*4882a593Smuzhiyun
316*4882a593Smuzhiyun    def _initRepo(self):
317*4882a593Smuzhiyun        runcmd("git init".split(), self.dir)
318*4882a593Smuzhiyun        runcmd("git add .".split(), self.dir)
319*4882a593Smuzhiyun        runcmd("git commit -a --allow-empty -m bitbake_patching_started".split(), self.dir)
320*4882a593Smuzhiyun
321*4882a593Smuzhiyun    @staticmethod
322*4882a593Smuzhiyun    def extractPatchHeader(patchfile):
323*4882a593Smuzhiyun        """
324*4882a593Smuzhiyun        Extract just the header lines from the top of a patch file
325*4882a593Smuzhiyun        """
326*4882a593Smuzhiyun        for encoding in ['utf-8', 'latin-1']:
327*4882a593Smuzhiyun            lines = []
328*4882a593Smuzhiyun            try:
329*4882a593Smuzhiyun                with open(patchfile, 'r', encoding=encoding) as f:
330*4882a593Smuzhiyun                    for line in f:
331*4882a593Smuzhiyun                        if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'):
332*4882a593Smuzhiyun                            break
333*4882a593Smuzhiyun                        lines.append(line)
334*4882a593Smuzhiyun            except UnicodeDecodeError:
335*4882a593Smuzhiyun                continue
336*4882a593Smuzhiyun            break
337*4882a593Smuzhiyun        else:
338*4882a593Smuzhiyun            raise PatchError('Unable to find a character encoding to decode %s' % patchfile)
339*4882a593Smuzhiyun        return lines
340*4882a593Smuzhiyun
341*4882a593Smuzhiyun    @staticmethod
342*4882a593Smuzhiyun    def decodeAuthor(line):
343*4882a593Smuzhiyun        from email.header import decode_header
344*4882a593Smuzhiyun        authorval = line.split(':', 1)[1].strip().replace('"', '')
345*4882a593Smuzhiyun        result =  decode_header(authorval)[0][0]
346*4882a593Smuzhiyun        if hasattr(result, 'decode'):
347*4882a593Smuzhiyun            result = result.decode('utf-8')
348*4882a593Smuzhiyun        return result
349*4882a593Smuzhiyun
350*4882a593Smuzhiyun    @staticmethod
351*4882a593Smuzhiyun    def interpretPatchHeader(headerlines):
352*4882a593Smuzhiyun        import re
353*4882a593Smuzhiyun        author_re = re.compile(r'[\S ]+ <\S+@\S+\.\S+>')
354*4882a593Smuzhiyun        from_commit_re = re.compile(r'^From [a-z0-9]{40} .*')
355*4882a593Smuzhiyun        outlines = []
356*4882a593Smuzhiyun        author = None
357*4882a593Smuzhiyun        date = None
358*4882a593Smuzhiyun        subject = None
359*4882a593Smuzhiyun        for line in headerlines:
360*4882a593Smuzhiyun            if line.startswith('Subject: '):
361*4882a593Smuzhiyun                subject = line.split(':', 1)[1]
362*4882a593Smuzhiyun                # Remove any [PATCH][oe-core] etc.
363*4882a593Smuzhiyun                subject = re.sub(r'\[.+?\]\s*', '', subject)
364*4882a593Smuzhiyun                continue
365*4882a593Smuzhiyun            elif line.startswith('From: ') or line.startswith('Author: '):
366*4882a593Smuzhiyun                authorval = GitApplyTree.decodeAuthor(line)
367*4882a593Smuzhiyun                # git is fussy about author formatting i.e. it must be Name <email@domain>
368*4882a593Smuzhiyun                if author_re.match(authorval):
369*4882a593Smuzhiyun                    author = authorval
370*4882a593Smuzhiyun                    continue
371*4882a593Smuzhiyun            elif line.startswith('Date: '):
372*4882a593Smuzhiyun                if date is None:
373*4882a593Smuzhiyun                    dateval = line.split(':', 1)[1].strip()
374*4882a593Smuzhiyun                    # Very crude check for date format, since git will blow up if it's not in the right
375*4882a593Smuzhiyun                    # format. Without e.g. a python-dateutils dependency we can't do a whole lot more
376*4882a593Smuzhiyun                    if len(dateval) > 12:
377*4882a593Smuzhiyun                        date = dateval
378*4882a593Smuzhiyun                continue
379*4882a593Smuzhiyun            elif not author and line.lower().startswith('signed-off-by: '):
380*4882a593Smuzhiyun                authorval = GitApplyTree.decodeAuthor(line)
381*4882a593Smuzhiyun                # git is fussy about author formatting i.e. it must be Name <email@domain>
382*4882a593Smuzhiyun                if author_re.match(authorval):
383*4882a593Smuzhiyun                    author = authorval
384*4882a593Smuzhiyun            elif from_commit_re.match(line):
385*4882a593Smuzhiyun                # We don't want the From <commit> line - if it's present it will break rebasing
386*4882a593Smuzhiyun                continue
387*4882a593Smuzhiyun            outlines.append(line)
388*4882a593Smuzhiyun
389*4882a593Smuzhiyun        if not subject:
390*4882a593Smuzhiyun            firstline = None
391*4882a593Smuzhiyun            for line in headerlines:
392*4882a593Smuzhiyun                line = line.strip()
393*4882a593Smuzhiyun                if firstline:
394*4882a593Smuzhiyun                    if line:
395*4882a593Smuzhiyun                        # Second line is not blank, the first line probably isn't usable
396*4882a593Smuzhiyun                        firstline = None
397*4882a593Smuzhiyun                    break
398*4882a593Smuzhiyun                elif line:
399*4882a593Smuzhiyun                    firstline = line
400*4882a593Smuzhiyun            if firstline and not firstline.startswith(('#', 'Index:', 'Upstream-Status:')) and len(firstline) < 100:
401*4882a593Smuzhiyun                subject = firstline
402*4882a593Smuzhiyun
403*4882a593Smuzhiyun        return outlines, author, date, subject
404*4882a593Smuzhiyun
405*4882a593Smuzhiyun    @staticmethod
406*4882a593Smuzhiyun    def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None):
407*4882a593Smuzhiyun        if d:
408*4882a593Smuzhiyun            commituser = d.getVar('PATCH_GIT_USER_NAME')
409*4882a593Smuzhiyun            commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
410*4882a593Smuzhiyun        if commituser:
411*4882a593Smuzhiyun            cmd += ['-c', 'user.name="%s"' % commituser]
412*4882a593Smuzhiyun        if commitemail:
413*4882a593Smuzhiyun            cmd += ['-c', 'user.email="%s"' % commitemail]
414*4882a593Smuzhiyun
415*4882a593Smuzhiyun    @staticmethod
416*4882a593Smuzhiyun    def prepareCommit(patchfile, commituser=None, commitemail=None):
417*4882a593Smuzhiyun        """
418*4882a593Smuzhiyun        Prepare a git commit command line based on the header from a patch file
419*4882a593Smuzhiyun        (typically this is useful for patches that cannot be applied with "git am" due to formatting)
420*4882a593Smuzhiyun        """
421*4882a593Smuzhiyun        import tempfile
422*4882a593Smuzhiyun        # Process patch header and extract useful information
423*4882a593Smuzhiyun        lines = GitApplyTree.extractPatchHeader(patchfile)
424*4882a593Smuzhiyun        outlines, author, date, subject = GitApplyTree.interpretPatchHeader(lines)
425*4882a593Smuzhiyun        if not author or not subject or not date:
426*4882a593Smuzhiyun            try:
427*4882a593Smuzhiyun                shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile]
428*4882a593Smuzhiyun                out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.dirname(patchfile))
429*4882a593Smuzhiyun            except CmdError:
430*4882a593Smuzhiyun                out = None
431*4882a593Smuzhiyun            if out:
432*4882a593Smuzhiyun                _, newauthor, newdate, newsubject = GitApplyTree.interpretPatchHeader(out.splitlines())
433*4882a593Smuzhiyun                if not author:
434*4882a593Smuzhiyun                    # If we're setting the author then the date should be set as well
435*4882a593Smuzhiyun                    author = newauthor
436*4882a593Smuzhiyun                    date = newdate
437*4882a593Smuzhiyun                elif not date:
438*4882a593Smuzhiyun                    # If we don't do this we'll get the current date, at least this will be closer
439*4882a593Smuzhiyun                    date = newdate
440*4882a593Smuzhiyun                if not subject:
441*4882a593Smuzhiyun                    subject = newsubject
442*4882a593Smuzhiyun        if subject and not (outlines and outlines[0].strip() == subject):
443*4882a593Smuzhiyun            outlines.insert(0, '%s\n\n' % subject.strip())
444*4882a593Smuzhiyun
445*4882a593Smuzhiyun        # Write out commit message to a file
446*4882a593Smuzhiyun        with tempfile.NamedTemporaryFile('w', delete=False) as tf:
447*4882a593Smuzhiyun            tmpfile = tf.name
448*4882a593Smuzhiyun            for line in outlines:
449*4882a593Smuzhiyun                tf.write(line)
450*4882a593Smuzhiyun        # Prepare git command
451*4882a593Smuzhiyun        cmd = ["git"]
452*4882a593Smuzhiyun        GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail)
453*4882a593Smuzhiyun        cmd += ["commit", "-F", tmpfile]
454*4882a593Smuzhiyun        # git doesn't like plain email addresses as authors
455*4882a593Smuzhiyun        if author and '<' in author:
456*4882a593Smuzhiyun            cmd.append('--author="%s"' % author)
457*4882a593Smuzhiyun        if date:
458*4882a593Smuzhiyun            cmd.append('--date="%s"' % date)
459*4882a593Smuzhiyun        return (tmpfile, cmd)
460*4882a593Smuzhiyun
461*4882a593Smuzhiyun    @staticmethod
462*4882a593Smuzhiyun    def extractPatches(tree, startcommit, outdir, paths=None):
463*4882a593Smuzhiyun        import tempfile
464*4882a593Smuzhiyun        import shutil
465*4882a593Smuzhiyun        tempdir = tempfile.mkdtemp(prefix='oepatch')
466*4882a593Smuzhiyun        try:
467*4882a593Smuzhiyun            shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", startcommit, "-o", tempdir]
468*4882a593Smuzhiyun            if paths:
469*4882a593Smuzhiyun                shellcmd.append('--')
470*4882a593Smuzhiyun                shellcmd.extend(paths)
471*4882a593Smuzhiyun            out = runcmd(["sh", "-c", " ".join(shellcmd)], tree)
472*4882a593Smuzhiyun            if out:
473*4882a593Smuzhiyun                for srcfile in out.split():
474*4882a593Smuzhiyun                    for encoding in ['utf-8', 'latin-1']:
475*4882a593Smuzhiyun                        patchlines = []
476*4882a593Smuzhiyun                        outfile = None
477*4882a593Smuzhiyun                        try:
478*4882a593Smuzhiyun                            with open(srcfile, 'r', encoding=encoding) as f:
479*4882a593Smuzhiyun                                for line in f:
480*4882a593Smuzhiyun                                    if line.startswith(GitApplyTree.patch_line_prefix):
481*4882a593Smuzhiyun                                        outfile = line.split()[-1].strip()
482*4882a593Smuzhiyun                                        continue
483*4882a593Smuzhiyun                                    if line.startswith(GitApplyTree.ignore_commit_prefix):
484*4882a593Smuzhiyun                                        continue
485*4882a593Smuzhiyun                                    patchlines.append(line)
486*4882a593Smuzhiyun                        except UnicodeDecodeError:
487*4882a593Smuzhiyun                            continue
488*4882a593Smuzhiyun                        break
489*4882a593Smuzhiyun                    else:
490*4882a593Smuzhiyun                        raise PatchError('Unable to find a character encoding to decode %s' % srcfile)
491*4882a593Smuzhiyun
492*4882a593Smuzhiyun                    if not outfile:
493*4882a593Smuzhiyun                        outfile = os.path.basename(srcfile)
494*4882a593Smuzhiyun                    with open(os.path.join(outdir, outfile), 'w') as of:
495*4882a593Smuzhiyun                        for line in patchlines:
496*4882a593Smuzhiyun                            of.write(line)
497*4882a593Smuzhiyun        finally:
498*4882a593Smuzhiyun            shutil.rmtree(tempdir)
499*4882a593Smuzhiyun
500*4882a593Smuzhiyun    def _applypatch(self, patch, force = False, reverse = False, run = True):
501*4882a593Smuzhiyun        import shutil
502*4882a593Smuzhiyun
503*4882a593Smuzhiyun        def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True):
504*4882a593Smuzhiyun            if reverse:
505*4882a593Smuzhiyun                shellcmd.append('-R')
506*4882a593Smuzhiyun
507*4882a593Smuzhiyun            shellcmd.append(patch['file'])
508*4882a593Smuzhiyun
509*4882a593Smuzhiyun            if not run:
510*4882a593Smuzhiyun                return "sh" + "-c" + " ".join(shellcmd)
511*4882a593Smuzhiyun
512*4882a593Smuzhiyun            return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
513*4882a593Smuzhiyun
514*4882a593Smuzhiyun        # Add hooks which add a pointer to the original patch file name in the commit message
515*4882a593Smuzhiyun        reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
516*4882a593Smuzhiyun        if not reporoot:
517*4882a593Smuzhiyun            raise Exception("Cannot get repository root for directory %s" % self.dir)
518*4882a593Smuzhiyun        hooks_dir = os.path.join(reporoot, '.git', 'hooks')
519*4882a593Smuzhiyun        hooks_dir_backup = hooks_dir + '.devtool-orig'
520*4882a593Smuzhiyun        if os.path.lexists(hooks_dir_backup):
521*4882a593Smuzhiyun            raise Exception("Git hooks backup directory already exists: %s" % hooks_dir_backup)
522*4882a593Smuzhiyun        if os.path.lexists(hooks_dir):
523*4882a593Smuzhiyun            shutil.move(hooks_dir, hooks_dir_backup)
524*4882a593Smuzhiyun        os.mkdir(hooks_dir)
525*4882a593Smuzhiyun        commithook = os.path.join(hooks_dir, 'commit-msg')
526*4882a593Smuzhiyun        applyhook = os.path.join(hooks_dir, 'applypatch-msg')
527*4882a593Smuzhiyun        with open(commithook, 'w') as f:
528*4882a593Smuzhiyun            # NOTE: the formatting here is significant; if you change it you'll also need to
529*4882a593Smuzhiyun            # change other places which read it back
530*4882a593Smuzhiyun            f.write('echo "\n%s: $PATCHFILE" >> $1' % GitApplyTree.patch_line_prefix)
531*4882a593Smuzhiyun        os.chmod(commithook, 0o755)
532*4882a593Smuzhiyun        shutil.copy2(commithook, applyhook)
533*4882a593Smuzhiyun        try:
534*4882a593Smuzhiyun            patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file'])
535*4882a593Smuzhiyun            try:
536*4882a593Smuzhiyun                shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot]
537*4882a593Smuzhiyun                self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail)
538*4882a593Smuzhiyun                shellcmd += ["am", "-3", "--keep-cr", "--no-scissors", "-p%s" % patch['strippath']]
539*4882a593Smuzhiyun                return _applypatchhelper(shellcmd, patch, force, reverse, run)
540*4882a593Smuzhiyun            except CmdError:
541*4882a593Smuzhiyun                # Need to abort the git am, or we'll still be within it at the end
542*4882a593Smuzhiyun                try:
543*4882a593Smuzhiyun                    shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"]
544*4882a593Smuzhiyun                    runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
545*4882a593Smuzhiyun                except CmdError:
546*4882a593Smuzhiyun                    pass
547*4882a593Smuzhiyun                # git am won't always clean up after itself, sadly, so...
548*4882a593Smuzhiyun                shellcmd = ["git", "--work-tree=%s" % reporoot, "reset", "--hard", "HEAD"]
549*4882a593Smuzhiyun                runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
550*4882a593Smuzhiyun                # Also need to take care of any stray untracked files
551*4882a593Smuzhiyun                shellcmd = ["git", "--work-tree=%s" % reporoot, "clean", "-f"]
552*4882a593Smuzhiyun                runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
553*4882a593Smuzhiyun
554*4882a593Smuzhiyun                # Fall back to git apply
555*4882a593Smuzhiyun                shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']]
556*4882a593Smuzhiyun                try:
557*4882a593Smuzhiyun                    output = _applypatchhelper(shellcmd, patch, force, reverse, run)
558*4882a593Smuzhiyun                except CmdError:
559*4882a593Smuzhiyun                    # Fall back to patch
560*4882a593Smuzhiyun                    output = PatchTree._applypatch(self, patch, force, reverse, run)
561*4882a593Smuzhiyun                # Add all files
562*4882a593Smuzhiyun                shellcmd = ["git", "add", "-f", "-A", "."]
563*4882a593Smuzhiyun                output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
564*4882a593Smuzhiyun                # Exclude the patches directory
565*4882a593Smuzhiyun                shellcmd = ["git", "reset", "HEAD", self.patchdir]
566*4882a593Smuzhiyun                output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
567*4882a593Smuzhiyun                # Commit the result
568*4882a593Smuzhiyun                (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail)
569*4882a593Smuzhiyun                try:
570*4882a593Smuzhiyun                    shellcmd.insert(0, patchfilevar)
571*4882a593Smuzhiyun                    output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
572*4882a593Smuzhiyun                finally:
573*4882a593Smuzhiyun                    os.remove(tmpfile)
574*4882a593Smuzhiyun                return output
575*4882a593Smuzhiyun        finally:
576*4882a593Smuzhiyun            shutil.rmtree(hooks_dir)
577*4882a593Smuzhiyun            if os.path.lexists(hooks_dir_backup):
578*4882a593Smuzhiyun                shutil.move(hooks_dir_backup, hooks_dir)
579*4882a593Smuzhiyun
580*4882a593Smuzhiyun
581*4882a593Smuzhiyunclass QuiltTree(PatchSet):
582*4882a593Smuzhiyun    def _runcmd(self, args, run = True):
583*4882a593Smuzhiyun        quiltrc = self.d.getVar('QUILTRCFILE')
584*4882a593Smuzhiyun        if not run:
585*4882a593Smuzhiyun            return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
586*4882a593Smuzhiyun        runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
587*4882a593Smuzhiyun
588*4882a593Smuzhiyun    def _quiltpatchpath(self, file):
589*4882a593Smuzhiyun        return os.path.join(self.dir, "patches", os.path.basename(file))
590*4882a593Smuzhiyun
591*4882a593Smuzhiyun
592*4882a593Smuzhiyun    def __init__(self, dir, d):
593*4882a593Smuzhiyun        PatchSet.__init__(self, dir, d)
594*4882a593Smuzhiyun        self.initialized = False
595*4882a593Smuzhiyun        p = os.path.join(self.dir, 'patches')
596*4882a593Smuzhiyun        if not os.path.exists(p):
597*4882a593Smuzhiyun            os.makedirs(p)
598*4882a593Smuzhiyun
599*4882a593Smuzhiyun    def Clean(self):
600*4882a593Smuzhiyun        try:
601*4882a593Smuzhiyun            # make sure that patches/series file exists before quilt pop to keep quilt-0.67 happy
602*4882a593Smuzhiyun            open(os.path.join(self.dir, "patches","series"), 'a').close()
603*4882a593Smuzhiyun            self._runcmd(["pop", "-a", "-f"])
604*4882a593Smuzhiyun            oe.path.remove(os.path.join(self.dir, "patches","series"))
605*4882a593Smuzhiyun        except Exception:
606*4882a593Smuzhiyun            pass
607*4882a593Smuzhiyun        self.initialized = True
608*4882a593Smuzhiyun
609*4882a593Smuzhiyun    def InitFromDir(self):
610*4882a593Smuzhiyun        # read series -> self.patches
611*4882a593Smuzhiyun        seriespath = os.path.join(self.dir, 'patches', 'series')
612*4882a593Smuzhiyun        if not os.path.exists(self.dir):
613*4882a593Smuzhiyun            raise NotFoundError(self.dir)
614*4882a593Smuzhiyun        if os.path.exists(seriespath):
615*4882a593Smuzhiyun            with open(seriespath, 'r') as f:
616*4882a593Smuzhiyun                for line in f.readlines():
617*4882a593Smuzhiyun                    patch = {}
618*4882a593Smuzhiyun                    parts = line.strip().split()
619*4882a593Smuzhiyun                    patch["quiltfile"] = self._quiltpatchpath(parts[0])
620*4882a593Smuzhiyun                    patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
621*4882a593Smuzhiyun                    if len(parts) > 1:
622*4882a593Smuzhiyun                        patch["strippath"] = parts[1][2:]
623*4882a593Smuzhiyun                    self.patches.append(patch)
624*4882a593Smuzhiyun
625*4882a593Smuzhiyun            # determine which patches are applied -> self._current
626*4882a593Smuzhiyun            try:
627*4882a593Smuzhiyun                output = runcmd(["quilt", "applied"], self.dir)
628*4882a593Smuzhiyun            except CmdError:
629*4882a593Smuzhiyun                import sys
630*4882a593Smuzhiyun                if sys.exc_value.output.strip() == "No patches applied":
631*4882a593Smuzhiyun                    return
632*4882a593Smuzhiyun                else:
633*4882a593Smuzhiyun                    raise
634*4882a593Smuzhiyun            output = [val for val in output.split('\n') if not val.startswith('#')]
635*4882a593Smuzhiyun            for patch in self.patches:
636*4882a593Smuzhiyun                if os.path.basename(patch["quiltfile"]) == output[-1]:
637*4882a593Smuzhiyun                    self._current = self.patches.index(patch)
638*4882a593Smuzhiyun        self.initialized = True
639*4882a593Smuzhiyun
640*4882a593Smuzhiyun    def Import(self, patch, force = None):
641*4882a593Smuzhiyun        if not self.initialized:
642*4882a593Smuzhiyun            self.InitFromDir()
643*4882a593Smuzhiyun        PatchSet.Import(self, patch, force)
644*4882a593Smuzhiyun        oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
645*4882a593Smuzhiyun        with open(os.path.join(self.dir, "patches", "series"), "a") as f:
646*4882a593Smuzhiyun            f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n")
647*4882a593Smuzhiyun        patch["quiltfile"] = self._quiltpatchpath(patch["file"])
648*4882a593Smuzhiyun        patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
649*4882a593Smuzhiyun
650*4882a593Smuzhiyun        # TODO: determine if the file being imported:
651*4882a593Smuzhiyun        #      1) is already imported, and is the same
652*4882a593Smuzhiyun        #      2) is already imported, but differs
653*4882a593Smuzhiyun
654*4882a593Smuzhiyun        self.patches.insert(self._current or 0, patch)
655*4882a593Smuzhiyun
656*4882a593Smuzhiyun
657*4882a593Smuzhiyun    def Push(self, force = False, all = False, run = True):
658*4882a593Smuzhiyun        # quilt push [-f]
659*4882a593Smuzhiyun
660*4882a593Smuzhiyun        args = ["push"]
661*4882a593Smuzhiyun        if force:
662*4882a593Smuzhiyun            args.append("-f")
663*4882a593Smuzhiyun        if all:
664*4882a593Smuzhiyun            args.append("-a")
665*4882a593Smuzhiyun        if not run:
666*4882a593Smuzhiyun            return self._runcmd(args, run)
667*4882a593Smuzhiyun
668*4882a593Smuzhiyun        self._runcmd(args)
669*4882a593Smuzhiyun
670*4882a593Smuzhiyun        if self._current is not None:
671*4882a593Smuzhiyun            self._current = self._current + 1
672*4882a593Smuzhiyun        else:
673*4882a593Smuzhiyun            self._current = 0
674*4882a593Smuzhiyun
675*4882a593Smuzhiyun    def Pop(self, force = None, all = None):
676*4882a593Smuzhiyun        # quilt pop [-f]
677*4882a593Smuzhiyun        args = ["pop"]
678*4882a593Smuzhiyun        if force:
679*4882a593Smuzhiyun            args.append("-f")
680*4882a593Smuzhiyun        if all:
681*4882a593Smuzhiyun            args.append("-a")
682*4882a593Smuzhiyun
683*4882a593Smuzhiyun        self._runcmd(args)
684*4882a593Smuzhiyun
685*4882a593Smuzhiyun        if self._current == 0:
686*4882a593Smuzhiyun            self._current = None
687*4882a593Smuzhiyun
688*4882a593Smuzhiyun        if self._current is not None:
689*4882a593Smuzhiyun            self._current = self._current - 1
690*4882a593Smuzhiyun
691*4882a593Smuzhiyun    def Refresh(self, **kwargs):
692*4882a593Smuzhiyun        if kwargs.get("remote"):
693*4882a593Smuzhiyun            patch = self.patches[kwargs["patch"]]
694*4882a593Smuzhiyun            if not patch:
695*4882a593Smuzhiyun                raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
696*4882a593Smuzhiyun            (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
697*4882a593Smuzhiyun            if type == "file":
698*4882a593Smuzhiyun                import shutil
699*4882a593Smuzhiyun                if not patch.get("file") and patch.get("remote"):
700*4882a593Smuzhiyun                    patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
701*4882a593Smuzhiyun
702*4882a593Smuzhiyun                shutil.copyfile(patch["quiltfile"], patch["file"])
703*4882a593Smuzhiyun            else:
704*4882a593Smuzhiyun                raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
705*4882a593Smuzhiyun        else:
706*4882a593Smuzhiyun            # quilt refresh
707*4882a593Smuzhiyun            args = ["refresh"]
708*4882a593Smuzhiyun            if kwargs.get("quiltfile"):
709*4882a593Smuzhiyun                args.append(os.path.basename(kwargs["quiltfile"]))
710*4882a593Smuzhiyun            elif kwargs.get("patch"):
711*4882a593Smuzhiyun                args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
712*4882a593Smuzhiyun            self._runcmd(args)
713*4882a593Smuzhiyun
714*4882a593Smuzhiyunclass Resolver(object):
715*4882a593Smuzhiyun    def __init__(self, patchset, terminal):
716*4882a593Smuzhiyun        raise NotImplementedError()
717*4882a593Smuzhiyun
718*4882a593Smuzhiyun    def Resolve(self):
719*4882a593Smuzhiyun        raise NotImplementedError()
720*4882a593Smuzhiyun
721*4882a593Smuzhiyun    def Revert(self):
722*4882a593Smuzhiyun        raise NotImplementedError()
723*4882a593Smuzhiyun
724*4882a593Smuzhiyun    def Finalize(self):
725*4882a593Smuzhiyun        raise NotImplementedError()
726*4882a593Smuzhiyun
727*4882a593Smuzhiyunclass NOOPResolver(Resolver):
728*4882a593Smuzhiyun    def __init__(self, patchset, terminal):
729*4882a593Smuzhiyun        self.patchset = patchset
730*4882a593Smuzhiyun        self.terminal = terminal
731*4882a593Smuzhiyun
732*4882a593Smuzhiyun    def Resolve(self):
733*4882a593Smuzhiyun        olddir = os.path.abspath(os.curdir)
734*4882a593Smuzhiyun        os.chdir(self.patchset.dir)
735*4882a593Smuzhiyun        try:
736*4882a593Smuzhiyun            self.patchset.Push()
737*4882a593Smuzhiyun        except Exception:
738*4882a593Smuzhiyun            import sys
739*4882a593Smuzhiyun            os.chdir(olddir)
740*4882a593Smuzhiyun            raise
741*4882a593Smuzhiyun
742*4882a593Smuzhiyun# Patch resolver which relies on the user doing all the work involved in the
743*4882a593Smuzhiyun# resolution, with the exception of refreshing the remote copy of the patch
744*4882a593Smuzhiyun# files (the urls).
745*4882a593Smuzhiyunclass UserResolver(Resolver):
746*4882a593Smuzhiyun    def __init__(self, patchset, terminal):
747*4882a593Smuzhiyun        self.patchset = patchset
748*4882a593Smuzhiyun        self.terminal = terminal
749*4882a593Smuzhiyun
750*4882a593Smuzhiyun    # Force a push in the patchset, then drop to a shell for the user to
751*4882a593Smuzhiyun    # resolve any rejected hunks
752*4882a593Smuzhiyun    def Resolve(self):
753*4882a593Smuzhiyun        olddir = os.path.abspath(os.curdir)
754*4882a593Smuzhiyun        os.chdir(self.patchset.dir)
755*4882a593Smuzhiyun        try:
756*4882a593Smuzhiyun            self.patchset.Push(False)
757*4882a593Smuzhiyun        except CmdError as v:
758*4882a593Smuzhiyun            # Patch application failed
759*4882a593Smuzhiyun            patchcmd = self.patchset.Push(True, False, False)
760*4882a593Smuzhiyun
761*4882a593Smuzhiyun            t = self.patchset.d.getVar('T')
762*4882a593Smuzhiyun            if not t:
763*4882a593Smuzhiyun                bb.msg.fatal("Build", "T not set")
764*4882a593Smuzhiyun            bb.utils.mkdirhier(t)
765*4882a593Smuzhiyun            import random
766*4882a593Smuzhiyun            rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
767*4882a593Smuzhiyun            with open(rcfile, "w") as f:
768*4882a593Smuzhiyun                f.write("echo '*** Manual patch resolution mode ***'\n")
769*4882a593Smuzhiyun                f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
770*4882a593Smuzhiyun                f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
771*4882a593Smuzhiyun                f.write("echo ''\n")
772*4882a593Smuzhiyun                f.write(" ".join(patchcmd) + "\n")
773*4882a593Smuzhiyun            os.chmod(rcfile, 0o775)
774*4882a593Smuzhiyun
775*4882a593Smuzhiyun            self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
776*4882a593Smuzhiyun
777*4882a593Smuzhiyun            # Construct a new PatchSet after the user's changes, compare the
778*4882a593Smuzhiyun            # sets, checking patches for modifications, and doing a remote
779*4882a593Smuzhiyun            # refresh on each.
780*4882a593Smuzhiyun            oldpatchset = self.patchset
781*4882a593Smuzhiyun            self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
782*4882a593Smuzhiyun
783*4882a593Smuzhiyun            for patch in self.patchset.patches:
784*4882a593Smuzhiyun                oldpatch = None
785*4882a593Smuzhiyun                for opatch in oldpatchset.patches:
786*4882a593Smuzhiyun                    if opatch["quiltfile"] == patch["quiltfile"]:
787*4882a593Smuzhiyun                        oldpatch = opatch
788*4882a593Smuzhiyun
789*4882a593Smuzhiyun                if oldpatch:
790*4882a593Smuzhiyun                    patch["remote"] = oldpatch["remote"]
791*4882a593Smuzhiyun                    if patch["quiltfile"] == oldpatch["quiltfile"]:
792*4882a593Smuzhiyun                        if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
793*4882a593Smuzhiyun                            bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
794*4882a593Smuzhiyun                            # user change?  remote refresh
795*4882a593Smuzhiyun                            self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
796*4882a593Smuzhiyun                        else:
797*4882a593Smuzhiyun                            # User did not fix the problem.  Abort.
798*4882a593Smuzhiyun                            raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
799*4882a593Smuzhiyun        except Exception:
800*4882a593Smuzhiyun            os.chdir(olddir)
801*4882a593Smuzhiyun            raise
802*4882a593Smuzhiyun        os.chdir(olddir)
803*4882a593Smuzhiyun
804*4882a593Smuzhiyun
805*4882a593Smuzhiyundef patch_path(url, fetch, workdir, expand=True):
806*4882a593Smuzhiyun    """Return the local path of a patch, or return nothing if this isn't a patch"""
807*4882a593Smuzhiyun
808*4882a593Smuzhiyun    local = fetch.localpath(url)
809*4882a593Smuzhiyun    if os.path.isdir(local):
810*4882a593Smuzhiyun        return
811*4882a593Smuzhiyun    base, ext = os.path.splitext(os.path.basename(local))
812*4882a593Smuzhiyun    if ext in ('.gz', '.bz2', '.xz', '.Z'):
813*4882a593Smuzhiyun        if expand:
814*4882a593Smuzhiyun            local = os.path.join(workdir, base)
815*4882a593Smuzhiyun        ext = os.path.splitext(base)[1]
816*4882a593Smuzhiyun
817*4882a593Smuzhiyun    urldata = fetch.ud[url]
818*4882a593Smuzhiyun    if "apply" in urldata.parm:
819*4882a593Smuzhiyun        apply = oe.types.boolean(urldata.parm["apply"])
820*4882a593Smuzhiyun        if not apply:
821*4882a593Smuzhiyun            return
822*4882a593Smuzhiyun    elif ext not in (".diff", ".patch"):
823*4882a593Smuzhiyun        return
824*4882a593Smuzhiyun
825*4882a593Smuzhiyun    return local
826*4882a593Smuzhiyun
827*4882a593Smuzhiyundef src_patches(d, all=False, expand=True):
828*4882a593Smuzhiyun    workdir = d.getVar('WORKDIR')
829*4882a593Smuzhiyun    fetch = bb.fetch2.Fetch([], d)
830*4882a593Smuzhiyun    patches = []
831*4882a593Smuzhiyun    sources = []
832*4882a593Smuzhiyun    for url in fetch.urls:
833*4882a593Smuzhiyun        local = patch_path(url, fetch, workdir, expand)
834*4882a593Smuzhiyun        if not local:
835*4882a593Smuzhiyun            if all:
836*4882a593Smuzhiyun                local = fetch.localpath(url)
837*4882a593Smuzhiyun                sources.append(local)
838*4882a593Smuzhiyun            continue
839*4882a593Smuzhiyun
840*4882a593Smuzhiyun        urldata = fetch.ud[url]
841*4882a593Smuzhiyun        parm = urldata.parm
842*4882a593Smuzhiyun        patchname = parm.get('pname') or os.path.basename(local)
843*4882a593Smuzhiyun
844*4882a593Smuzhiyun        apply, reason = should_apply(parm, d)
845*4882a593Smuzhiyun        if not apply:
846*4882a593Smuzhiyun            if reason:
847*4882a593Smuzhiyun                bb.note("Patch %s %s" % (patchname, reason))
848*4882a593Smuzhiyun            continue
849*4882a593Smuzhiyun
850*4882a593Smuzhiyun        patchparm = {'patchname': patchname}
851*4882a593Smuzhiyun        if "striplevel" in parm:
852*4882a593Smuzhiyun            striplevel = parm["striplevel"]
853*4882a593Smuzhiyun        elif "pnum" in parm:
854*4882a593Smuzhiyun            #bb.msg.warn(None, "Deprecated usage of 'pnum' url parameter in '%s', please use 'striplevel'" % url)
855*4882a593Smuzhiyun            striplevel = parm["pnum"]
856*4882a593Smuzhiyun        else:
857*4882a593Smuzhiyun            striplevel = '1'
858*4882a593Smuzhiyun        patchparm['striplevel'] = striplevel
859*4882a593Smuzhiyun
860*4882a593Smuzhiyun        patchdir = parm.get('patchdir')
861*4882a593Smuzhiyun        if patchdir:
862*4882a593Smuzhiyun            patchparm['patchdir'] = patchdir
863*4882a593Smuzhiyun
864*4882a593Smuzhiyun        localurl = bb.fetch.encodeurl(('file', '', local, '', '', patchparm))
865*4882a593Smuzhiyun        patches.append(localurl)
866*4882a593Smuzhiyun
867*4882a593Smuzhiyun    if all:
868*4882a593Smuzhiyun        return sources
869*4882a593Smuzhiyun
870*4882a593Smuzhiyun    return patches
871*4882a593Smuzhiyun
872*4882a593Smuzhiyun
873*4882a593Smuzhiyundef should_apply(parm, d):
874*4882a593Smuzhiyun    import bb.utils
875*4882a593Smuzhiyun    if "mindate" in parm or "maxdate" in parm:
876*4882a593Smuzhiyun        pn = d.getVar('PN')
877*4882a593Smuzhiyun        srcdate = d.getVar('SRCDATE_%s' % pn)
878*4882a593Smuzhiyun        if not srcdate:
879*4882a593Smuzhiyun            srcdate = d.getVar('SRCDATE')
880*4882a593Smuzhiyun
881*4882a593Smuzhiyun        if srcdate == "now":
882*4882a593Smuzhiyun            srcdate = d.getVar('DATE')
883*4882a593Smuzhiyun
884*4882a593Smuzhiyun        if "maxdate" in parm and parm["maxdate"] < srcdate:
885*4882a593Smuzhiyun            return False, 'is outdated'
886*4882a593Smuzhiyun
887*4882a593Smuzhiyun        if "mindate" in parm and parm["mindate"] > srcdate:
888*4882a593Smuzhiyun            return False, 'is predated'
889*4882a593Smuzhiyun
890*4882a593Smuzhiyun
891*4882a593Smuzhiyun    if "minrev" in parm:
892*4882a593Smuzhiyun        srcrev = d.getVar('SRCREV')
893*4882a593Smuzhiyun        if srcrev and srcrev < parm["minrev"]:
894*4882a593Smuzhiyun            return False, 'applies to later revisions'
895*4882a593Smuzhiyun
896*4882a593Smuzhiyun    if "maxrev" in parm:
897*4882a593Smuzhiyun        srcrev = d.getVar('SRCREV')
898*4882a593Smuzhiyun        if srcrev and srcrev > parm["maxrev"]:
899*4882a593Smuzhiyun            return False, 'applies to earlier revisions'
900*4882a593Smuzhiyun
901*4882a593Smuzhiyun    if "rev" in parm:
902*4882a593Smuzhiyun        srcrev = d.getVar('SRCREV')
903*4882a593Smuzhiyun        if srcrev and parm["rev"] not in srcrev:
904*4882a593Smuzhiyun            return False, "doesn't apply to revision"
905*4882a593Smuzhiyun
906*4882a593Smuzhiyun    if "notrev" in parm:
907*4882a593Smuzhiyun        srcrev = d.getVar('SRCREV')
908*4882a593Smuzhiyun        if srcrev and parm["notrev"] in srcrev:
909*4882a593Smuzhiyun            return False, "doesn't apply to revision"
910*4882a593Smuzhiyun
911*4882a593Smuzhiyun    if "maxver" in parm:
912*4882a593Smuzhiyun        pv = d.getVar('PV')
913*4882a593Smuzhiyun        if bb.utils.vercmp_string_op(pv, parm["maxver"], ">"):
914*4882a593Smuzhiyun            return False, "applies to earlier version"
915*4882a593Smuzhiyun
916*4882a593Smuzhiyun    if "minver" in parm:
917*4882a593Smuzhiyun        pv = d.getVar('PV')
918*4882a593Smuzhiyun        if bb.utils.vercmp_string_op(pv, parm["minver"], "<"):
919*4882a593Smuzhiyun            return False, "applies to later version"
920*4882a593Smuzhiyun
921*4882a593Smuzhiyun    return True, None
922*4882a593Smuzhiyun
923