patch.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import oe.path
  2. class NotFoundError(Exception):
  3. def __init__(self, path):
  4. self.path = path
  5. def __str__(self):
  6. return "Error: %s not found." % self.path
  7. class CmdError(Exception):
  8. def __init__(self, exitstatus, output):
  9. self.status = exitstatus
  10. self.output = output
  11. def __str__(self):
  12. return "Command Error: exit status: %d Output:\n%s" % (self.status, self.output)
  13. def runcmd(args, dir = None):
  14. import commands
  15. if dir:
  16. olddir = os.path.abspath(os.curdir)
  17. if not os.path.exists(dir):
  18. raise NotFoundError(dir)
  19. os.chdir(dir)
  20. # print("cwd: %s -> %s" % (olddir, dir))
  21. try:
  22. args = [ commands.mkarg(str(arg)) for arg in args ]
  23. cmd = " ".join(args)
  24. # print("cmd: %s" % cmd)
  25. (exitstatus, output) = commands.getstatusoutput(cmd)
  26. if exitstatus != 0:
  27. raise CmdError(exitstatus >> 8, output)
  28. return output
  29. finally:
  30. if dir:
  31. os.chdir(olddir)
  32. class PatchError(Exception):
  33. def __init__(self, msg):
  34. self.msg = msg
  35. def __str__(self):
  36. return "Patch Error: %s" % self.msg
  37. class PatchSet(object):
  38. defaults = {
  39. "strippath": 1
  40. }
  41. def __init__(self, dir, d):
  42. self.dir = dir
  43. self.d = d
  44. self.patches = []
  45. self._current = None
  46. def current(self):
  47. return self._current
  48. def Clean(self):
  49. """
  50. Clean out the patch set. Generally includes unapplying all
  51. patches and wiping out all associated metadata.
  52. """
  53. raise NotImplementedError()
  54. def Import(self, patch, force):
  55. if not patch.get("file"):
  56. if not patch.get("remote"):
  57. raise PatchError("Patch file must be specified in patch import.")
  58. else:
  59. patch["file"] = bb.fetch.localpath(patch["remote"], self.d)
  60. for param in PatchSet.defaults:
  61. if not patch.get(param):
  62. patch[param] = PatchSet.defaults[param]
  63. if patch.get("remote"):
  64. patch["file"] = bb.data.expand(bb.fetch.localpath(patch["remote"], self.d), self.d)
  65. patch["filemd5"] = bb.utils.md5_file(patch["file"])
  66. def Push(self, force):
  67. raise NotImplementedError()
  68. def Pop(self, force):
  69. raise NotImplementedError()
  70. def Refresh(self, remote = None, all = None):
  71. raise NotImplementedError()
  72. class PatchTree(PatchSet):
  73. def __init__(self, dir, d):
  74. PatchSet.__init__(self, dir, d)
  75. def Import(self, patch, force = None):
  76. """"""
  77. PatchSet.Import(self, patch, force)
  78. if self._current is not None:
  79. i = self._current + 1
  80. else:
  81. i = 0
  82. self.patches.insert(i, patch)
  83. def _applypatch(self, patch, force = False, reverse = False, run = True):
  84. shellcmd = ["cat", patch['file'], "|", "patch", "-p", patch['strippath']]
  85. if reverse:
  86. shellcmd.append('-R')
  87. if not run:
  88. return "sh" + "-c" + " ".join(shellcmd)
  89. if not force:
  90. shellcmd.append('--dry-run')
  91. output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  92. if force:
  93. return
  94. shellcmd.pop(len(shellcmd) - 1)
  95. output = runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  96. return output
  97. def Push(self, force = False, all = False, run = True):
  98. bb.note("self._current is %s" % self._current)
  99. bb.note("patches is %s" % self.patches)
  100. if all:
  101. for i in self.patches:
  102. if self._current is not None:
  103. self._current = self._current + 1
  104. else:
  105. self._current = 0
  106. bb.note("applying patch %s" % i)
  107. self._applypatch(i, force)
  108. else:
  109. if self._current is not None:
  110. self._current = self._current + 1
  111. else:
  112. self._current = 0
  113. bb.note("applying patch %s" % self.patches[self._current])
  114. return self._applypatch(self.patches[self._current], force)
  115. def Pop(self, force = None, all = None):
  116. if all:
  117. for i in self.patches:
  118. self._applypatch(i, force, True)
  119. else:
  120. self._applypatch(self.patches[self._current], force, True)
  121. def Clean(self):
  122. """"""
  123. class GitApplyTree(PatchTree):
  124. def __init__(self, dir, d):
  125. PatchTree.__init__(self, dir, d)
  126. def _applypatch(self, patch, force = False, reverse = False, run = True):
  127. shellcmd = ["git", "--git-dir=.", "apply", "-p%s" % patch['strippath']]
  128. if reverse:
  129. shellcmd.append('-R')
  130. shellcmd.append(patch['file'])
  131. if not run:
  132. return "sh" + "-c" + " ".join(shellcmd)
  133. return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  134. class QuiltTree(PatchSet):
  135. def _runcmd(self, args, run = True):
  136. quiltrc = bb.data.getVar('QUILTRCFILE', self.d, 1)
  137. if not run:
  138. return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
  139. runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
  140. def _quiltpatchpath(self, file):
  141. return os.path.join(self.dir, "patches", os.path.basename(file))
  142. def __init__(self, dir, d):
  143. PatchSet.__init__(self, dir, d)
  144. self.initialized = False
  145. p = os.path.join(self.dir, 'patches')
  146. if not os.path.exists(p):
  147. os.makedirs(p)
  148. def Clean(self):
  149. try:
  150. self._runcmd(["pop", "-a", "-f"])
  151. except Exception:
  152. pass
  153. self.initialized = True
  154. def InitFromDir(self):
  155. # read series -> self.patches
  156. seriespath = os.path.join(self.dir, 'patches', 'series')
  157. if not os.path.exists(self.dir):
  158. raise Exception("Error: %s does not exist." % self.dir)
  159. if os.path.exists(seriespath):
  160. series = file(seriespath, 'r')
  161. for line in series.readlines():
  162. patch = {}
  163. parts = line.strip().split()
  164. patch["quiltfile"] = self._quiltpatchpath(parts[0])
  165. patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
  166. if len(parts) > 1:
  167. patch["strippath"] = parts[1][2:]
  168. self.patches.append(patch)
  169. series.close()
  170. # determine which patches are applied -> self._current
  171. try:
  172. output = runcmd(["quilt", "applied"], self.dir)
  173. except CmdError:
  174. import sys
  175. if sys.exc_value.output.strip() == "No patches applied":
  176. return
  177. else:
  178. raise sys.exc_value
  179. output = [val for val in output.split('\n') if not val.startswith('#')]
  180. for patch in self.patches:
  181. if os.path.basename(patch["quiltfile"]) == output[-1]:
  182. self._current = self.patches.index(patch)
  183. self.initialized = True
  184. def Import(self, patch, force = None):
  185. if not self.initialized:
  186. self.InitFromDir()
  187. PatchSet.Import(self, patch, force)
  188. oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]))
  189. f = open(os.path.join(self.dir, "patches","series"), "a");
  190. f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"]+"\n")
  191. f.close()
  192. patch["quiltfile"] = self._quiltpatchpath(patch["file"])
  193. patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
  194. # TODO: determine if the file being imported:
  195. # 1) is already imported, and is the same
  196. # 2) is already imported, but differs
  197. self.patches.insert(self._current or 0, patch)
  198. def Push(self, force = False, all = False, run = True):
  199. # quilt push [-f]
  200. args = ["push"]
  201. if force:
  202. args.append("-f")
  203. if all:
  204. args.append("-a")
  205. if not run:
  206. return self._runcmd(args, run)
  207. self._runcmd(args)
  208. if self._current is not None:
  209. self._current = self._current + 1
  210. else:
  211. self._current = 0
  212. def Pop(self, force = None, all = None):
  213. # quilt pop [-f]
  214. args = ["pop"]
  215. if force:
  216. args.append("-f")
  217. if all:
  218. args.append("-a")
  219. self._runcmd(args)
  220. if self._current == 0:
  221. self._current = None
  222. if self._current is not None:
  223. self._current = self._current - 1
  224. def Refresh(self, **kwargs):
  225. if kwargs.get("remote"):
  226. patch = self.patches[kwargs["patch"]]
  227. if not patch:
  228. raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
  229. (type, host, path, user, pswd, parm) = bb.decodeurl(patch["remote"])
  230. if type == "file":
  231. import shutil
  232. if not patch.get("file") and patch.get("remote"):
  233. patch["file"] = bb.fetch.localpath(patch["remote"], self.d)
  234. shutil.copyfile(patch["quiltfile"], patch["file"])
  235. else:
  236. raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
  237. else:
  238. # quilt refresh
  239. args = ["refresh"]
  240. if kwargs.get("quiltfile"):
  241. args.append(os.path.basename(kwargs["quiltfile"]))
  242. elif kwargs.get("patch"):
  243. args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
  244. self._runcmd(args)
  245. class Resolver(object):
  246. def __init__(self, patchset):
  247. raise NotImplementedError()
  248. def Resolve(self):
  249. raise NotImplementedError()
  250. def Revert(self):
  251. raise NotImplementedError()
  252. def Finalize(self):
  253. raise NotImplementedError()
  254. class NOOPResolver(Resolver):
  255. def __init__(self, patchset):
  256. self.patchset = patchset
  257. def Resolve(self):
  258. olddir = os.path.abspath(os.curdir)
  259. os.chdir(self.patchset.dir)
  260. try:
  261. self.patchset.Push()
  262. except Exception:
  263. import sys
  264. os.chdir(olddir)
  265. raise sys.exc_value
  266. # Patch resolver which relies on the user doing all the work involved in the
  267. # resolution, with the exception of refreshing the remote copy of the patch
  268. # files (the urls).
  269. class UserResolver(Resolver):
  270. def __init__(self, patchset):
  271. self.patchset = patchset
  272. # Force a push in the patchset, then drop to a shell for the user to
  273. # resolve any rejected hunks
  274. def Resolve(self):
  275. olddir = os.path.abspath(os.curdir)
  276. os.chdir(self.patchset.dir)
  277. try:
  278. self.patchset.Push(False)
  279. except CmdError, v:
  280. # Patch application failed
  281. patchcmd = self.patchset.Push(True, False, False)
  282. t = bb.data.getVar('T', self.patchset.d, 1)
  283. if not t:
  284. bb.msg.fatal(bb.msg.domain.Build, "T not set")
  285. bb.mkdirhier(t)
  286. import random
  287. rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
  288. f = open(rcfile, "w")
  289. f.write("echo '*** Manual patch resolution mode ***'\n")
  290. f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
  291. f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
  292. f.write("echo ''\n")
  293. f.write(" ".join(patchcmd) + "\n")
  294. f.write("#" + bb.data.getVar('TERMCMDRUN', self.patchset.d, 1))
  295. f.close()
  296. os.chmod(rcfile, 0775)
  297. os.environ['TERMWINDOWTITLE'] = "Bitbake: Please fix patch rejects manually"
  298. os.environ['SHELLCMDS'] = "bash --rcfile " + rcfile
  299. rc = os.system(bb.data.getVar('TERMCMDRUN', self.patchset.d, 1))
  300. if os.WIFEXITED(rc) and os.WEXITSTATUS(rc) != 0:
  301. bb.msg.fatal(bb.msg.domain.Build, ("Cannot proceed with manual patch resolution - '%s' not found. " \
  302. + "Check TERMCMDRUN variable.") % bb.data.getVar('TERMCMDRUN', self.patchset.d, 1))
  303. # Construct a new PatchSet after the user's changes, compare the
  304. # sets, checking patches for modifications, and doing a remote
  305. # refresh on each.
  306. oldpatchset = self.patchset
  307. self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
  308. for patch in self.patchset.patches:
  309. oldpatch = None
  310. for opatch in oldpatchset.patches:
  311. if opatch["quiltfile"] == patch["quiltfile"]:
  312. oldpatch = opatch
  313. if oldpatch:
  314. patch["remote"] = oldpatch["remote"]
  315. if patch["quiltfile"] == oldpatch["quiltfile"]:
  316. if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
  317. bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
  318. # user change? remote refresh
  319. self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
  320. else:
  321. # User did not fix the problem. Abort.
  322. raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
  323. except Exception:
  324. os.chdir(olddir)
  325. raise
  326. os.chdir(olddir)