summaryrefslogtreecommitdiffstats
path: root/meta/lib/oe/patch.py
diff options
context:
space:
mode:
Diffstat (limited to 'meta/lib/oe/patch.py')
-rw-r--r--meta/lib/oe/patch.py447
1 files changed, 447 insertions, 0 deletions
diff --git a/meta/lib/oe/patch.py b/meta/lib/oe/patch.py
new file mode 100644
index 0000000..b085c9d
--- /dev/null
+++ b/meta/lib/oe/patch.py
@@ -0,0 +1,447 @@
1import oe.path
2
3class NotFoundError(bb.BBHandledException):
4 def __init__(self, path):
5 self.path = path
6
7 def __str__(self):
8 return "Error: %s not found." % self.path
9
10class CmdError(bb.BBHandledException):
11 def __init__(self, exitstatus, output):
12 self.status = exitstatus
13 self.output = output
14
15 def __str__(self):
16 return "Command Error: exit status: %d Output:\n%s" % (self.status, self.output)
17
18
19def runcmd(args, dir = None):
20 import pipes
21
22 if dir:
23 olddir = os.path.abspath(os.curdir)
24 if not os.path.exists(dir):
25 raise NotFoundError(dir)
26 os.chdir(dir)
27 # print("cwd: %s -> %s" % (olddir, dir))
28
29 try:
30 args = [ pipes.quote(str(arg)) for arg in args ]
31 cmd = " ".join(args)
32 # print("cmd: %s" % cmd)
33 (exitstatus, output) = oe.utils.getstatusoutput(cmd)
34 if exitstatus != 0:
35 raise CmdError(exitstatus >> 8, output)
36 return output
37
38 finally:
39 if dir:
40 os.chdir(olddir)
41
42class PatchError(Exception):
43 def __init__(self, msg):
44 self.msg = msg
45
46 def __str__(self):
47 return "Patch Error: %s" % self.msg
48
49class PatchSet(object):
50 defaults = {
51 "strippath": 1
52 }
53
54 def __init__(self, dir, d):
55 self.dir = dir
56 self.d = d
57 self.patches = []
58 self._current = None
59
60 def current(self):
61 return self._current
62
63 def Clean(self):
64 """
65 Clean out the patch set. Generally includes unapplying all
66 patches and wiping out all associated metadata.
67 """
68 raise NotImplementedError()
69
70 def Import(self, patch, force):
71 if not patch.get("file"):
72 if not patch.get("remote"):
73 raise PatchError("Patch file must be specified in patch import.")
74 else:
75 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
76
77 for param in PatchSet.defaults:
78 if not patch.get(param):
79 patch[param] = PatchSet.defaults[param]
80
81 if patch.get("remote"):
82 patch["file"] = bb.data.expand(bb.fetch2.localpath(patch["remote"], self.d), self.d)
83
84 patch["filemd5"] = bb.utils.md5_file(patch["file"])
85
86 def Push(self, force):
87 raise NotImplementedError()
88
89 def Pop(self, force):
90 raise NotImplementedError()
91
92 def Refresh(self, remote = None, all = None):
93 raise NotImplementedError()
94
95
96class PatchTree(PatchSet):
97 def __init__(self, dir, d):
98 PatchSet.__init__(self, dir, d)
99 self.patchdir = os.path.join(self.dir, 'patches')
100 self.seriespath = os.path.join(self.dir, 'patches', 'series')
101 bb.utils.mkdirhier(self.patchdir)
102
103 def _appendPatchFile(self, patch, strippath):
104 with open(self.seriespath, 'a') as f:
105 f.write(os.path.basename(patch) + "," + strippath + "\n")
106 shellcmd = ["cat", patch, ">" , self.patchdir + "/" + os.path.basename(patch)]
107 runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
108
109 def _removePatch(self, p):
110 patch = {}
111 patch['file'] = p.split(",")[0]
112 patch['strippath'] = p.split(",")[1]
113 self._applypatch(patch, False, True)
114
115 def _removePatchFile(self, all = False):
116 if not os.path.exists(self.seriespath):
117 return
118 patches = open(self.seriespath, 'r+').readlines()
119 if all:
120 for p in reversed(patches):
121 self._removePatch(os.path.join(self.patchdir, p.strip()))
122 patches = []
123 else:
124 self._removePatch(os.path.join(self.patchdir, patches[-1].strip()))
125 patches.pop()
126 with open(self.seriespath, 'w') as f:
127 for p in patches:
128 f.write(p)
129
130 def Import(self, patch, force = None):
131 """"""
132 PatchSet.Import(self, patch, force)
133
134 if self._current is not None:
135 i = self._current + 1
136 else:
137 i = 0
138 self.patches.insert(i, patch)
139
140 def _applypatch(self, patch, force = False, reverse = False, run = True):
141 shellcmd = ["cat", patch['file'], "|", "patch", "-p", patch['strippath']]
142 if reverse:
143 shellcmd.append('-R')
144
145 if not run:
146 return "sh" + "-c" + " ".join(shellcmd)
147
148 if not force:
149 shellcmd.append('--dry-run')
150
151 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
152
153 if force:
154 return
155
156 shellcmd.pop(len(shellcmd) - 1)
157 output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
158
159 if not reverse:
160 self._appendPatchFile(patch['file'], patch['strippath'])
161
162 return output
163
164 def Push(self, force = False, all = False, run = True):
165 bb.note("self._current is %s" % self._current)
166 bb.note("patches is %s" % self.patches)
167 if all:
168 for i in self.patches:
169 bb.note("applying patch %s" % i)
170 self._applypatch(i, force)
171 self._current = i
172 else:
173 if self._current is not None:
174 next = self._current + 1
175 else:
176 next = 0
177
178 bb.note("applying patch %s" % self.patches[next])
179 ret = self._applypatch(self.patches[next], force)
180
181 self._current = next
182 return ret
183
184 def Pop(self, force = None, all = None):
185 if all:
186 self._removePatchFile(True)
187 self._current = None
188 else:
189 self._removePatchFile(False)
190
191 if self._current == 0:
192 self._current = None
193
194 if self._current is not None:
195 self._current = self._current - 1
196
197 def Clean(self):
198 """"""
199 self.Pop(all=True)
200
201class GitApplyTree(PatchTree):
202 def __init__(self, dir, d):
203 PatchTree.__init__(self, dir, d)
204
205 def _applypatch(self, patch, force = False, reverse = False, run = True):
206 def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True):
207 if reverse:
208 shellcmd.append('-R')
209
210 shellcmd.append(patch['file'])
211
212 if not run:
213 return "sh" + "-c" + " ".join(shellcmd)
214
215 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
216
217 try:
218 shellcmd = ["git", "--work-tree=.", "am", "-3", "-p%s" % patch['strippath']]
219 return _applypatchhelper(shellcmd, patch, force, reverse, run)
220 except CmdError:
221 shellcmd = ["git", "--git-dir=.", "apply", "-p%s" % patch['strippath']]
222 return _applypatchhelper(shellcmd, patch, force, reverse, run)
223
224
225class QuiltTree(PatchSet):
226 def _runcmd(self, args, run = True):
227 quiltrc = self.d.getVar('QUILTRCFILE', True)
228 if not run:
229 return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
230 runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
231
232 def _quiltpatchpath(self, file):
233 return os.path.join(self.dir, "patches", os.path.basename(file))
234
235
236 def __init__(self, dir, d):
237 PatchSet.__init__(self, dir, d)
238 self.initialized = False
239 p = os.path.join(self.dir, 'patches')
240 if not os.path.exists(p):
241 os.makedirs(p)
242
243 def Clean(self):
244 try:
245 self._runcmd(["pop", "-a", "-f"])
246 oe.path.remove(os.path.join(self.dir, "patches","series"))
247 except Exception:
248 pass
249 self.initialized = True
250
251 def InitFromDir(self):
252 # read series -> self.patches
253 seriespath = os.path.join(self.dir, 'patches', 'series')
254 if not os.path.exists(self.dir):
255 raise NotFoundError(self.dir)
256 if os.path.exists(seriespath):
257 series = file(seriespath, 'r')
258 for line in series.readlines():
259 patch = {}
260 parts = line.strip().split()
261 patch["quiltfile"] = self._quiltpatchpath(parts[0])
262 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
263 if len(parts) > 1:
264 patch["strippath"] = parts[1][2:]
265 self.patches.append(patch)
266 series.close()
267
268 # determine which patches are applied -> self._current
269 try:
270 output = runcmd(["quilt", "applied"], self.dir)
271 except CmdError:
272 import sys
273 if sys.exc_value.output.strip() == "No patches applied":
274 return
275 else:
276 raise
277 output = [val for val in output.split('\n') if not val.startswith('#')]
278 for patch in self.patches:
279 if os.path.basename(patch["quiltfile"]) == output[-1]:
280 self._current = self.patches.index(patch)
281 self.initialized = True
282
283 def Import(self, patch, force = None):
284 if not self.initialized:
285 self.InitFromDir()
286 PatchSet.Import(self, patch, force)
287 oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
288 f = open(os.path.join(self.dir, "patches","series"), "a");
289 f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"]+"\n")
290 f.close()
291 patch["quiltfile"] = self._quiltpatchpath(patch["file"])
292 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
293
294 # TODO: determine if the file being imported:
295 # 1) is already imported, and is the same
296 # 2) is already imported, but differs
297
298 self.patches.insert(self._current or 0, patch)
299
300
301 def Push(self, force = False, all = False, run = True):
302 # quilt push [-f]
303
304 args = ["push"]
305 if force:
306 args.append("-f")
307 if all:
308 args.append("-a")
309 if not run:
310 return self._runcmd(args, run)
311
312 self._runcmd(args)
313
314 if self._current is not None:
315 self._current = self._current + 1
316 else:
317 self._current = 0
318
319 def Pop(self, force = None, all = None):
320 # quilt pop [-f]
321 args = ["pop"]
322 if force:
323 args.append("-f")
324 if all:
325 args.append("-a")
326
327 self._runcmd(args)
328
329 if self._current == 0:
330 self._current = None
331
332 if self._current is not None:
333 self._current = self._current - 1
334
335 def Refresh(self, **kwargs):
336 if kwargs.get("remote"):
337 patch = self.patches[kwargs["patch"]]
338 if not patch:
339 raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
340 (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
341 if type == "file":
342 import shutil
343 if not patch.get("file") and patch.get("remote"):
344 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
345
346 shutil.copyfile(patch["quiltfile"], patch["file"])
347 else:
348 raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
349 else:
350 # quilt refresh
351 args = ["refresh"]
352 if kwargs.get("quiltfile"):
353 args.append(os.path.basename(kwargs["quiltfile"]))
354 elif kwargs.get("patch"):
355 args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
356 self._runcmd(args)
357
358class Resolver(object):
359 def __init__(self, patchset, terminal):
360 raise NotImplementedError()
361
362 def Resolve(self):
363 raise NotImplementedError()
364
365 def Revert(self):
366 raise NotImplementedError()
367
368 def Finalize(self):
369 raise NotImplementedError()
370
371class NOOPResolver(Resolver):
372 def __init__(self, patchset, terminal):
373 self.patchset = patchset
374 self.terminal = terminal
375
376 def Resolve(self):
377 olddir = os.path.abspath(os.curdir)
378 os.chdir(self.patchset.dir)
379 try:
380 self.patchset.Push()
381 except Exception:
382 import sys
383 os.chdir(olddir)
384 raise
385
386# Patch resolver which relies on the user doing all the work involved in the
387# resolution, with the exception of refreshing the remote copy of the patch
388# files (the urls).
389class UserResolver(Resolver):
390 def __init__(self, patchset, terminal):
391 self.patchset = patchset
392 self.terminal = terminal
393
394 # Force a push in the patchset, then drop to a shell for the user to
395 # resolve any rejected hunks
396 def Resolve(self):
397 olddir = os.path.abspath(os.curdir)
398 os.chdir(self.patchset.dir)
399 try:
400 self.patchset.Push(False)
401 except CmdError as v:
402 # Patch application failed
403 patchcmd = self.patchset.Push(True, False, False)
404
405 t = self.patchset.d.getVar('T', True)
406 if not t:
407 bb.msg.fatal("Build", "T not set")
408 bb.utils.mkdirhier(t)
409 import random
410 rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
411 f = open(rcfile, "w")
412 f.write("echo '*** Manual patch resolution mode ***'\n")
413 f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
414 f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
415 f.write("echo ''\n")
416 f.write(" ".join(patchcmd) + "\n")
417 f.close()
418 os.chmod(rcfile, 0775)
419
420 self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
421
422 # Construct a new PatchSet after the user's changes, compare the
423 # sets, checking patches for modifications, and doing a remote
424 # refresh on each.
425 oldpatchset = self.patchset
426 self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
427
428 for patch in self.patchset.patches:
429 oldpatch = None
430 for opatch in oldpatchset.patches:
431 if opatch["quiltfile"] == patch["quiltfile"]:
432 oldpatch = opatch
433
434 if oldpatch:
435 patch["remote"] = oldpatch["remote"]
436 if patch["quiltfile"] == oldpatch["quiltfile"]:
437 if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
438 bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
439 # user change? remote refresh
440 self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
441 else:
442 # User did not fix the problem. Abort.
443 raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
444 except Exception:
445 os.chdir(olddir)
446 raise
447 os.chdir(olddir)