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