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