patch.py 33 KB

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