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.py441
1 files changed, 441 insertions, 0 deletions
diff --git a/meta/lib/oe/patch.py b/meta/lib/oe/patch.py
new file mode 100644
index 0000000000..59abd0af19
--- /dev/null
+++ b/meta/lib/oe/patch.py
@@ -0,0 +1,441 @@
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 shellcmd = ["git", "--git-dir=.", "apply", "-p%s" % patch['strippath']]
207
208 if reverse:
209 shellcmd.append('-R')
210
211 shellcmd.append(patch['file'])
212
213 if not run:
214 return "sh" + "-c" + " ".join(shellcmd)
215
216 return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
217
218
219class QuiltTree(PatchSet):
220 def _runcmd(self, args, run = True):
221 quiltrc = self.d.getVar('QUILTRCFILE', True)
222 if not run:
223 return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
224 runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
225
226 def _quiltpatchpath(self, file):
227 return os.path.join(self.dir, "patches", os.path.basename(file))
228
229
230 def __init__(self, dir, d):
231 PatchSet.__init__(self, dir, d)
232 self.initialized = False
233 p = os.path.join(self.dir, 'patches')
234 if not os.path.exists(p):
235 os.makedirs(p)
236
237 def Clean(self):
238 try:
239 self._runcmd(["pop", "-a", "-f"])
240 oe.path.remove(os.path.join(self.dir, "patches","series"))
241 except Exception:
242 pass
243 self.initialized = True
244
245 def InitFromDir(self):
246 # read series -> self.patches
247 seriespath = os.path.join(self.dir, 'patches', 'series')
248 if not os.path.exists(self.dir):
249 raise NotFoundError(self.dir)
250 if os.path.exists(seriespath):
251 series = file(seriespath, 'r')
252 for line in series.readlines():
253 patch = {}
254 parts = line.strip().split()
255 patch["quiltfile"] = self._quiltpatchpath(parts[0])
256 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
257 if len(parts) > 1:
258 patch["strippath"] = parts[1][2:]
259 self.patches.append(patch)
260 series.close()
261
262 # determine which patches are applied -> self._current
263 try:
264 output = runcmd(["quilt", "applied"], self.dir)
265 except CmdError:
266 import sys
267 if sys.exc_value.output.strip() == "No patches applied":
268 return
269 else:
270 raise
271 output = [val for val in output.split('\n') if not val.startswith('#')]
272 for patch in self.patches:
273 if os.path.basename(patch["quiltfile"]) == output[-1]:
274 self._current = self.patches.index(patch)
275 self.initialized = True
276
277 def Import(self, patch, force = None):
278 if not self.initialized:
279 self.InitFromDir()
280 PatchSet.Import(self, patch, force)
281 oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
282 f = open(os.path.join(self.dir, "patches","series"), "a");
283 f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"]+"\n")
284 f.close()
285 patch["quiltfile"] = self._quiltpatchpath(patch["file"])
286 patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
287
288 # TODO: determine if the file being imported:
289 # 1) is already imported, and is the same
290 # 2) is already imported, but differs
291
292 self.patches.insert(self._current or 0, patch)
293
294
295 def Push(self, force = False, all = False, run = True):
296 # quilt push [-f]
297
298 args = ["push"]
299 if force:
300 args.append("-f")
301 if all:
302 args.append("-a")
303 if not run:
304 return self._runcmd(args, run)
305
306 self._runcmd(args)
307
308 if self._current is not None:
309 self._current = self._current + 1
310 else:
311 self._current = 0
312
313 def Pop(self, force = None, all = None):
314 # quilt pop [-f]
315 args = ["pop"]
316 if force:
317 args.append("-f")
318 if all:
319 args.append("-a")
320
321 self._runcmd(args)
322
323 if self._current == 0:
324 self._current = None
325
326 if self._current is not None:
327 self._current = self._current - 1
328
329 def Refresh(self, **kwargs):
330 if kwargs.get("remote"):
331 patch = self.patches[kwargs["patch"]]
332 if not patch:
333 raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
334 (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
335 if type == "file":
336 import shutil
337 if not patch.get("file") and patch.get("remote"):
338 patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
339
340 shutil.copyfile(patch["quiltfile"], patch["file"])
341 else:
342 raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
343 else:
344 # quilt refresh
345 args = ["refresh"]
346 if kwargs.get("quiltfile"):
347 args.append(os.path.basename(kwargs["quiltfile"]))
348 elif kwargs.get("patch"):
349 args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
350 self._runcmd(args)
351
352class Resolver(object):
353 def __init__(self, patchset, terminal):
354 raise NotImplementedError()
355
356 def Resolve(self):
357 raise NotImplementedError()
358
359 def Revert(self):
360 raise NotImplementedError()
361
362 def Finalize(self):
363 raise NotImplementedError()
364
365class NOOPResolver(Resolver):
366 def __init__(self, patchset, terminal):
367 self.patchset = patchset
368 self.terminal = terminal
369
370 def Resolve(self):
371 olddir = os.path.abspath(os.curdir)
372 os.chdir(self.patchset.dir)
373 try:
374 self.patchset.Push()
375 except Exception:
376 import sys
377 os.chdir(olddir)
378 raise
379
380# Patch resolver which relies on the user doing all the work involved in the
381# resolution, with the exception of refreshing the remote copy of the patch
382# files (the urls).
383class UserResolver(Resolver):
384 def __init__(self, patchset, terminal):
385 self.patchset = patchset
386 self.terminal = terminal
387
388 # Force a push in the patchset, then drop to a shell for the user to
389 # resolve any rejected hunks
390 def Resolve(self):
391 olddir = os.path.abspath(os.curdir)
392 os.chdir(self.patchset.dir)
393 try:
394 self.patchset.Push(False)
395 except CmdError as v:
396 # Patch application failed
397 patchcmd = self.patchset.Push(True, False, False)
398
399 t = self.patchset.d.getVar('T', True)
400 if not t:
401 bb.msg.fatal("Build", "T not set")
402 bb.utils.mkdirhier(t)
403 import random
404 rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
405 f = open(rcfile, "w")
406 f.write("echo '*** Manual patch resolution mode ***'\n")
407 f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
408 f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
409 f.write("echo ''\n")
410 f.write(" ".join(patchcmd) + "\n")
411 f.close()
412 os.chmod(rcfile, 0775)
413
414 self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
415
416 # Construct a new PatchSet after the user's changes, compare the
417 # sets, checking patches for modifications, and doing a remote
418 # refresh on each.
419 oldpatchset = self.patchset
420 self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
421
422 for patch in self.patchset.patches:
423 oldpatch = None
424 for opatch in oldpatchset.patches:
425 if opatch["quiltfile"] == patch["quiltfile"]:
426 oldpatch = opatch
427
428 if oldpatch:
429 patch["remote"] = oldpatch["remote"]
430 if patch["quiltfile"] == oldpatch["quiltfile"]:
431 if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
432 bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
433 # user change? remote refresh
434 self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
435 else:
436 # User did not fix the problem. Abort.
437 raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
438 except Exception:
439 os.chdir(olddir)
440 raise
441 os.chdir(olddir)