patch.py 33 KB

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