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