patch.py 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003
  1. #
  2. # Copyright OpenEmbedded Contributors
  3. #
  4. # SPDX-License-Identifier: GPL-2.0-only
  5. #
  6. import os
  7. import shlex
  8. import subprocess
  9. import oe.path
  10. import oe.types
  11. class NotFoundError(bb.BBHandledException):
  12. def __init__(self, path):
  13. self.path = path
  14. def __str__(self):
  15. return "Error: %s not found." % self.path
  16. class CmdError(bb.BBHandledException):
  17. def __init__(self, command, exitstatus, output):
  18. self.command = command
  19. self.status = exitstatus
  20. self.output = output
  21. def __str__(self):
  22. return "Command Error: '%s' exited with %d Output:\n%s" % \
  23. (self.command, self.status, self.output)
  24. def runcmd(args, dir = None):
  25. if dir:
  26. olddir = os.path.abspath(os.curdir)
  27. if not os.path.exists(dir):
  28. raise NotFoundError(dir)
  29. os.chdir(dir)
  30. # print("cwd: %s -> %s" % (olddir, dir))
  31. try:
  32. args = [ shlex.quote(str(arg)) for arg in args ]
  33. cmd = " ".join(args)
  34. # print("cmd: %s" % cmd)
  35. proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
  36. stdout, stderr = proc.communicate()
  37. stdout = stdout.decode('utf-8')
  38. stderr = stderr.decode('utf-8')
  39. exitstatus = proc.returncode
  40. if exitstatus != 0:
  41. raise CmdError(cmd, exitstatus >> 8, "stdout: %s\nstderr: %s" % (stdout, stderr))
  42. if " fuzz " in stdout and "Hunk " in stdout:
  43. # Drop patch fuzz info with header and footer to log file so
  44. # insane.bbclass can handle to throw error/warning
  45. bb.note("--- Patch fuzz start ---\n%s\n--- Patch fuzz end ---" % format(stdout))
  46. return stdout
  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. notes_ref = "refs/notes/devtool"
  246. original_patch = 'original patch'
  247. ignore_commit = 'ignore'
  248. def __init__(self, dir, d):
  249. PatchTree.__init__(self, dir, d)
  250. self.commituser = d.getVar('PATCH_GIT_USER_NAME')
  251. self.commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
  252. if not self._isInitialized(d):
  253. self._initRepo()
  254. def _isInitialized(self, d):
  255. cmd = "git rev-parse --show-toplevel"
  256. try:
  257. output = runcmd(cmd.split(), self.dir).strip()
  258. except CmdError as err:
  259. ## runcmd returned non-zero which most likely means 128
  260. ## Not a git directory
  261. return False
  262. ## Make sure repo is in builddir to not break top-level git repos, or under workdir
  263. return os.path.samefile(output, self.dir) or oe.path.is_path_parent(d.getVar('WORKDIR'), output)
  264. def _initRepo(self):
  265. runcmd("git init".split(), self.dir)
  266. runcmd("git add .".split(), self.dir)
  267. runcmd("git commit -a --allow-empty -m bitbake_patching_started".split(), self.dir)
  268. @staticmethod
  269. def extractPatchHeader(patchfile):
  270. """
  271. Extract just the header lines from the top of a patch file
  272. """
  273. for encoding in ['utf-8', 'latin-1']:
  274. lines = []
  275. try:
  276. with open(patchfile, 'r', encoding=encoding) as f:
  277. for line in f:
  278. if line.startswith('Index: ') or line.startswith('diff -') or line.startswith('---'):
  279. break
  280. lines.append(line)
  281. except UnicodeDecodeError:
  282. continue
  283. break
  284. else:
  285. raise PatchError('Unable to find a character encoding to decode %s' % patchfile)
  286. return lines
  287. @staticmethod
  288. def decodeAuthor(line):
  289. from email.header import decode_header
  290. authorval = line.split(':', 1)[1].strip().replace('"', '')
  291. result = decode_header(authorval)[0][0]
  292. if hasattr(result, 'decode'):
  293. result = result.decode('utf-8')
  294. return result
  295. @staticmethod
  296. def interpretPatchHeader(headerlines):
  297. import re
  298. author_re = re.compile(r'[\S ]+ <\S+@\S+\.\S+>')
  299. from_commit_re = re.compile(r'^From [a-z0-9]{40} .*')
  300. outlines = []
  301. author = None
  302. date = None
  303. subject = None
  304. for line in headerlines:
  305. if line.startswith('Subject: '):
  306. subject = line.split(':', 1)[1]
  307. # Remove any [PATCH][oe-core] etc.
  308. subject = re.sub(r'\[.+?\]\s*', '', subject)
  309. continue
  310. elif line.startswith('From: ') or line.startswith('Author: '):
  311. authorval = GitApplyTree.decodeAuthor(line)
  312. # git is fussy about author formatting i.e. it must be Name <email@domain>
  313. if author_re.match(authorval):
  314. author = authorval
  315. continue
  316. elif line.startswith('Date: '):
  317. if date is None:
  318. dateval = line.split(':', 1)[1].strip()
  319. # Very crude check for date format, since git will blow up if it's not in the right
  320. # format. Without e.g. a python-dateutils dependency we can't do a whole lot more
  321. if len(dateval) > 12:
  322. date = dateval
  323. continue
  324. elif not author and line.lower().startswith('signed-off-by: '):
  325. authorval = GitApplyTree.decodeAuthor(line)
  326. # git is fussy about author formatting i.e. it must be Name <email@domain>
  327. if author_re.match(authorval):
  328. author = authorval
  329. elif from_commit_re.match(line):
  330. # We don't want the From <commit> line - if it's present it will break rebasing
  331. continue
  332. outlines.append(line)
  333. if not subject:
  334. firstline = None
  335. for line in headerlines:
  336. line = line.strip()
  337. if firstline:
  338. if line:
  339. # Second line is not blank, the first line probably isn't usable
  340. firstline = None
  341. break
  342. elif line:
  343. firstline = line
  344. if firstline and not firstline.startswith(('#', 'Index:', 'Upstream-Status:')) and len(firstline) < 100:
  345. subject = firstline
  346. return outlines, author, date, subject
  347. @staticmethod
  348. def gitCommandUserOptions(cmd, commituser=None, commitemail=None, d=None):
  349. if d:
  350. commituser = d.getVar('PATCH_GIT_USER_NAME')
  351. commitemail = d.getVar('PATCH_GIT_USER_EMAIL')
  352. if commituser:
  353. cmd += ['-c', 'user.name="%s"' % commituser]
  354. if commitemail:
  355. cmd += ['-c', 'user.email="%s"' % commitemail]
  356. @staticmethod
  357. def prepareCommit(patchfile, commituser=None, commitemail=None):
  358. """
  359. Prepare a git commit command line based on the header from a patch file
  360. (typically this is useful for patches that cannot be applied with "git am" due to formatting)
  361. """
  362. import tempfile
  363. # Process patch header and extract useful information
  364. lines = GitApplyTree.extractPatchHeader(patchfile)
  365. outlines, author, date, subject = GitApplyTree.interpretPatchHeader(lines)
  366. if not author or not subject or not date:
  367. try:
  368. shellcmd = ["git", "log", "--format=email", "--follow", "--diff-filter=A", "--", patchfile]
  369. out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.dirname(patchfile))
  370. except CmdError:
  371. out = None
  372. if out:
  373. _, newauthor, newdate, newsubject = GitApplyTree.interpretPatchHeader(out.splitlines())
  374. if not author:
  375. # If we're setting the author then the date should be set as well
  376. author = newauthor
  377. date = newdate
  378. elif not date:
  379. # If we don't do this we'll get the current date, at least this will be closer
  380. date = newdate
  381. if not subject:
  382. subject = newsubject
  383. if subject and not (outlines and outlines[0].strip() == subject):
  384. outlines.insert(0, '%s\n\n' % subject.strip())
  385. # Write out commit message to a file
  386. with tempfile.NamedTemporaryFile('w', delete=False) as tf:
  387. tmpfile = tf.name
  388. for line in outlines:
  389. tf.write(line)
  390. # Prepare git command
  391. cmd = ["git"]
  392. GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail)
  393. cmd += ["commit", "-F", tmpfile, "--no-verify"]
  394. # git doesn't like plain email addresses as authors
  395. if author and '<' in author:
  396. cmd.append('--author="%s"' % author)
  397. if date:
  398. cmd.append('--date="%s"' % date)
  399. return (tmpfile, cmd)
  400. @staticmethod
  401. def addNote(repo, ref, key, value=None, commituser=None, commitemail=None):
  402. note = key + (": %s" % value if value else "")
  403. notes_ref = GitApplyTree.notes_ref
  404. runcmd(["git", "config", "notes.rewriteMode", "ignore"], repo)
  405. runcmd(["git", "config", "notes.displayRef", notes_ref, notes_ref], repo)
  406. runcmd(["git", "config", "notes.rewriteRef", notes_ref, notes_ref], repo)
  407. cmd = ["git"]
  408. GitApplyTree.gitCommandUserOptions(cmd, commituser, commitemail)
  409. runcmd(cmd + ["notes", "--ref", notes_ref, "append", "-m", note, ref], repo)
  410. @staticmethod
  411. def removeNote(repo, ref, key, commituser=None, commitemail=None):
  412. notes = GitApplyTree.getNotes(repo, ref)
  413. notes = {k: v for k, v in notes.items() if k != key and not k.startswith(key + ":")}
  414. runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "remove", "--ignore-missing", ref], repo)
  415. for note, value in notes.items():
  416. GitApplyTree.addNote(repo, ref, note, value, commituser, commitemail)
  417. @staticmethod
  418. def getNotes(repo, ref):
  419. import re
  420. note = None
  421. try:
  422. note = runcmd(["git", "notes", "--ref", GitApplyTree.notes_ref, "show", ref], repo)
  423. prefix = ""
  424. except CmdError:
  425. note = runcmd(['git', 'show', '-s', '--format=%B', ref], repo)
  426. prefix = "%% "
  427. note_re = re.compile(r'^%s(.*?)(?::\s*(.*))?$' % prefix)
  428. notes = dict()
  429. for line in note.splitlines():
  430. m = note_re.match(line)
  431. if m:
  432. notes[m.group(1)] = m.group(2)
  433. return notes
  434. @staticmethod
  435. def commitIgnored(subject, dir=None, files=None, d=None):
  436. if files:
  437. runcmd(['git', 'add'] + files, dir)
  438. cmd = ["git"]
  439. GitApplyTree.gitCommandUserOptions(cmd, d=d)
  440. cmd += ["commit", "-m", subject, "--no-verify"]
  441. runcmd(cmd, dir)
  442. GitApplyTree.addNote(dir, "HEAD", GitApplyTree.ignore_commit, d.getVar('PATCH_GIT_USER_NAME'), d.getVar('PATCH_GIT_USER_EMAIL'))
  443. @staticmethod
  444. def extractPatches(tree, startcommits, outdir, paths=None):
  445. import tempfile
  446. import shutil
  447. tempdir = tempfile.mkdtemp(prefix='oepatch')
  448. try:
  449. for name, rev in startcommits.items():
  450. shellcmd = ["git", "format-patch", "--no-signature", "--no-numbered", rev, "-o", tempdir]
  451. if paths:
  452. shellcmd.append('--')
  453. shellcmd.extend(paths)
  454. out = runcmd(["sh", "-c", " ".join(shellcmd)], os.path.join(tree, name))
  455. if out:
  456. for srcfile in out.split():
  457. # This loop, which is used to remove any line that
  458. # starts with "%% original patch", is kept for backwards
  459. # compatibility. If/when that compatibility is dropped,
  460. # it can be replaced with code to just read the first
  461. # line of the patch file to get the SHA-1, and the code
  462. # below that writes the modified patch file can be
  463. # replaced with a simple file move.
  464. for encoding in ['utf-8', 'latin-1']:
  465. patchlines = []
  466. try:
  467. with open(srcfile, 'r', encoding=encoding, newline='') as f:
  468. for line in f:
  469. if line.startswith("%% " + GitApplyTree.original_patch):
  470. continue
  471. patchlines.append(line)
  472. except UnicodeDecodeError:
  473. continue
  474. break
  475. else:
  476. raise PatchError('Unable to find a character encoding to decode %s' % srcfile)
  477. sha1 = patchlines[0].split()[1]
  478. notes = GitApplyTree.getNotes(os.path.join(tree, name), sha1)
  479. if GitApplyTree.ignore_commit in notes:
  480. continue
  481. outfile = notes.get(GitApplyTree.original_patch, os.path.basename(srcfile))
  482. bb.utils.mkdirhier(os.path.join(outdir, name))
  483. with open(os.path.join(outdir, name, outfile), 'w') as of:
  484. for line in patchlines:
  485. of.write(line)
  486. finally:
  487. shutil.rmtree(tempdir)
  488. def _need_dirty_check(self):
  489. fetch = bb.fetch2.Fetch([], self.d)
  490. check_dirtyness = False
  491. for url in fetch.urls:
  492. url_data = fetch.ud[url]
  493. parm = url_data.parm
  494. # a git url with subpath param will surely be dirty
  495. # since the git tree from which we clone will be emptied
  496. # from all files that are not in the subpath
  497. if url_data.type == 'git' and parm.get('subpath'):
  498. check_dirtyness = True
  499. return check_dirtyness
  500. def _commitpatch(self, patch, patchfilevar):
  501. output = ""
  502. # Add all files
  503. shellcmd = ["git", "add", "-f", "-A", "."]
  504. output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  505. # Exclude the patches directory
  506. shellcmd = ["git", "reset", "HEAD", self.patchdir]
  507. output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  508. # Commit the result
  509. (tmpfile, shellcmd) = self.prepareCommit(patch['file'], self.commituser, self.commitemail)
  510. try:
  511. shellcmd.insert(0, patchfilevar)
  512. output += runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  513. finally:
  514. os.remove(tmpfile)
  515. return output
  516. def _applypatch(self, patch, force = False, reverse = False, run = True):
  517. import shutil
  518. def _applypatchhelper(shellcmd, patch, force = False, reverse = False, run = True):
  519. if reverse:
  520. shellcmd.append('-R')
  521. shellcmd.append(patch['file'])
  522. if not run:
  523. return "sh" + "-c" + " ".join(shellcmd)
  524. return runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  525. reporoot = (runcmd("git rev-parse --show-toplevel".split(), self.dir) or '').strip()
  526. if not reporoot:
  527. raise Exception("Cannot get repository root for directory %s" % self.dir)
  528. patch_applied = True
  529. try:
  530. patchfilevar = 'PATCHFILE="%s"' % os.path.basename(patch['file'])
  531. if self._need_dirty_check():
  532. # Check dirtyness of the tree
  533. try:
  534. output = runcmd(["git", "--work-tree=%s" % reporoot, "status", "--short"])
  535. except CmdError:
  536. pass
  537. else:
  538. if output:
  539. # The tree is dirty, no need to try to apply patches with git anymore
  540. # since they fail, fallback directly to patch
  541. output = PatchTree._applypatch(self, patch, force, reverse, run)
  542. output += self._commitpatch(patch, patchfilevar)
  543. return output
  544. try:
  545. shellcmd = [patchfilevar, "git", "--work-tree=%s" % reporoot]
  546. self.gitCommandUserOptions(shellcmd, self.commituser, self.commitemail)
  547. shellcmd += ["am", "-3", "--keep-cr", "--no-scissors", "-p%s" % patch['strippath']]
  548. return _applypatchhelper(shellcmd, patch, force, reverse, run)
  549. except CmdError:
  550. # Need to abort the git am, or we'll still be within it at the end
  551. try:
  552. shellcmd = ["git", "--work-tree=%s" % reporoot, "am", "--abort"]
  553. runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  554. except CmdError:
  555. pass
  556. # git am won't always clean up after itself, sadly, so...
  557. shellcmd = ["git", "--work-tree=%s" % reporoot, "reset", "--hard", "HEAD"]
  558. runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  559. # Also need to take care of any stray untracked files
  560. shellcmd = ["git", "--work-tree=%s" % reporoot, "clean", "-f"]
  561. runcmd(["sh", "-c", " ".join(shellcmd)], self.dir)
  562. # Fall back to git apply
  563. shellcmd = ["git", "--git-dir=%s" % reporoot, "apply", "-p%s" % patch['strippath']]
  564. try:
  565. output = _applypatchhelper(shellcmd, patch, force, reverse, run)
  566. except CmdError:
  567. # Fall back to patch
  568. output = PatchTree._applypatch(self, patch, force, reverse, run)
  569. output += self._commitpatch(patch, patchfilevar)
  570. return output
  571. except:
  572. patch_applied = False
  573. raise
  574. finally:
  575. if patch_applied:
  576. GitApplyTree.addNote(self.dir, "HEAD", GitApplyTree.original_patch, os.path.basename(patch['file']), self.commituser, self.commitemail)
  577. class QuiltTree(PatchSet):
  578. def _runcmd(self, args, run = True):
  579. quiltrc = self.d.getVar('QUILTRCFILE')
  580. if not run:
  581. return ["quilt"] + ["--quiltrc"] + [quiltrc] + args
  582. runcmd(["quilt"] + ["--quiltrc"] + [quiltrc] + args, self.dir)
  583. def _quiltpatchpath(self, file):
  584. return os.path.join(self.dir, "patches", os.path.basename(file))
  585. def __init__(self, dir, d):
  586. PatchSet.__init__(self, dir, d)
  587. self.initialized = False
  588. p = os.path.join(self.dir, 'patches')
  589. if not os.path.exists(p):
  590. os.makedirs(p)
  591. def Clean(self):
  592. try:
  593. # make sure that patches/series file exists before quilt pop to keep quilt-0.67 happy
  594. open(os.path.join(self.dir, "patches","series"), 'a').close()
  595. self._runcmd(["pop", "-a", "-f"])
  596. oe.path.remove(os.path.join(self.dir, "patches","series"))
  597. except Exception:
  598. pass
  599. self.initialized = True
  600. def InitFromDir(self):
  601. # read series -> self.patches
  602. seriespath = os.path.join(self.dir, 'patches', 'series')
  603. if not os.path.exists(self.dir):
  604. raise NotFoundError(self.dir)
  605. if os.path.exists(seriespath):
  606. with open(seriespath, 'r') as f:
  607. for line in f.readlines():
  608. patch = {}
  609. parts = line.strip().split()
  610. patch["quiltfile"] = self._quiltpatchpath(parts[0])
  611. patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
  612. if len(parts) > 1:
  613. patch["strippath"] = parts[1][2:]
  614. self.patches.append(patch)
  615. # determine which patches are applied -> self._current
  616. try:
  617. output = runcmd(["quilt", "applied"], self.dir)
  618. except CmdError:
  619. import sys
  620. if sys.exc_value.output.strip() == "No patches applied":
  621. return
  622. else:
  623. raise
  624. output = [val for val in output.split('\n') if not val.startswith('#')]
  625. for patch in self.patches:
  626. if os.path.basename(patch["quiltfile"]) == output[-1]:
  627. self._current = self.patches.index(patch)
  628. self.initialized = True
  629. def Import(self, patch, force = None):
  630. if not self.initialized:
  631. self.InitFromDir()
  632. PatchSet.Import(self, patch, force)
  633. oe.path.symlink(patch["file"], self._quiltpatchpath(patch["file"]), force=True)
  634. with open(os.path.join(self.dir, "patches", "series"), "a") as f:
  635. f.write(os.path.basename(patch["file"]) + " -p" + patch["strippath"] + "\n")
  636. patch["quiltfile"] = self._quiltpatchpath(patch["file"])
  637. patch["quiltfilemd5"] = bb.utils.md5_file(patch["quiltfile"])
  638. # TODO: determine if the file being imported:
  639. # 1) is already imported, and is the same
  640. # 2) is already imported, but differs
  641. self.patches.insert(self._current or 0, patch)
  642. def Push(self, force = False, all = False, run = True):
  643. # quilt push [-f]
  644. args = ["push"]
  645. if force:
  646. args.append("-f")
  647. if all:
  648. args.append("-a")
  649. if not run:
  650. return self._runcmd(args, run)
  651. self._runcmd(args)
  652. if self._current is not None:
  653. self._current = self._current + 1
  654. else:
  655. self._current = 0
  656. def Pop(self, force = None, all = None):
  657. # quilt pop [-f]
  658. args = ["pop"]
  659. if force:
  660. args.append("-f")
  661. if all:
  662. args.append("-a")
  663. self._runcmd(args)
  664. if self._current == 0:
  665. self._current = None
  666. if self._current is not None:
  667. self._current = self._current - 1
  668. def Refresh(self, **kwargs):
  669. if kwargs.get("remote"):
  670. patch = self.patches[kwargs["patch"]]
  671. if not patch:
  672. raise PatchError("No patch found at index %s in patchset." % kwargs["patch"])
  673. (type, host, path, user, pswd, parm) = bb.fetch.decodeurl(patch["remote"])
  674. if type == "file":
  675. import shutil
  676. if not patch.get("file") and patch.get("remote"):
  677. patch["file"] = bb.fetch2.localpath(patch["remote"], self.d)
  678. shutil.copyfile(patch["quiltfile"], patch["file"])
  679. else:
  680. raise PatchError("Unable to do a remote refresh of %s, unsupported remote url scheme %s." % (os.path.basename(patch["quiltfile"]), type))
  681. else:
  682. # quilt refresh
  683. args = ["refresh"]
  684. if kwargs.get("quiltfile"):
  685. args.append(os.path.basename(kwargs["quiltfile"]))
  686. elif kwargs.get("patch"):
  687. args.append(os.path.basename(self.patches[kwargs["patch"]]["quiltfile"]))
  688. self._runcmd(args)
  689. class Resolver(object):
  690. def __init__(self, patchset, terminal):
  691. raise NotImplementedError()
  692. def Resolve(self):
  693. raise NotImplementedError()
  694. def Revert(self):
  695. raise NotImplementedError()
  696. def Finalize(self):
  697. raise NotImplementedError()
  698. class NOOPResolver(Resolver):
  699. def __init__(self, patchset, terminal):
  700. self.patchset = patchset
  701. self.terminal = terminal
  702. def Resolve(self):
  703. olddir = os.path.abspath(os.curdir)
  704. os.chdir(self.patchset.dir)
  705. try:
  706. self.patchset.Push()
  707. except Exception:
  708. import sys
  709. raise
  710. finally:
  711. os.chdir(olddir)
  712. # Patch resolver which relies on the user doing all the work involved in the
  713. # resolution, with the exception of refreshing the remote copy of the patch
  714. # files (the urls).
  715. class UserResolver(Resolver):
  716. def __init__(self, patchset, terminal):
  717. self.patchset = patchset
  718. self.terminal = terminal
  719. # Force a push in the patchset, then drop to a shell for the user to
  720. # resolve any rejected hunks
  721. def Resolve(self):
  722. olddir = os.path.abspath(os.curdir)
  723. os.chdir(self.patchset.dir)
  724. try:
  725. self.patchset.Push(False)
  726. except CmdError as v:
  727. # Patch application failed
  728. patchcmd = self.patchset.Push(True, False, False)
  729. t = self.patchset.d.getVar('T')
  730. if not t:
  731. bb.msg.fatal("Build", "T not set")
  732. bb.utils.mkdirhier(t)
  733. import random
  734. rcfile = "%s/bashrc.%s.%s" % (t, str(os.getpid()), random.random())
  735. with open(rcfile, "w") as f:
  736. f.write("echo '*** Manual patch resolution mode ***'\n")
  737. f.write("echo 'Dropping to a shell, so patch rejects can be fixed manually.'\n")
  738. f.write("echo 'Run \"quilt refresh\" when patch is corrected, press CTRL+D to exit.'\n")
  739. f.write("echo ''\n")
  740. f.write(" ".join(patchcmd) + "\n")
  741. os.chmod(rcfile, 0o775)
  742. self.terminal("bash --rcfile " + rcfile, 'Patch Rejects: Please fix patch rejects manually', self.patchset.d)
  743. # Construct a new PatchSet after the user's changes, compare the
  744. # sets, checking patches for modifications, and doing a remote
  745. # refresh on each.
  746. oldpatchset = self.patchset
  747. self.patchset = oldpatchset.__class__(self.patchset.dir, self.patchset.d)
  748. for patch in self.patchset.patches:
  749. oldpatch = None
  750. for opatch in oldpatchset.patches:
  751. if opatch["quiltfile"] == patch["quiltfile"]:
  752. oldpatch = opatch
  753. if oldpatch:
  754. patch["remote"] = oldpatch["remote"]
  755. if patch["quiltfile"] == oldpatch["quiltfile"]:
  756. if patch["quiltfilemd5"] != oldpatch["quiltfilemd5"]:
  757. bb.note("Patch %s has changed, updating remote url %s" % (os.path.basename(patch["quiltfile"]), patch["remote"]))
  758. # user change? remote refresh
  759. self.patchset.Refresh(remote=True, patch=self.patchset.patches.index(patch))
  760. else:
  761. # User did not fix the problem. Abort.
  762. raise PatchError("Patch application failed, and user did not fix and refresh the patch.")
  763. except Exception:
  764. raise
  765. finally:
  766. os.chdir(olddir)
  767. def patch_path(url, fetch, unpackdir, expand=True):
  768. """Return the local path of a patch, or return nothing if this isn't a patch"""
  769. local = fetch.localpath(url)
  770. if os.path.isdir(local):
  771. return
  772. base, ext = os.path.splitext(os.path.basename(local))
  773. if ext in ('.gz', '.bz2', '.xz', '.Z'):
  774. if expand:
  775. local = os.path.join(unpackdir, base)
  776. ext = os.path.splitext(base)[1]
  777. urldata = fetch.ud[url]
  778. if "apply" in urldata.parm:
  779. apply = oe.types.boolean(urldata.parm["apply"])
  780. if not apply:
  781. return
  782. elif ext not in (".diff", ".patch"):
  783. return
  784. return local
  785. def src_patches(d, all=False, expand=True):
  786. unpackdir = d.getVar('UNPACKDIR')
  787. fetch = bb.fetch2.Fetch([], d)
  788. patches = []
  789. sources = []
  790. for url in fetch.urls:
  791. local = patch_path(url, fetch, unpackdir, expand)
  792. if not local:
  793. if all:
  794. local = fetch.localpath(url)
  795. sources.append(local)
  796. continue
  797. urldata = fetch.ud[url]
  798. parm = urldata.parm
  799. patchname = parm.get('pname') or os.path.basename(local)
  800. apply, reason = should_apply(parm, d)
  801. if not apply:
  802. if reason:
  803. bb.note("Patch %s %s" % (patchname, reason))
  804. continue
  805. patchparm = {'patchname': patchname}
  806. if "striplevel" in parm:
  807. striplevel = parm["striplevel"]
  808. elif "pnum" in parm:
  809. #bb.msg.warn(None, "Deprecated usage of 'pnum' url parameter in '%s', please use 'striplevel'" % url)
  810. striplevel = parm["pnum"]
  811. else:
  812. striplevel = '1'
  813. patchparm['striplevel'] = striplevel
  814. patchdir = parm.get('patchdir')
  815. if patchdir:
  816. patchparm['patchdir'] = patchdir
  817. localurl = bb.fetch.encodeurl(('file', '', local, '', '', patchparm))
  818. patches.append(localurl)
  819. if all:
  820. return sources
  821. return patches
  822. def should_apply(parm, d):
  823. import bb.utils
  824. if "mindate" in parm or "maxdate" in parm:
  825. pn = d.getVar('PN')
  826. srcdate = d.getVar('SRCDATE_%s' % pn)
  827. if not srcdate:
  828. srcdate = d.getVar('SRCDATE')
  829. if srcdate == "now":
  830. srcdate = d.getVar('DATE')
  831. if "maxdate" in parm and parm["maxdate"] < srcdate:
  832. return False, 'is outdated'
  833. if "mindate" in parm and parm["mindate"] > srcdate:
  834. return False, 'is predated'
  835. if "minrev" in parm:
  836. srcrev = d.getVar('SRCREV')
  837. if srcrev and srcrev < parm["minrev"]:
  838. return False, 'applies to later revisions'
  839. if "maxrev" in parm:
  840. srcrev = d.getVar('SRCREV')
  841. if srcrev and srcrev > parm["maxrev"]:
  842. return False, 'applies to earlier revisions'
  843. if "rev" in parm:
  844. srcrev = d.getVar('SRCREV')
  845. if srcrev and parm["rev"] not in srcrev:
  846. return False, "doesn't apply to revision"
  847. if "notrev" in parm:
  848. srcrev = d.getVar('SRCREV')
  849. if srcrev and parm["notrev"] in srcrev:
  850. return False, "doesn't apply to revision"
  851. if "maxver" in parm:
  852. pv = d.getVar('PV')
  853. if bb.utils.vercmp_string_op(pv, parm["maxver"], ">"):
  854. return False, "applies to earlier version"
  855. if "minver" in parm:
  856. pv = d.getVar('PV')
  857. if bb.utils.vercmp_string_op(pv, parm["minver"], "<"):
  858. return False, "applies to later version"
  859. return True, None