You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1012 lines
37 KiB

  1. #-*- coding: utf-8 -*-
  2. # Copyright 2014 Calculate Ltd. http://www.calculate-linux.org
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from collections import Mapping, defaultdict
  16. import re
  17. import sys
  18. from calculate.lib.cl_template import iniParser
  19. from calculate.lib.utils.colortext.palette import TextState
  20. from calculate.lib.utils.tools import AddonError, SavableIterator
  21. import time
  22. import datetime
  23. Colors = TextState.Colors
  24. import pexpect
  25. from os import path
  26. from calculate.lib.utils.files import (getProgPath, STDOUT,
  27. PercentProgress, process, readFile,
  28. readLinesFile)
  29. from calculate.lib.utils.common import cmpVersion
  30. from calculate.lib.utils.tools import ignore
  31. from contextlib import closing
  32. import xml.etree.ElementTree as ET
  33. from calculate.lib.cl_lang import setLocalTranslate, getLazyLocalTranslate
  34. from functools import total_ordering
  35. from itertools import ifilter, imap, chain
  36. setLocalTranslate('cl_update3', sys.modules[__name__])
  37. __ = getLazyLocalTranslate(_)
  38. class GitError(AddonError):
  39. """Git Error"""
  40. class Layman:
  41. """
  42. Объект для управления репозиториями Layman
  43. Args:
  44. installed: путь до installed.xml
  45. makeconf: путь до makeconf
  46. """
  47. def __init__(self, installed, makeconf):
  48. self.installed = installed
  49. self.makeconf = makeconf
  50. def _add_to_installed(self, rname, rurl):
  51. """
  52. Добавить репозиторий в installed.xml
  53. """
  54. if path.exists(self.installed) and readFile(self.installed).strip():
  55. tree = ET.parse(self.installed)
  56. root = tree.getroot()
  57. # если репозиторий уже присутсвует в installed.xml
  58. if root.find("repo[name='%s']" % rname) is not None:
  59. return
  60. else:
  61. root = ET.Element("repositories", version="1.0")
  62. tree = ET.ElementTree(root)
  63. newrepo = ET.SubElement(root, "repo", priority="50",
  64. quality="experimental",
  65. status="unofficial")
  66. name = ET.SubElement(newrepo, "name")
  67. name.text = rname
  68. source = ET.SubElement(newrepo, "source", type="git")
  69. source.text = rurl
  70. try:
  71. from layman.utils import indent
  72. indent(root)
  73. except ImportError as e:
  74. pass
  75. with open(self.installed, 'w') as f:
  76. f.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  77. tree.write(f, encoding="utf-8")
  78. def _add_to_makeconf(self, rpath):
  79. """
  80. Добавить репозиторий в layman/make.conf
  81. """
  82. def fixContent(match):
  83. repos = match.group(1).strip().split('\n')
  84. if not rpath in repos:
  85. repos.insert(0, rpath)
  86. return 'PORTDIR_OVERLAY="\n%s"' % "\n".join(repos);
  87. if path.exists(self.makeconf):
  88. content = readFile(self.makeconf)
  89. if "PORTDIR_OVERLAY" in content:
  90. new_content = re.sub("\APORTDIR_OVERLAY=\"([^\"]+)\"",
  91. fixContent, content, re.DOTALL)
  92. if new_content == content:
  93. return
  94. else:
  95. content = new_content
  96. else:
  97. content = 'PORTDIR_OVERLAY="\n%s"\n' % rpath + content
  98. else:
  99. content = 'PORTDIR_OVERLAY="\n%s"\n' % rpath
  100. with open(self.makeconf, 'w') as f:
  101. f.write(content)
  102. def add(self, rname, rurl, rpath):
  103. """
  104. Добавить репозиторий в installed.xml и layman/make.conf
  105. """
  106. self._add_to_installed(rname, rurl)
  107. self._add_to_makeconf(rpath)
  108. return True
  109. def get_installed(self):
  110. """
  111. Получить список установленных репозиториев
  112. """
  113. if path.exists(self.installed) and readFile(self.installed).strip():
  114. tree = ET.parse(self.installed)
  115. return [x.text for x in tree.findall("repo/name")]
  116. return []
  117. class Git:
  118. """
  119. Объект для управление git репозиторием
  120. """
  121. skip_files = ["metadata/md5-cache",
  122. "metadata/cache"]
  123. def __init__(self):
  124. self._git = self.__getGit()
  125. def checkExistsRep(self, rpath):
  126. """
  127. Проверить путь на наличие репозитория
  128. """
  129. if path.exists(rpath):
  130. if not path.isdir(rpath):
  131. raise GitError(
  132. _("Repository {path} is not a directory").format(
  133. path=rpath))
  134. if not path.isdir(self._gitDir(rpath)):
  135. raise GitError(
  136. _("Repository {path} is not Git").format(
  137. path=rpath))
  138. return True
  139. return False
  140. def __getGit(self):
  141. """
  142. Получить утилиту git
  143. """
  144. git = getProgPath("/usr/bin/git")
  145. if not git:
  146. raise GitError(_("The Git tool is not found"))
  147. return git
  148. @staticmethod
  149. def _gitDir(rpath):
  150. return path.join(rpath, ".git")
  151. @staticmethod
  152. def is_git(gitpath):
  153. return path.isdir(Git._gitDir(gitpath))
  154. def get_url(self, rpath, remote_name):
  155. ini_parser = iniParser(path.join(Git._gitDir(rpath), "config"))
  156. return ini_parser.getVar(u'remote "%s"'%remote_name, "url")
  157. def cloneRepository(self, url, rpath, branch, cb_progress=None):
  158. """
  159. Сделать локальную копию репозитория
  160. Args:
  161. url: откуда качать репозиторий
  162. rpath: куда сохранять репозиторий
  163. branch: ветка на которую необходимо переключиться
  164. """
  165. if cb_progress:
  166. gitClone = PercentProgress(self._git, "clone", "-q",
  167. "--no-single-branch", "--progress",
  168. "--verbose",
  169. "--depth=1", "-b", branch, url, rpath,
  170. part=4, stderr=STDOUT)
  171. for perc in gitClone.progress():
  172. cb_progress(perc)
  173. else:
  174. gitClone = process(self._git, "clone", "-q", "--no-single-branch",
  175. "--depth=1", "-b", branch, url, rpath,
  176. stderr=STDOUT)
  177. if gitClone.failed():
  178. error = gitClone.read()
  179. if "Remote branch %s not found" % branch in error:
  180. raise GitError(
  181. _("Branch {branch} not found in repository {url}").format(
  182. branch=branch, url=url))
  183. raise GitError(_("Failed to clone repository {url}").format(
  184. url=url), error)
  185. return True
  186. def cloneRevRepository(self, url, rpath, branch, revision,
  187. cb_progress=None):
  188. """
  189. Сделать локальную копию репозитория с указанной ревизией
  190. Args:
  191. url: откуда качать репозиторий
  192. rpath: куда сохранять репозиторий
  193. branch: ветка на которую необходимо переключиться
  194. revision: если указана - сделать ревизию текущей
  195. Return:
  196. Возвращает True если клонирование произведено с установкой на
  197. указанную ревизию. False если клонирование произведено с
  198. установкой на последнюю ревизию.
  199. Raises:
  200. GitError: Выполнение ключевых команд выполнено с ошибками (не
  201. удалось скачать и получить данные из удаленного репозитория)
  202. """
  203. git_dir = self._gitDir(rpath)
  204. error = []
  205. def command(cmd, startpart=0):
  206. """
  207. Выполнить одну из команд необходимой для клонирования репозитория
  208. """
  209. commands = { # инициализация пустого репозитория
  210. 'init': ["init", rpath],
  211. # подключить указанный удаленный как origin
  212. 'add_remote': ["remote", "add", "origin", url],
  213. # скачать последние коммиты веток
  214. 'fetchshallow': ["fetch", "--depth=1"],
  215. # проверить есть указанный коммит
  216. 'has_revision': ["log", "-n1", revision],
  217. # проверить есть указанный коммит
  218. 'has_branch': ["log", "-n1",
  219. "remotes/origin/%s" % branch],
  220. # получить ревизию из revs тэгов
  221. 'get_rev_tag': ["fetch", "--depth=1", "origin",
  222. "+refs/revs/%s:refs/remotes/origin/%s" %
  223. (revision, branch)],
  224. # переключиться на указанную ревизию указанной веткой
  225. 'checkout_revision': ["checkout", "-b", branch,
  226. revision],
  227. # переключить на указанную ветку
  228. 'checkout': ["checkout", branch],
  229. # установить upstream для локальной ветки
  230. 'set_track': ["branch", branch, '-u',
  231. "origin/%s" % branch]
  232. }
  233. if cmd == "init":
  234. wholeCommand = [self._git] + commands[cmd]
  235. else:
  236. wholeCommand = [self._git, "--git-dir", git_dir,
  237. "--work-tree", rpath] + commands[cmd]
  238. if cb_progress and commands[cmd][0] in ("fetch", "checkout"):
  239. progressParam = {'fetch': {'part': 4, 'end': False},
  240. 'checkout': {'part': 4, 'startpart': 3}}
  241. gitClone = PercentProgress(
  242. *wholeCommand + ["--progress", "--verbose"],
  243. stderr=STDOUT, **progressParam)
  244. for perc in gitClone.progress():
  245. cb_progress(perc)
  246. else:
  247. gitCmd = process(*wholeCommand, stderr=STDOUT)
  248. if gitCmd.failed():
  249. error.append(gitCmd.read())
  250. return False
  251. return True
  252. # получить последние коммиты из удаленного репозитория
  253. if command("init") and command("add_remote"):
  254. if command("get_rev_tag") or command("fetchshallow"):
  255. if not command("has_branch"):
  256. raise GitError(
  257. _("Branch {branch} not found in repository {url}"
  258. ).format(branch=branch, url=url))
  259. # если среди коммитов есть указанный коммит
  260. if command("has_revision"):
  261. # переключаемся на нужный коммита, устанавливаем связь
  262. if command("checkout_revision") and command("set_track"):
  263. return True
  264. elif command("checkout"):
  265. return False
  266. raise GitError(_("Failed to clone repository {url}").format(
  267. url=url), error[-1])
  268. def pullRepository(self, rpath, quiet_error=False, cb_progress=None):
  269. """
  270. Обновить репозиторий до последней версии
  271. """
  272. gitPull = process(self._git, "--git-dir", self._gitDir(rpath),
  273. "pull", "--ff-only", stderr=STDOUT)
  274. if gitPull.failed():
  275. if not quiet_error:
  276. error = gitPull.read()
  277. raise GitError(
  278. _("Failed to update the repository in {rpath}").format(
  279. rpath=rpath), error)
  280. return False
  281. return True
  282. def fetchRepository(self, rpath, cb_progress=None):
  283. """
  284. Получить изменения из удаленно репозитория
  285. """
  286. if cb_progress:
  287. gitFetch = PercentProgress(self._git, "--git-dir",
  288. self._gitDir(rpath),
  289. "fetch", "--progress", "--verbose",
  290. part=3, stderr=STDOUT)
  291. for perc in gitFetch.progress():
  292. cb_progress(perc)
  293. else:
  294. gitFetch = process(self._git, "--git-dir", self._gitDir(rpath),
  295. "fetch", stderr=STDOUT)
  296. if gitFetch.failed():
  297. error = gitFetch.read()
  298. raise GitError(
  299. _("Failed to update the repository in {rpath}").format(
  300. rpath=rpath), error)
  301. return True
  302. def checkChanges(self, rpath):
  303. """
  304. Проверить наличие изменений пользователем файлов в репозитории
  305. """
  306. git_dir = self._gitDir(rpath)
  307. git_status = process(self._git, "--git-dir", git_dir, "--work-tree",
  308. rpath,
  309. "status", "--porcelain", stderr=STDOUT)
  310. if git_status.success():
  311. return not any(x.strip() for x in git_status)
  312. else:
  313. raise GitError(
  314. _("Wrong repository in the {rpath} directory").format(
  315. rpath=rpath))
  316. def parseStatusInfo(self, data):
  317. """
  318. Разобрать информацию полученную через git status -b --porcelain
  319. Returns:
  320. Словарь
  321. # есть ли измененные файлы пользователем
  322. {'files':True/False,
  323. # есть коммиты после текущего
  324. 'ahead':True/False,
  325. # есть коммиты перед текущим (означает, что обновление
  326. # с основной веткой не осуществляется через fast-forward
  327. 'behind':True/False,
  328. # текущая ветка
  329. 'branch':'',
  330. # оригинальная ветка
  331. 'origin':'origin/master'}
  332. """
  333. reStatus = re.compile("^## (\w+)(?:\.\.\.(\S+))?(?:\s+\[(ahead \d+)?"
  334. "(?:, )?(behind \d+)?\])?\n?(.*|$)", re.S)
  335. match = reStatus.search(data)
  336. if not match:
  337. return {}
  338. return {'files': True if match.group(5) else False,
  339. 'ahead': True if match.group(3) else False,
  340. 'behind': True if match.group(4) else False,
  341. 'origin': match.group(2) or "",
  342. 'branch': match.group(1)}
  343. def getCurrentCommit(self, rpath):
  344. """
  345. Получить текущий коммит в репозитории
  346. """
  347. git_dir = self._gitDir(rpath)
  348. git_show = process(self._git, "--git-dir", git_dir, "show",
  349. "--format=format:%H",
  350. "--quiet", stderr=STDOUT)
  351. if git_show.success():
  352. return git_show.read().strip()
  353. else:
  354. raise GitError(
  355. _("Failed to get the repository status for {rpath}").format(
  356. rpath=rpath))
  357. def getStatusInfo(self, rpath):
  358. """
  359. Получить информацию об изменениях в репозитории
  360. Returns:
  361. Словарь выдаваемый функцией _parseStatusInfo
  362. """
  363. git_dir = self._gitDir(rpath)
  364. git_status = process(self._git, "--git-dir", git_dir, "--work-tree",
  365. rpath,
  366. "status", "-b", "--porcelain", stderr=STDOUT)
  367. if git_status.success():
  368. retDict = self.parseStatusInfo(git_status.read())
  369. if not retDict:
  370. raise GitError(
  371. _("Failed to get the repository status for {rpath}").format(
  372. rpath=rpath))
  373. return retDict
  374. else:
  375. raise GitError(
  376. _("Wrong repository in the {rpath} directory").format(
  377. rpath=rpath))
  378. def resetRepository(self, rpath, to_origin=False, to_rev=None, info=None):
  379. """
  380. Удалить неиндексированные изменения в репозитории
  381. Args:
  382. to_origin: откатить все изменения до удаленного репозитория
  383. to_rev: откатить все изменения до определенной ревизии
  384. info: использовать уже полученную информация об изменения в репозитории
  385. Return:
  386. True - успешное выполнение
  387. False - нет необходимости выполнять reset
  388. Raises:
  389. GitError: выполнение комманд reset и clean прошло с ошибкой
  390. """
  391. git_dir = self._gitDir(rpath)
  392. if not info:
  393. info = self.getStatusInfo(rpath)
  394. if (all(not info[x] for x in ("files", "ahead", "behind") if x in info)
  395. and (not info["origin"] or
  396. "origin/%s" % info["branch"] == info["origin"])):
  397. return False
  398. commit = (info['origin'] if to_origin else to_rev) or "HEAD"
  399. git_reset = process(self._git, "--git-dir", git_dir, "--work-tree",
  400. rpath,
  401. "reset", "--hard", commit, stderr=STDOUT)
  402. git_clean = process(self._git, "--git-dir", git_dir, "--work-tree",
  403. rpath,
  404. "clean", "-fd", stderr=STDOUT)
  405. if git_reset.failed() or git_clean.failed():
  406. raise GitError(_("Failed to clean the {rpath} repository").format(
  407. rpath=rpath))
  408. return True
  409. def getBranch(self, rpath):
  410. """
  411. Получить текущую ветку
  412. """
  413. return self.getStatusInfo(rpath)['branch']
  414. def checkoutBranch(self, rpath, branch):
  415. """
  416. Сменить ветку
  417. """
  418. git_dir = self._gitDir(rpath)
  419. git_checkout = process(self._git, "--git-dir", git_dir,
  420. "--work-tree", rpath,
  421. "checkout", "-f", branch, stderr=STDOUT)
  422. if git_checkout.failed():
  423. error = git_checkout.read()
  424. if "pathspec '%s' did not match" % branch in error:
  425. raise GitError(
  426. _("Branch {branch} not found in repository {rpath}").format(
  427. branch=branch, rpath=rpath))
  428. raise GitError(
  429. _("Failed to change branch to {branch} in the {rpath} "
  430. "repository").format(branch=branch,
  431. rpath=rpath), error)
  432. return True
  433. @total_ordering
  434. class EmergePackage(Mapping):
  435. """
  436. Данные о пакете
  437. Item keys: CATEGORY, P, PN, PV, P, PF, PR, PVR
  438. Поддерживает сравнение объекта с другим таким же объектом по версии, либо
  439. со строкой, содержащей версию. Сравнение выполняется по категория/имя, затем
  440. по версии
  441. """
  442. default_repo = 'gentoo'
  443. prefix = r"(?:.*/var/db/pkg/|=)?"
  444. category = r"(?:(\w+(?:-\w+)?)/)?"
  445. pn = "([^/]*?)"
  446. pv = r"(?:-(\d[^-]*?))?"
  447. pr = r"(?:-(r\d+))?"
  448. tbz = r"(?:.(tbz2))?"
  449. slot = r'(?::(\w+(?:\.\w+)*(?:/\w+(?:\.\w+)*)?))?'
  450. repo = r'(?:::(\w+))?'
  451. reParse = re.compile(
  452. r'^{prefix}{category}(({pn}{pv}){pr}){slot}{repo}{tbz}$'.format(
  453. prefix=prefix, category=category, pn=pn, pv=pv, pr=pr, tbz=tbz,
  454. slot=slot, repo=repo))
  455. attrs = ('CATEGORY', 'PN', 'PF', 'SLOT', 'REPO', 'P', 'PV', 'PR', 'PVR',
  456. 'CATEGORY/PN')
  457. def _parsePackageString(self, s):
  458. """
  459. Преобразовать строка в части названия пакета
  460. """
  461. x = self.reParse.search(s)
  462. if x:
  463. CATEGORY, PF, P, PN, PV, PR, SLOT, REPO, TBZ = range(0, 9)
  464. x = x.groups()
  465. d = {'CATEGORY': x[CATEGORY] or "",
  466. 'PN': x[PN],
  467. 'PV': x[PV] or '0',
  468. 'PF': x[PF],
  469. 'P': x[P],
  470. 'SLOT': x[SLOT] or '0',
  471. 'REPO': x[REPO] or self.default_repo,
  472. 'CATEGORY/PN': "%s/%s" % (x[CATEGORY], x[PN]),
  473. 'PR': x[PR] or 'r0'}
  474. if x[PR]:
  475. d['PVR'] = "%s-%s" % (d['PV'], d['PR'])
  476. else:
  477. d['PVR'] = d['PV']
  478. if d['PF'].endswith('-r0'):
  479. d['PF'] = d['PF'][:-3]
  480. return d.copy()
  481. else:
  482. return {k: '' for k in self.attrs}
  483. def __iter__(self):
  484. return iter(self.attrs)
  485. def __len__(self):
  486. if not self['PN']:
  487. return 0
  488. else:
  489. return len(self.attrs)
  490. def __lt__(self, version):
  491. """
  492. В объектах сравнивается совпадение категории и PF
  493. """
  494. if "CATEGORY/PN" in version and "PVR" in version:
  495. if self['CATEGORY/PN'] < version['CATEGORY/PN']:
  496. return True
  497. elif self['CATEGORY/PN'] > version['CATEGORY/PN']:
  498. return False
  499. version = "%s-%s" % (version['PV'], version['PR'])
  500. currentVersion = "%s-%s" % (self['PV'], self['PR'])
  501. return cmpVersion(currentVersion, version) == -1
  502. def __eq__(self, version):
  503. if "CATEGORY" in version and "PF" in version:
  504. return ("%s/%s" % (self['CATEGORY'], self['PF']) ==
  505. "%s/%s" % (version['CATEGORY'], version['PF']))
  506. else:
  507. currentVersion = "%s-%s" % (self['PV'], self['PR'])
  508. return cmpVersion(currentVersion, version) == 0
  509. def __init__(self, package):
  510. if isinstance(package, EmergePackage):
  511. self.__result = package.__result
  512. self._package = package._package
  513. else:
  514. self._package = package
  515. self.__result = None
  516. def __getitem__(self, item):
  517. if not self.__result:
  518. self.__result = self._parsePackageString(self._package)
  519. return self.__result[item]
  520. def __repr__(self):
  521. return "EmergePackage(%s/%s)" % (self['CATEGORY'], self['PF'])
  522. def __str__(self):
  523. return "%s/%s" % (self['CATEGORY'], self['PF'])
  524. class PackageInformation:
  525. """
  526. Объект позволяет получать информацию о пакете из eix
  527. """
  528. eix_cmd = getProgPath("/usr/bin/eix")
  529. query_packages = []
  530. information_cache = defaultdict(dict)
  531. fields = ["DESCRIPTION"]
  532. def __init__(self, pkg):
  533. self._pkg = pkg
  534. if not pkg in self.query_packages:
  535. self.query_packages.append(pkg)
  536. def __getitem__(self, item):
  537. if not self._pkg['CATEGORY/PN'] in self.information_cache and \
  538. self.query_packages:
  539. self.query_information()
  540. try:
  541. return self.information_cache[self._pkg['CATEGORY/PN']][item]
  542. except KeyError:
  543. return ""
  544. def query_information(self):
  545. pkg_list = "|".join(
  546. [x['CATEGORY/PN'].replace("+", r"\+") for x in self.query_packages])
  547. output = pexpect.spawn(self.eix_cmd, ["--xml", pkg_list]).read()
  548. re_cut = re.compile("^.*?(?=<\?xml version)",re.S)
  549. with ignore(ET.ParseError):
  550. xml = ET.fromstring(re_cut.sub('',output))
  551. for pkg in self.query_packages:
  552. cat_pn = pkg['CATEGORY/PN']
  553. if not cat_pn in self.information_cache:
  554. descr_node = xml.find(
  555. 'category[@name="%s"]/package[@name="%s"]/description'
  556. % (pkg['CATEGORY'], pkg['PN']))
  557. if descr_node is not None:
  558. self.information_cache[cat_pn]['DESCRIPTION'] = \
  559. descr_node.text
  560. while self.query_packages:
  561. self.query_packages.pop()
  562. @classmethod
  563. def add_info(cls, pkg):
  564. pkg.info = cls(pkg)
  565. return pkg
  566. class UnmergePackage(EmergePackage):
  567. """
  568. Информация об обновлении одного пакета
  569. """
  570. re_pkg_info = re.compile("^\s(\S+)\n\s+selected:\s(\S+)",re.MULTILINE)
  571. def __init__(self, package):
  572. super(UnmergePackage, self).__init__(package)
  573. if not isinstance(package, EmergePackage):
  574. self._package = self.convert_package_info(package)
  575. def convert_package_info(self, package):
  576. match = self.re_pkg_info.search(package)
  577. if match:
  578. return "%s-%s" % match.groups()
  579. return ""
  580. def recalculate_update_info(cls):
  581. """
  582. Добавить
  583. """
  584. cls.update_info = re.compile(
  585. r"^({install_info})\s+({atom_info})\s*(?:{prev_version})?"
  586. r"\s*({use_info})?.*?({pkg_size})?$".format(
  587. install_info=cls.install_info,
  588. atom_info=cls.atom_info,
  589. prev_version=cls.prev_version,
  590. use_info=cls.use_info,
  591. pkg_size=cls.pkg_size), re.MULTILINE)
  592. return cls
  593. @recalculate_update_info
  594. @total_ordering
  595. class EmergeUpdateInfo(Mapping):
  596. """
  597. Информация об обновлении одного пакета
  598. """
  599. install_info = "\[(binary|ebuild)([^\]]+)\]"
  600. atom_info = r"\S+"
  601. use_info = 'USE="[^"]+"'
  602. prev_version = "\[([^\]]+)\]"
  603. pkg_size = r"[\d,]+ \w+"
  604. attrs = ['binary', 'REPLACING_VERSIONS', 'SIZE', 'new', 'newslot',
  605. 'updating', 'downgrading', 'reinstall']
  606. def __init__(self, data):
  607. self._data = data
  608. self._package = None
  609. self._info = {}
  610. def _parseData(self):
  611. r = self.update_info.search(self._data)
  612. if r:
  613. self._info['binary'] = r.group(2) == 'binary'
  614. install_flag = r.group(3)
  615. self._info['newslot'] = "S" in install_flag
  616. self._info['new'] = "N" in install_flag and not "S" in install_flag
  617. self._info['updating'] = ("U" in install_flag and
  618. not "D" in install_flag)
  619. self._info['downgrading'] = "D" in install_flag
  620. self._info['reinstall'] = "R" in install_flag
  621. self._package = EmergePackage(r.group(4))
  622. self._info['REPLACING_VERSIONS'] = r.group(5) or ""
  623. self._info['SIZE'] = r.group(7) or ""
  624. def __iter__(self):
  625. return chain(EmergePackage.attrs, self.attrs)
  626. def __len__(self):
  627. if not self['PN']:
  628. return 0
  629. else:
  630. return len(EmergePackage.attrs) + len(self.attrs)
  631. def __getitem__(self, item):
  632. if not self._info:
  633. self._parseData()
  634. if item in self._info:
  635. return self._info[item]
  636. if self._package:
  637. return self._package[item]
  638. return None
  639. def __lt__(self, version):
  640. if not self._info:
  641. self._parseData()
  642. return self._package < version
  643. def __eq__(self, version):
  644. if not self._info:
  645. self._parseData()
  646. return self._package == version
  647. def __contains__(self, item):
  648. if not self._info:
  649. self._parseData()
  650. return item in self.attrs or item in self._package
  651. def __repr__(self):
  652. return "EmergeUpdateInfo(%s/%s,%s)" % (
  653. self['CATEGORY'], self['PF'],
  654. "binary" if self.binary else "ebuild")
  655. def __str__(self):
  656. return "%s/%s" % (self['CATEGORY'], self['PF'])
  657. @recalculate_update_info
  658. class EmergeRemoveInfo(EmergeUpdateInfo):
  659. """
  660. Информация об удалении одного пакета (в списке обновляемых пакетов)
  661. """
  662. install_info = "\[(uninstall)([^\]]+)\]"
  663. class Eix:
  664. """
  665. Вызов eix
  666. package : пакет или список пакетов
  667. *options : параметры eix
  668. all_versions : отобразить все версии пакета или наилучшую
  669. """
  670. cmd = getProgPath("/usr/bin/eix")
  671. class Option:
  672. Installed = '--installed'
  673. Xml = '--xml'
  674. Upgrade = '--upgrade'
  675. default_options = [Option.Xml]
  676. def __init__(self, package, *options, **kwargs):
  677. if type(package) in (tuple, list):
  678. self.package = list(package)
  679. else:
  680. self.package = [package]
  681. self.options = list(options) + self.package + self.default_options
  682. if not kwargs.get('all_versions', False):
  683. self.__get_versions = self._get_versions
  684. self._get_versions = self._get_best_version
  685. def _get_best_version(self, et):
  686. ret = None
  687. for ver in ifilter(lambda x: x.find('mask') is None,
  688. et.iterfind('version')):
  689. ret = ver.attrib['id']
  690. yield ret
  691. def get_output(self):
  692. """
  693. Получить вывод eix
  694. """
  695. with closing(process(self.cmd, *self.options)) as p:
  696. return p.read()
  697. def get_packages(self):
  698. """
  699. Получить список пакетов
  700. """
  701. return list(self._parseXml(self.get_output()))
  702. def _get_versions(self, et):
  703. for ver in et.iterfind('version'):
  704. yield ver.attrib['id']
  705. def _get_packages(self, et):
  706. for pkg in et:
  707. for version in self._get_versions(pkg):
  708. yield "%s-%s" % (pkg.attrib['name'], version)
  709. def _get_categories(self, et):
  710. for category in et:
  711. for pkg in self._get_packages(category):
  712. yield "%s/%s" % (category.attrib['name'], pkg)
  713. def _parseXml(self, buffer):
  714. try:
  715. eix_xml = ET.fromstring(buffer)
  716. return self._get_categories(eix_xml)
  717. except ET.ParseError:
  718. return iter(())
  719. class EmergeLogTask(object):
  720. def has_marker(self, line):
  721. """
  722. Определить есть ли в строке маркер задачи
  723. """
  724. return False
  725. def get_begin_marker(self):
  726. """
  727. Получить маркер начала задачи
  728. """
  729. return ""
  730. def get_end_marker(self):
  731. """
  732. Получить маркер завершения задачи
  733. """
  734. return ""
  735. class EmergeLogNamedTask(EmergeLogTask):
  736. date_format = "%b %d, %Y %T"
  737. def __init__(self, taskname):
  738. self.taskname = taskname
  739. def has_marker(self, line):
  740. """
  741. Определить есть ли в строке маркер задачи
  742. """
  743. return self.get_end_marker() in line
  744. def get_begin_marker(self):
  745. """
  746. Получить маркер начала задачи
  747. """
  748. return "Calculate: Started {taskname} on: {date}".format(
  749. taskname=self.taskname,
  750. date=datetime.datetime.now().strftime(self.date_format))
  751. def get_end_marker(self):
  752. """
  753. Получить маркер завершения задачи
  754. """
  755. return " *** Calculate: Finished %s" % self.taskname
  756. class EmergeLog:
  757. """
  758. EmergeLog(after).get_update(package_pattern)
  759. """
  760. emerge_log = "/var/log/emerge.log"
  761. re_complete_emerge = re.compile(r":::\scompleted (emerge) \(.*?\) (\S+)",
  762. re.M)
  763. re_complete_unmerge = re.compile(r">>>\s(unmerge) success: (\S+)", re.M)
  764. def __init__(self, emerge_task=EmergeLogTask()):
  765. """
  766. @type emerge_task: EmergeLogTask
  767. """
  768. self.emerge_task = emerge_task
  769. self._list = None
  770. self._remove_list = None
  771. def _get_last_changes(self):
  772. """
  773. Получить список измений по логу, от последней записи маркера
  774. """
  775. log_data = SavableIterator(iter(readLinesFile(self.emerge_log)))
  776. for line in log_data.save():
  777. if self.emerge_task.has_marker(line):
  778. log_data.save()
  779. return log_data.restore()
  780. def get_last_time(self):
  781. """
  782. Получить время послдней записи маркера
  783. """
  784. last_line = ""
  785. for line in readLinesFile(self.emerge_log):
  786. if self.emerge_task.has_marker(line):
  787. last_line = line
  788. return last_line
  789. @property
  790. def list(self):
  791. if self._list is None:
  792. self.get_packages()
  793. return self._list
  794. @property
  795. def remove_list(self):
  796. if self._remove_list is None:
  797. self.get_packages()
  798. return self._remove_list
  799. def get_packages(self):
  800. """
  801. Получить список пакетов
  802. """
  803. self._list, self._remove_list = \
  804. zip(*self._parse_log(self._get_last_changes()))
  805. self._list = filter(None,self._list)
  806. self._remove_list = filter(None, self._remove_list)
  807. def _parse_log(self, data):
  808. searcher = lambda x:(self.re_complete_emerge.search(x) or
  809. self.re_complete_unmerge.search(x))
  810. for re_match in ifilter(None, imap(searcher, data)):
  811. if re_match.group(1) == "emerge":
  812. yield re_match.group(2), None
  813. else:
  814. yield None, re_match.group(2)
  815. yield None, None
  816. def _set_marker(self, text_marker):
  817. with open(self.emerge_log, 'a') as f:
  818. f.write("{0:.0f}: {1}\n".format(time.time(), text_marker))
  819. def mark_begin_task(self):
  820. """
  821. Отметить в emerge.log начало выполнения задачи
  822. """
  823. marker = self.emerge_task.get_begin_marker()
  824. if marker:
  825. self._set_marker(marker)
  826. def mark_end_task(self):
  827. """
  828. Отметить в emerge.log завершение выполнения задачи
  829. """
  830. marker = self.emerge_task.get_end_marker()
  831. if marker:
  832. self._set_marker(marker)
  833. class PackageList(object):
  834. """
  835. Список пакетов с возможностью среза и сравнением с версией
  836. """
  837. def __init__(self, packages):
  838. self._raw_list = packages
  839. self.result = None
  840. def _packages(self):
  841. if self.result is None:
  842. self.result = filter(lambda x: x['PN'],
  843. imap(lambda x: (x if isinstance(x, Mapping)
  844. else EmergePackage(x)),
  845. self._raw_list))
  846. return self.result
  847. def __getitem__(self, item):
  848. re_item = re.compile(item)
  849. return PackageList([pkg for pkg in self._packages() if
  850. re_item.search(pkg['CATEGORY/PN'])])
  851. def __iter__(self):
  852. return iter(self._packages())
  853. def __len__(self):
  854. return len(list(self._packages()))
  855. def __lt__(self, other):
  856. return any(pkg < other for pkg in self._packages())
  857. def __le__(self, other):
  858. return any(pkg <= other for pkg in self._packages())
  859. def __gt__(self, other):
  860. return any(pkg > other for pkg in self._packages())
  861. def __ge__(self, other):
  862. return any(pkg >= other for pkg in self._packages())
  863. def __eq__(self, other):
  864. for pkg in self._packages():
  865. if pkg == other:
  866. return True
  867. else:
  868. return False
  869. return any(pkg == other for pkg in self._packages())
  870. def __ne__(self, other):
  871. return any(pkg != other for pkg in self._packages())