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