pkg-stats 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  1. #!/usr/bin/env python
  2. # Copyright (C) 2009 by Thomas Petazzoni <thomas.petazzoni@free-electrons.com>
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. # General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
  17. import argparse
  18. import datetime
  19. import fnmatch
  20. import os
  21. from collections import defaultdict
  22. import re
  23. import subprocess
  24. import requests # URL checking
  25. import json
  26. import certifi
  27. from urllib3 import HTTPSConnectionPool
  28. from urllib3.exceptions import HTTPError
  29. from multiprocessing import Pool
  30. INFRA_RE = re.compile(r"\$\(eval \$\(([a-z-]*)-package\)\)")
  31. URL_RE = re.compile(r"\s*https?://\S*\s*$")
  32. RM_API_STATUS_ERROR = 1
  33. RM_API_STATUS_FOUND_BY_DISTRO = 2
  34. RM_API_STATUS_FOUND_BY_PATTERN = 3
  35. RM_API_STATUS_NOT_FOUND = 4
  36. # Used to make multiple requests to the same host. It is global
  37. # because it's used by sub-processes.
  38. http_pool = None
  39. class Package:
  40. all_licenses = list()
  41. all_license_files = list()
  42. all_versions = dict()
  43. def __init__(self, name, path):
  44. self.name = name
  45. self.path = path
  46. self.infras = None
  47. self.has_license = False
  48. self.has_license_files = False
  49. self.has_hash = False
  50. self.patch_count = 0
  51. self.warnings = 0
  52. self.current_version = None
  53. self.url = None
  54. self.url_status = None
  55. self.url_worker = None
  56. self.latest_version = (RM_API_STATUS_ERROR, None, None)
  57. def pkgvar(self):
  58. return self.name.upper().replace("-", "_")
  59. def set_url(self):
  60. """
  61. Fills in the .url field
  62. """
  63. self.url_status = "No Config.in"
  64. for filename in os.listdir(os.path.dirname(self.path)):
  65. if fnmatch.fnmatch(filename, 'Config.*'):
  66. fp = open(os.path.join(os.path.dirname(self.path), filename), "r")
  67. for config_line in fp:
  68. if URL_RE.match(config_line):
  69. self.url = config_line.strip()
  70. self.url_status = "Found"
  71. fp.close()
  72. return
  73. self.url_status = "Missing"
  74. fp.close()
  75. def set_infra(self):
  76. """
  77. Fills in the .infras field
  78. """
  79. self.infras = list()
  80. with open(self.path, 'r') as f:
  81. lines = f.readlines()
  82. for l in lines:
  83. match = INFRA_RE.match(l)
  84. if not match:
  85. continue
  86. infra = match.group(1)
  87. if infra.startswith("host-"):
  88. self.infras.append(("host", infra[5:]))
  89. else:
  90. self.infras.append(("target", infra))
  91. def set_license(self):
  92. """
  93. Fills in the .has_license and .has_license_files fields
  94. """
  95. var = self.pkgvar()
  96. if var in self.all_licenses:
  97. self.has_license = True
  98. if var in self.all_license_files:
  99. self.has_license_files = True
  100. def set_hash_info(self):
  101. """
  102. Fills in the .has_hash field
  103. """
  104. hashpath = self.path.replace(".mk", ".hash")
  105. self.has_hash = os.path.exists(hashpath)
  106. def set_patch_count(self):
  107. """
  108. Fills in the .patch_count field
  109. """
  110. self.patch_count = 0
  111. pkgdir = os.path.dirname(self.path)
  112. for subdir, _, _ in os.walk(pkgdir):
  113. self.patch_count += len(fnmatch.filter(os.listdir(subdir), '*.patch'))
  114. def set_current_version(self):
  115. """
  116. Fills in the .current_version field
  117. """
  118. var = self.pkgvar()
  119. if var in self.all_versions:
  120. self.current_version = self.all_versions[var]
  121. def set_check_package_warnings(self):
  122. """
  123. Fills in the .warnings field
  124. """
  125. cmd = ["./utils/check-package"]
  126. pkgdir = os.path.dirname(self.path)
  127. for root, dirs, files in os.walk(pkgdir):
  128. for f in files:
  129. if f.endswith(".mk") or f.endswith(".hash") or f == "Config.in" or f == "Config.in.host":
  130. cmd.append(os.path.join(root, f))
  131. o = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[1]
  132. lines = o.splitlines()
  133. for line in lines:
  134. m = re.match("^([0-9]*) warnings generated", line)
  135. if m:
  136. self.warnings = int(m.group(1))
  137. return
  138. def __eq__(self, other):
  139. return self.path == other.path
  140. def __lt__(self, other):
  141. return self.path < other.path
  142. def __str__(self):
  143. return "%s (path='%s', license='%s', license_files='%s', hash='%s', patches=%d)" % \
  144. (self.name, self.path, self.has_license, self.has_license_files, self.has_hash, self.patch_count)
  145. def get_pkglist(npackages, package_list):
  146. """
  147. Builds the list of Buildroot packages, returning a list of Package
  148. objects. Only the .name and .path fields of the Package object are
  149. initialized.
  150. npackages: limit to N packages
  151. package_list: limit to those packages in this list
  152. """
  153. WALK_USEFUL_SUBDIRS = ["boot", "linux", "package", "toolchain"]
  154. WALK_EXCLUDES = ["boot/common.mk",
  155. "linux/linux-ext-.*.mk",
  156. "package/freescale-imx/freescale-imx.mk",
  157. "package/gcc/gcc.mk",
  158. "package/gstreamer/gstreamer.mk",
  159. "package/gstreamer1/gstreamer1.mk",
  160. "package/gtk2-themes/gtk2-themes.mk",
  161. "package/matchbox/matchbox.mk",
  162. "package/opengl/opengl.mk",
  163. "package/qt5/qt5.mk",
  164. "package/x11r7/x11r7.mk",
  165. "package/doc-asciidoc.mk",
  166. "package/pkg-.*.mk",
  167. "package/nvidia-tegra23/nvidia-tegra23.mk",
  168. "toolchain/toolchain-external/pkg-toolchain-external.mk",
  169. "toolchain/toolchain-external/toolchain-external.mk",
  170. "toolchain/toolchain.mk",
  171. "toolchain/helpers.mk",
  172. "toolchain/toolchain-wrapper.mk"]
  173. packages = list()
  174. count = 0
  175. for root, dirs, files in os.walk("."):
  176. rootdir = root.split("/")
  177. if len(rootdir) < 2:
  178. continue
  179. if rootdir[1] not in WALK_USEFUL_SUBDIRS:
  180. continue
  181. for f in files:
  182. if not f.endswith(".mk"):
  183. continue
  184. # Strip ending ".mk"
  185. pkgname = f[:-3]
  186. if package_list and pkgname not in package_list:
  187. continue
  188. pkgpath = os.path.join(root, f)
  189. skip = False
  190. for exclude in WALK_EXCLUDES:
  191. # pkgpath[2:] strips the initial './'
  192. if re.match(exclude, pkgpath[2:]):
  193. skip = True
  194. continue
  195. if skip:
  196. continue
  197. p = Package(pkgname, pkgpath)
  198. packages.append(p)
  199. count += 1
  200. if npackages and count == npackages:
  201. return packages
  202. return packages
  203. def package_init_make_info():
  204. # Fetch all variables at once
  205. variables = subprocess.check_output(["make", "BR2_HAVE_DOT_CONFIG=y", "-s", "printvars",
  206. "VARS=%_LICENSE %_LICENSE_FILES %_VERSION"])
  207. variable_list = variables.splitlines()
  208. # We process first the host package VERSION, and then the target
  209. # package VERSION. This means that if a package exists in both
  210. # target and host variants, with different values (eg. version
  211. # numbers (unlikely)), we'll report the target one.
  212. variable_list = [x[5:] for x in variable_list if x.startswith("HOST_")] + \
  213. [x for x in variable_list if not x.startswith("HOST_")]
  214. for l in variable_list:
  215. # Get variable name and value
  216. pkgvar, value = l.split("=")
  217. # Strip the suffix according to the variable
  218. if pkgvar.endswith("_LICENSE"):
  219. # If value is "unknown", no license details available
  220. if value == "unknown":
  221. continue
  222. pkgvar = pkgvar[:-8]
  223. Package.all_licenses.append(pkgvar)
  224. elif pkgvar.endswith("_LICENSE_FILES"):
  225. if pkgvar.endswith("_MANIFEST_LICENSE_FILES"):
  226. continue
  227. pkgvar = pkgvar[:-14]
  228. Package.all_license_files.append(pkgvar)
  229. elif pkgvar.endswith("_VERSION"):
  230. if pkgvar.endswith("_DL_VERSION"):
  231. continue
  232. pkgvar = pkgvar[:-8]
  233. Package.all_versions[pkgvar] = value
  234. def check_url_status_worker(url, url_status):
  235. if url_status != "Missing" and url_status != "No Config.in":
  236. try:
  237. url_status_code = requests.head(url, timeout=30).status_code
  238. if url_status_code >= 400:
  239. return "Invalid(%s)" % str(url_status_code)
  240. except requests.exceptions.RequestException:
  241. return "Invalid(Err)"
  242. return "Ok"
  243. return url_status
  244. def check_package_urls(packages):
  245. Package.pool = Pool(processes=64)
  246. for pkg in packages:
  247. pkg.url_worker = pkg.pool.apply_async(check_url_status_worker, (pkg.url, pkg.url_status))
  248. for pkg in packages:
  249. pkg.url_status = pkg.url_worker.get(timeout=3600)
  250. def release_monitoring_get_latest_version_by_distro(pool, name):
  251. try:
  252. req = pool.request('GET', "/api/project/Buildroot/%s" % name)
  253. except HTTPError:
  254. return (RM_API_STATUS_ERROR, None, None)
  255. if req.status != 200:
  256. return (RM_API_STATUS_NOT_FOUND, None, None)
  257. data = json.loads(req.data)
  258. if 'version' in data:
  259. return (RM_API_STATUS_FOUND_BY_DISTRO, data['version'], data['id'])
  260. else:
  261. return (RM_API_STATUS_FOUND_BY_DISTRO, None, data['id'])
  262. def release_monitoring_get_latest_version_by_guess(pool, name):
  263. try:
  264. req = pool.request('GET', "/api/projects/?pattern=%s" % name)
  265. except HTTPError:
  266. return (RM_API_STATUS_ERROR, None, None)
  267. if req.status != 200:
  268. return (RM_API_STATUS_NOT_FOUND, None, None)
  269. data = json.loads(req.data)
  270. projects = data['projects']
  271. projects.sort(key=lambda x: x['id'])
  272. for p in projects:
  273. if p['name'] == name and 'version' in p:
  274. return (RM_API_STATUS_FOUND_BY_PATTERN, p['version'], p['id'])
  275. return (RM_API_STATUS_NOT_FOUND, None, None)
  276. def check_package_latest_version_worker(name):
  277. """Wrapper to try both by name then by guess"""
  278. print(name)
  279. res = release_monitoring_get_latest_version_by_distro(http_pool, name)
  280. if res[0] == RM_API_STATUS_NOT_FOUND:
  281. res = release_monitoring_get_latest_version_by_guess(http_pool, name)
  282. return res
  283. def check_package_latest_version(packages):
  284. """
  285. Fills in the .latest_version field of all Package objects
  286. This field has a special format:
  287. (status, version, id)
  288. with:
  289. - status: one of RM_API_STATUS_ERROR,
  290. RM_API_STATUS_FOUND_BY_DISTRO, RM_API_STATUS_FOUND_BY_PATTERN,
  291. RM_API_STATUS_NOT_FOUND
  292. - version: string containing the latest version known by
  293. release-monitoring.org for this package
  294. - id: string containing the id of the project corresponding to this
  295. package, as known by release-monitoring.org
  296. """
  297. global http_pool
  298. http_pool = HTTPSConnectionPool('release-monitoring.org', port=443,
  299. cert_reqs='CERT_REQUIRED', ca_certs=certifi.where(),
  300. timeout=30)
  301. worker_pool = Pool(processes=64)
  302. results = worker_pool.map(check_package_latest_version_worker, (pkg.name for pkg in packages))
  303. for pkg, r in zip(packages, results):
  304. pkg.latest_version = r
  305. del http_pool
  306. def calculate_stats(packages):
  307. stats = defaultdict(int)
  308. for pkg in packages:
  309. # If packages have multiple infra, take the first one. For the
  310. # vast majority of packages, the target and host infra are the
  311. # same. There are very few packages that use a different infra
  312. # for the host and target variants.
  313. if len(pkg.infras) > 0:
  314. infra = pkg.infras[0][1]
  315. stats["infra-%s" % infra] += 1
  316. else:
  317. stats["infra-unknown"] += 1
  318. if pkg.has_license:
  319. stats["license"] += 1
  320. else:
  321. stats["no-license"] += 1
  322. if pkg.has_license_files:
  323. stats["license-files"] += 1
  324. else:
  325. stats["no-license-files"] += 1
  326. if pkg.has_hash:
  327. stats["hash"] += 1
  328. else:
  329. stats["no-hash"] += 1
  330. if pkg.latest_version[0] == RM_API_STATUS_FOUND_BY_DISTRO:
  331. stats["rmo-mapping"] += 1
  332. else:
  333. stats["rmo-no-mapping"] += 1
  334. if not pkg.latest_version[1]:
  335. stats["version-unknown"] += 1
  336. elif pkg.latest_version[1] == pkg.current_version:
  337. stats["version-uptodate"] += 1
  338. else:
  339. stats["version-not-uptodate"] += 1
  340. stats["patches"] += pkg.patch_count
  341. return stats
  342. html_header = """
  343. <head>
  344. <script src=\"https://www.kryogenix.org/code/browser/sorttable/sorttable.js\"></script>
  345. <style type=\"text/css\">
  346. table {
  347. width: 100%;
  348. }
  349. td {
  350. border: 1px solid black;
  351. }
  352. td.centered {
  353. text-align: center;
  354. }
  355. td.wrong {
  356. background: #ff9a69;
  357. }
  358. td.correct {
  359. background: #d2ffc4;
  360. }
  361. td.nopatches {
  362. background: #d2ffc4;
  363. }
  364. td.somepatches {
  365. background: #ffd870;
  366. }
  367. td.lotsofpatches {
  368. background: #ff9a69;
  369. }
  370. td.good_url {
  371. background: #d2ffc4;
  372. }
  373. td.missing_url {
  374. background: #ffd870;
  375. }
  376. td.invalid_url {
  377. background: #ff9a69;
  378. }
  379. td.version-good {
  380. background: #d2ffc4;
  381. }
  382. td.version-needs-update {
  383. background: #ff9a69;
  384. }
  385. td.version-unknown {
  386. background: #ffd870;
  387. }
  388. td.version-error {
  389. background: #ccc;
  390. }
  391. </style>
  392. <title>Statistics of Buildroot packages</title>
  393. </head>
  394. <a href=\"#results\">Results</a><br/>
  395. <p id=\"sortable_hint\"></p>
  396. """
  397. html_footer = """
  398. </body>
  399. <script>
  400. if (typeof sorttable === \"object\") {
  401. document.getElementById(\"sortable_hint\").innerHTML =
  402. \"hint: the table can be sorted by clicking the column headers\"
  403. }
  404. </script>
  405. </html>
  406. """
  407. def infra_str(infra_list):
  408. if not infra_list:
  409. return "Unknown"
  410. elif len(infra_list) == 1:
  411. return "<b>%s</b><br/>%s" % (infra_list[0][1], infra_list[0][0])
  412. elif infra_list[0][1] == infra_list[1][1]:
  413. return "<b>%s</b><br/>%s + %s" % \
  414. (infra_list[0][1], infra_list[0][0], infra_list[1][0])
  415. else:
  416. return "<b>%s</b> (%s)<br/><b>%s</b> (%s)" % \
  417. (infra_list[0][1], infra_list[0][0],
  418. infra_list[1][1], infra_list[1][0])
  419. def boolean_str(b):
  420. if b:
  421. return "Yes"
  422. else:
  423. return "No"
  424. def dump_html_pkg(f, pkg):
  425. f.write(" <tr>\n")
  426. f.write(" <td>%s</td>\n" % pkg.path[2:])
  427. # Patch count
  428. td_class = ["centered"]
  429. if pkg.patch_count == 0:
  430. td_class.append("nopatches")
  431. elif pkg.patch_count < 5:
  432. td_class.append("somepatches")
  433. else:
  434. td_class.append("lotsofpatches")
  435. f.write(" <td class=\"%s\">%s</td>\n" %
  436. (" ".join(td_class), str(pkg.patch_count)))
  437. # Infrastructure
  438. infra = infra_str(pkg.infras)
  439. td_class = ["centered"]
  440. if infra == "Unknown":
  441. td_class.append("wrong")
  442. else:
  443. td_class.append("correct")
  444. f.write(" <td class=\"%s\">%s</td>\n" %
  445. (" ".join(td_class), infra_str(pkg.infras)))
  446. # License
  447. td_class = ["centered"]
  448. if pkg.has_license:
  449. td_class.append("correct")
  450. else:
  451. td_class.append("wrong")
  452. f.write(" <td class=\"%s\">%s</td>\n" %
  453. (" ".join(td_class), boolean_str(pkg.has_license)))
  454. # License files
  455. td_class = ["centered"]
  456. if pkg.has_license_files:
  457. td_class.append("correct")
  458. else:
  459. td_class.append("wrong")
  460. f.write(" <td class=\"%s\">%s</td>\n" %
  461. (" ".join(td_class), boolean_str(pkg.has_license_files)))
  462. # Hash
  463. td_class = ["centered"]
  464. if pkg.has_hash:
  465. td_class.append("correct")
  466. else:
  467. td_class.append("wrong")
  468. f.write(" <td class=\"%s\">%s</td>\n" %
  469. (" ".join(td_class), boolean_str(pkg.has_hash)))
  470. # Current version
  471. if len(pkg.current_version) > 20:
  472. current_version = pkg.current_version[:20] + "..."
  473. else:
  474. current_version = pkg.current_version
  475. f.write(" <td class=\"centered\">%s</td>\n" % current_version)
  476. # Latest version
  477. if pkg.latest_version[0] == RM_API_STATUS_ERROR:
  478. td_class.append("version-error")
  479. if pkg.latest_version[1] is None:
  480. td_class.append("version-unknown")
  481. elif pkg.latest_version[1] != pkg.current_version:
  482. td_class.append("version-needs-update")
  483. else:
  484. td_class.append("version-good")
  485. if pkg.latest_version[0] == RM_API_STATUS_ERROR:
  486. latest_version_text = "<b>Error</b>"
  487. elif pkg.latest_version[0] == RM_API_STATUS_NOT_FOUND:
  488. latest_version_text = "<b>Not found</b>"
  489. else:
  490. if pkg.latest_version[1] is None:
  491. latest_version_text = "<b>Found, but no version</b>"
  492. else:
  493. latest_version_text = "<a href=\"https://release-monitoring.org/project/%s\"><b>%s</b></a>" % \
  494. (pkg.latest_version[2], str(pkg.latest_version[1]))
  495. latest_version_text += "<br/>"
  496. if pkg.latest_version[0] == RM_API_STATUS_FOUND_BY_DISTRO:
  497. latest_version_text += "found by <a href=\"https://release-monitoring.org/distro/Buildroot/\">distro</a>"
  498. else:
  499. latest_version_text += "found by guess"
  500. f.write(" <td class=\"%s\">%s</td>\n" %
  501. (" ".join(td_class), latest_version_text))
  502. # Warnings
  503. td_class = ["centered"]
  504. if pkg.warnings == 0:
  505. td_class.append("correct")
  506. else:
  507. td_class.append("wrong")
  508. f.write(" <td class=\"%s\">%d</td>\n" %
  509. (" ".join(td_class), pkg.warnings))
  510. # URL status
  511. td_class = ["centered"]
  512. url_str = pkg.url_status
  513. if pkg.url_status == "Missing" or pkg.url_status == "No Config.in":
  514. td_class.append("missing_url")
  515. elif pkg.url_status.startswith("Invalid"):
  516. td_class.append("invalid_url")
  517. url_str = "<a href=%s>%s</a>" % (pkg.url, pkg.url_status)
  518. else:
  519. td_class.append("good_url")
  520. url_str = "<a href=%s>Link</a>" % pkg.url
  521. f.write(" <td class=\"%s\">%s</td>\n" %
  522. (" ".join(td_class), url_str))
  523. f.write(" </tr>\n")
  524. def dump_html_all_pkgs(f, packages):
  525. f.write("""
  526. <table class=\"sortable\">
  527. <tr>
  528. <td>Package</td>
  529. <td class=\"centered\">Patch count</td>
  530. <td class=\"centered\">Infrastructure</td>
  531. <td class=\"centered\">License</td>
  532. <td class=\"centered\">License files</td>
  533. <td class=\"centered\">Hash file</td>
  534. <td class=\"centered\">Current version</td>
  535. <td class=\"centered\">Latest version</td>
  536. <td class=\"centered\">Warnings</td>
  537. <td class=\"centered\">Upstream URL</td>
  538. </tr>
  539. """)
  540. for pkg in sorted(packages):
  541. dump_html_pkg(f, pkg)
  542. f.write("</table>")
  543. def dump_html_stats(f, stats):
  544. f.write("<a id=\"results\"></a>\n")
  545. f.write("<table>\n")
  546. infras = [infra[6:] for infra in stats.keys() if infra.startswith("infra-")]
  547. for infra in infras:
  548. f.write(" <tr><td>Packages using the <i>%s</i> infrastructure</td><td>%s</td></tr>\n" %
  549. (infra, stats["infra-%s" % infra]))
  550. f.write(" <tr><td>Packages having license information</td><td>%s</td></tr>\n" %
  551. stats["license"])
  552. f.write(" <tr><td>Packages not having license information</td><td>%s</td></tr>\n" %
  553. stats["no-license"])
  554. f.write(" <tr><td>Packages having license files information</td><td>%s</td></tr>\n" %
  555. stats["license-files"])
  556. f.write(" <tr><td>Packages not having license files information</td><td>%s</td></tr>\n" %
  557. stats["no-license-files"])
  558. f.write(" <tr><td>Packages having a hash file</td><td>%s</td></tr>\n" %
  559. stats["hash"])
  560. f.write(" <tr><td>Packages not having a hash file</td><td>%s</td></tr>\n" %
  561. stats["no-hash"])
  562. f.write(" <tr><td>Total number of patches</td><td>%s</td></tr>\n" %
  563. stats["patches"])
  564. f.write("<tr><td>Packages having a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" %
  565. stats["rmo-mapping"])
  566. f.write("<tr><td>Packages lacking a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" %
  567. stats["rmo-no-mapping"])
  568. f.write("<tr><td>Packages that are up-to-date</td><td>%s</td></tr>\n" %
  569. stats["version-uptodate"])
  570. f.write("<tr><td>Packages that are not up-to-date</td><td>%s</td></tr>\n" %
  571. stats["version-not-uptodate"])
  572. f.write("<tr><td>Packages with no known upstream version</td><td>%s</td></tr>\n" %
  573. stats["version-unknown"])
  574. f.write("</table>\n")
  575. def dump_html_gen_info(f, date, commit):
  576. # Updated on Mon Feb 19 08:12:08 CET 2018, Git commit aa77030b8f5e41f1c53eb1c1ad664b8c814ba032
  577. f.write("<p><i>Updated on %s, git commit %s</i></p>\n" % (str(date), commit))
  578. def dump_html(packages, stats, date, commit, output):
  579. with open(output, 'w') as f:
  580. f.write(html_header)
  581. dump_html_all_pkgs(f, packages)
  582. dump_html_stats(f, stats)
  583. dump_html_gen_info(f, date, commit)
  584. f.write(html_footer)
  585. def dump_json(packages, stats, date, commit, output):
  586. # Format packages as a dictionnary instead of a list
  587. # Exclude local field that does not contains real date
  588. excluded_fields = ['url_worker', 'name']
  589. pkgs = {
  590. pkg.name: {
  591. k: v
  592. for k, v in pkg.__dict__.items()
  593. if k not in excluded_fields
  594. } for pkg in packages
  595. }
  596. # Aggregate infrastructures into a single dict entry
  597. statistics = {
  598. k: v
  599. for k, v in stats.items()
  600. if not k.startswith('infra-')
  601. }
  602. statistics['infra'] = {k[6:]: v for k, v in stats.items() if k.startswith('infra-')}
  603. # The actual structure to dump, add commit and date to it
  604. final = {'packages': pkgs,
  605. 'stats': statistics,
  606. 'commit': commit,
  607. 'date': str(date)}
  608. with open(output, 'w') as f:
  609. json.dump(final, f, indent=2, separators=(',', ': '))
  610. f.write('\n')
  611. def parse_args():
  612. parser = argparse.ArgumentParser()
  613. output = parser.add_argument_group('output', 'Output file(s)')
  614. output.add_argument('--html', dest='html', action='store',
  615. help='HTML output file')
  616. output.add_argument('--json', dest='json', action='store',
  617. help='JSON output file')
  618. packages = parser.add_mutually_exclusive_group()
  619. packages.add_argument('-n', dest='npackages', type=int, action='store',
  620. help='Number of packages')
  621. packages.add_argument('-p', dest='packages', action='store',
  622. help='List of packages (comma separated)')
  623. args = parser.parse_args()
  624. if not args.html and not args.json:
  625. parser.error('at least one of --html or --json (or both) is required')
  626. return args
  627. def __main__():
  628. args = parse_args()
  629. if args.packages:
  630. package_list = args.packages.split(",")
  631. else:
  632. package_list = None
  633. date = datetime.datetime.utcnow()
  634. commit = subprocess.check_output(['git', 'rev-parse',
  635. 'HEAD']).splitlines()[0]
  636. print("Build package list ...")
  637. packages = get_pkglist(args.npackages, package_list)
  638. print("Getting package make info ...")
  639. package_init_make_info()
  640. print("Getting package details ...")
  641. for pkg in packages:
  642. pkg.set_infra()
  643. pkg.set_license()
  644. pkg.set_hash_info()
  645. pkg.set_patch_count()
  646. pkg.set_check_package_warnings()
  647. pkg.set_current_version()
  648. pkg.set_url()
  649. print("Checking URL status")
  650. check_package_urls(packages)
  651. print("Getting latest versions ...")
  652. check_package_latest_version(packages)
  653. print("Calculate stats")
  654. stats = calculate_stats(packages)
  655. if args.html:
  656. print("Write HTML")
  657. dump_html(packages, stats, date, commit, args.html)
  658. if args.json:
  659. print("Write JSON")
  660. dump_json(packages, stats, date, commit, args.json)
  661. __main__()