patch.py 32 KB

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