g.extension.py 79 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305
  1. #!/usr/bin/env python3
  2. ############################################################################
  3. #
  4. # MODULE: g.extension
  5. # AUTHOR(S): Markus Neteler (original shell script)
  6. # Martin Landa <landa.martin gmail com> (Pythonized & upgraded for GRASS 7)
  7. # Vaclav Petras <wenzeslaus gmail com> (support for general sources)
  8. # PURPOSE: Tool to download and install extensions into local installation
  9. #
  10. # COPYRIGHT: (C) 2009-2019 by Markus Neteler, and the GRASS Development Team
  11. #
  12. # This program is free software under the GNU General
  13. # Public License (>=v2). Read the file COPYING that
  14. # comes with GRASS for details.
  15. #
  16. # TODO: - add sudo support where needed (i.e. check first permission to write into
  17. # $GISBASE directory)
  18. # - fix toolbox support in install_private_extension_xml()
  19. #############################################################################
  20. #%module
  21. #% label: Maintains GRASS Addons extensions in local GRASS installation.
  22. #% description: Downloads and installs extensions from GRASS Addons repository or other source into the local GRASS installation or removes installed extensions.
  23. #% keyword: general
  24. #% keyword: installation
  25. #% keyword: extensions
  26. #% keyword: addons
  27. #% keyword: download
  28. #%end
  29. #%option
  30. #% key: extension
  31. #% type: string
  32. #% key_desc: name
  33. #% label: Name of extension to install or remove
  34. #% description: Name of toolbox (set of extensions) when -t flag is given
  35. #% required: yes
  36. #%end
  37. #%option
  38. #% key: operation
  39. #% type: string
  40. #% description: Operation to be performed
  41. #% required: yes
  42. #% options: add,remove
  43. #% answer: add
  44. #%end
  45. #%option
  46. #% key: url
  47. #% type: string
  48. #% key_desc: url
  49. #% label: URL or directory to get the extension from (supported only on Linux and Mac)
  50. #% description: The official repository is used by default. User can specify a ZIP file, directory or a repository on common hosting services. If not identified, Subversion repository is assumed. See manual for all options.
  51. #%end
  52. #%option
  53. #% key: prefix
  54. #% type: string
  55. #% key_desc: path
  56. #% description: Prefix where to install extension (ignored when flag -s is given)
  57. #% answer: $GRASS_ADDON_BASE
  58. #% required: no
  59. #%end
  60. #%option
  61. #% key: proxy
  62. #% type: string
  63. #% key_desc: proxy
  64. #% description: Set the proxy with: "http=<value>,ftp=<value>"
  65. #% required: no
  66. #% multiple: yes
  67. #%end
  68. #%flag
  69. #% key: l
  70. #% description: List available extensions in the official GRASS GIS Addons repository
  71. #% guisection: Print
  72. #% suppress_required: yes
  73. #%end
  74. #%flag
  75. #% key: c
  76. #% description: List available extensions in the official GRASS GIS Addons repository including module description
  77. #% guisection: Print
  78. #% suppress_required: yes
  79. #%end
  80. #%flag
  81. #% key: g
  82. #% description: List available extensions in the official GRASS GIS Addons repository (shell script style)
  83. #% guisection: Print
  84. #% suppress_required: yes
  85. #%end
  86. #%flag
  87. #% key: a
  88. #% description: List locally installed extensions
  89. #% guisection: Print
  90. #% suppress_required: yes
  91. #%end
  92. #%flag
  93. #% key: s
  94. #% description: Install system-wide (may need system administrator rights)
  95. #% guisection: Install
  96. #%end
  97. #%flag
  98. #% key: d
  99. #% description: Download source code and exit
  100. #% guisection: Install
  101. #%end
  102. #%flag
  103. #% key: i
  104. #% description: Do not install new extension, just compile it
  105. #% guisection: Install
  106. #%end
  107. #%flag
  108. #% key: f
  109. #% description: Force removal when uninstalling extension (operation=remove)
  110. #% guisection: Remove
  111. #%end
  112. #%flag
  113. #% key: t
  114. #% description: Operate on toolboxes instead of single modules (experimental)
  115. #% suppress_required: yes
  116. #%end
  117. #%flag
  118. #% key: j
  119. #% description: Generates JSON file containing the download URLs of the official Addons
  120. #% guisection: Install
  121. #% suppress_required: yes
  122. #%end
  123. #%rules
  124. #% required: extension, -l, -c, -g, -a, -j
  125. #% exclusive: extension, -l, -c, -g
  126. #% exclusive: extension, -l, -c, -a
  127. #%end
  128. # TODO: solve addon-extension(-module) confusion
  129. from __future__ import print_function
  130. import fileinput
  131. import http
  132. import os
  133. import sys
  134. import re
  135. import atexit
  136. import shutil
  137. import zipfile
  138. import tempfile
  139. import json
  140. import xml.etree.ElementTree as etree
  141. from distutils.dir_util import copy_tree
  142. from six.moves.urllib import request as urlrequest
  143. from six.moves.urllib.error import HTTPError, URLError
  144. # Get the XML parsing exceptions to catch. The behavior changed with Python 2.7
  145. # and ElementTree 1.3.
  146. from xml.parsers import expat # TODO: works for any Python?
  147. if hasattr(etree, 'ParseError'):
  148. ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
  149. else:
  150. ETREE_EXCEPTIONS = (expat.ExpatError)
  151. import grass.script as gscript
  152. from grass.script.utils import try_rmdir
  153. from grass.script import core as grass
  154. from grass.script import task as gtask
  155. # temp dir
  156. REMOVE_TMPDIR = True
  157. PROXIES = {}
  158. HEADERS = {
  159. "User-Agent": "Mozilla/5.0",
  160. }
  161. HTTP_STATUS_CODES = list(http.HTTPStatus)
  162. def urlretrieve(url, filename, *args, **kwargs):
  163. """Same function as 'urlretrieve', but with the ability to
  164. define headers.
  165. """
  166. request = urlrequest.Request(url, headers=HEADERS)
  167. response = urlrequest.urlopen(request, *args, **kwargs)
  168. with open(filename, 'wb') as f:
  169. f.write(response.read())
  170. def urlopen(url, *args, **kwargs):
  171. """Wrapper around urlopen. Same function as 'urlopen', but with the
  172. ability to define headers.
  173. """
  174. request = urlrequest.Request(url, headers=HEADERS)
  175. return urlrequest.urlopen(request, *args, **kwargs)
  176. def download_addons_paths_file(
  177. url, response_format, *args, **kwargs):
  178. """Generates JSON file containing the download URLs of the official
  179. Addons
  180. :param str url: url address
  181. :param str response_format: content type
  182. :return response: urllib.request.urlopen response object
  183. """
  184. try:
  185. response = urlopen(url, *args, **kwargs)
  186. if not response.code == 200:
  187. index = HTTP_STATUS_CODES.index(response.code)
  188. desc = HTTP_STATUS_CODES[index].description
  189. gscript.fatal(
  190. _(
  191. "Download file from <{url}>, "
  192. "return status code {code}, "
  193. "{desc}".format(
  194. url=url,
  195. code=response.code,
  196. desc=desc,
  197. ),
  198. ),
  199. )
  200. if response_format not in response.getheader('Content-Type'):
  201. gscript.fatal(
  202. _(
  203. "Wrong downloaded file format. "
  204. "Check url <{url}>. Allowed file format is "
  205. "{response_format}.".format(
  206. url=url,
  207. response_format=response_format,
  208. ),
  209. ),
  210. )
  211. return response
  212. except HTTPError as err:
  213. gscript.fatal(
  214. _(
  215. "Download file from <{url}>, "
  216. "return status code {code}, ".format(
  217. url=url,
  218. code=err,
  219. ),
  220. ),
  221. )
  222. except URLError:
  223. gscript.fatal(
  224. _(
  225. "Download file from <{url}>, "
  226. "failed. Check internet connection.".format(
  227. url=url,
  228. ),
  229. ),
  230. )
  231. def etree_fromfile(filename):
  232. """Create XML element tree from a given file name"""
  233. with open(filename, 'r') as file_:
  234. return etree.fromstring(file_.read())
  235. def etree_fromurl(url):
  236. """Create XML element tree from a given URL"""
  237. file_ = urlopen(url)
  238. return etree.fromstring(file_.read())
  239. def check_progs():
  240. """Check if the necessary programs are available"""
  241. # TODO: we need svn for the Subversion repo downloads
  242. # also git would be tested once supported
  243. for prog in ('make', 'gcc'):
  244. if not grass.find_program(prog, '--help'):
  245. grass.fatal(_("'%s' required. Please install '%s' first.")
  246. % (prog, prog))
  247. # expand prefix to class name
  248. def expand_module_class_name(class_letters):
  249. """Convert module class (family) letter or letters to class (family) name
  250. The letter or letters are used in module names, e.g. r.slope.aspect.
  251. The names are used in directories in Addons but also in the source code.
  252. >>> expand_module_class_name('r')
  253. 'raster'
  254. >>> expand_module_class_name('v')
  255. 'vector'
  256. """
  257. name = {
  258. 'd': 'display',
  259. 'db': 'db',
  260. 'g': 'general',
  261. 'i': 'imagery',
  262. 'm': 'misc',
  263. 'ps': 'postscript',
  264. 'p': 'paint',
  265. 'r': 'raster',
  266. 'r3': 'raster3d',
  267. 's': 'sites',
  268. 't': 'temporal',
  269. 'v': 'vector',
  270. 'wx': 'gui/wxpython'
  271. }
  272. return name.get(class_letters, class_letters)
  273. def get_module_class_name(module_name):
  274. """Return class (family) name for a module
  275. The names are used in directories in Addons but also in the source code.
  276. >>> get_module_class_name('r.slope.aspect')
  277. 'raster'
  278. >>> get_module_class_name('v.to.rast')
  279. 'vector'
  280. """
  281. classchar = module_name.split('.', 1)[0]
  282. return expand_module_class_name(classchar)
  283. def get_installed_extensions(force=False):
  284. """Get list of installed extensions or toolboxes (if -t is set)"""
  285. if flags['t']:
  286. return get_installed_toolboxes(force)
  287. # TODO: extension != module
  288. return get_installed_modules(force)
  289. def list_installed_extensions(toolboxes=False):
  290. """List installed extensions"""
  291. elist = get_installed_extensions()
  292. if elist:
  293. if toolboxes:
  294. grass.message(_("List of installed extensions (toolboxes):"))
  295. else:
  296. grass.message(_("List of installed extensions (modules):"))
  297. sys.stdout.write('\n'.join(elist))
  298. sys.stdout.write('\n')
  299. else:
  300. if toolboxes:
  301. grass.info(_("No extension (toolbox) installed"))
  302. else:
  303. grass.info(_("No extension (module) installed"))
  304. def get_installed_toolboxes(force=False):
  305. """Get list of installed toolboxes
  306. Writes toolboxes file if it does not exist.
  307. Creates a new toolboxes file if it is not possible
  308. to read the current one.
  309. """
  310. xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
  311. if not os.path.exists(xml_file):
  312. write_xml_toolboxes(xml_file)
  313. # read XML file
  314. try:
  315. tree = etree_fromfile(xml_file)
  316. except ETREE_EXCEPTIONS + (OSError, IOError):
  317. os.remove(xml_file)
  318. write_xml_toolboxes(xml_file)
  319. return []
  320. ret = list()
  321. for tnode in tree.findall('toolbox'):
  322. ret.append(tnode.get('code'))
  323. return ret
  324. def get_installed_modules(force=False):
  325. """Get list of installed modules.
  326. Writes modules file if it does not exist and *force* is set to ``True``.
  327. Creates a new modules file if it is not possible
  328. to read the current one.
  329. """
  330. xml_file = os.path.join(options['prefix'], 'modules.xml')
  331. if not os.path.exists(xml_file):
  332. if force:
  333. write_xml_modules(xml_file)
  334. else:
  335. grass.debug("No addons metadata file available", 1)
  336. return []
  337. # read XML file
  338. try:
  339. tree = etree_fromfile(xml_file)
  340. except ETREE_EXCEPTIONS + (OSError, IOError):
  341. os.remove(xml_file)
  342. write_xml_modules(xml_file)
  343. return []
  344. ret = list()
  345. for tnode in tree.findall('task'):
  346. if flags['g']:
  347. desc, keyw = get_optional_params(tnode)
  348. ret.append('name={0}'.format(tnode.get('name').strip()))
  349. ret.append('description={0}'.format(desc))
  350. ret.append('keywords={0}'.format(keyw))
  351. ret.append('executables={0}'.format(','.join(
  352. get_module_executables(tnode))
  353. ))
  354. else:
  355. ret.append(tnode.get('name').strip())
  356. return ret
  357. # list extensions (read XML file from grass.osgeo.org/addons)
  358. def list_available_extensions(url):
  359. """List available extensions/modules or toolboxes (if -t is given)
  360. For toolboxes it lists also all modules.
  361. """
  362. gscript.debug("list_available_extensions(url={0})".format(url))
  363. if flags['t']:
  364. grass.message(_("List of available extensions (toolboxes):"))
  365. tlist = get_available_toolboxes(url)
  366. tkeys = sorted(tlist.keys())
  367. for toolbox_code in tkeys:
  368. toolbox_data = tlist[toolbox_code]
  369. if flags['g']:
  370. print('toolbox_name=' + toolbox_data['name'])
  371. print('toolbox_code=' + toolbox_code)
  372. else:
  373. print('%s (%s)' % (toolbox_data['name'], toolbox_code))
  374. if flags['c'] or flags['g']:
  375. list_available_modules(url, toolbox_data['modules'])
  376. else:
  377. if toolbox_data['modules']:
  378. print(os.linesep.join(['* ' + x for x in toolbox_data['modules']]))
  379. else:
  380. grass.message(_("List of available extensions (modules):"))
  381. # TODO: extensions with several modules + lib
  382. list_available_modules(url)
  383. def get_available_toolboxes(url):
  384. """Return toolboxes available in the repository"""
  385. tdict = dict()
  386. url = url + "toolboxes.xml"
  387. try:
  388. tree = etree_fromurl(url)
  389. for tnode in tree.findall('toolbox'):
  390. mlist = list()
  391. clist = list()
  392. tdict[tnode.get('code')] = {'name': tnode.get('name'),
  393. 'correlate': clist,
  394. 'modules': mlist}
  395. for cnode in tnode.findall('correlate'):
  396. clist.append(cnode.get('name'))
  397. for mnode in tnode.findall('task'):
  398. mlist.append(mnode.get('name'))
  399. except (HTTPError, IOError, OSError):
  400. grass.fatal(_("Unable to fetch addons metadata file"))
  401. return tdict
  402. def get_toolbox_extensions(url, name):
  403. """Get extensions inside a toolbox in toolbox file at given URL
  404. :param url: URL of the directory (file name will be attached)
  405. :param name: toolbox name
  406. """
  407. # dictionary of extensions
  408. edict = dict()
  409. url = url + "toolboxes.xml"
  410. try:
  411. tree = etree_fromurl(url)
  412. for tnode in tree.findall('toolbox'):
  413. if name == tnode.get('code'):
  414. for enode in tnode.findall('task'):
  415. # extension name
  416. ename = enode.get('name')
  417. edict[ename] = dict()
  418. # list of modules installed by this extension
  419. edict[ename]['mlist'] = list()
  420. # list of files installed by this extension
  421. edict[ename]['flist'] = list()
  422. break
  423. except (HTTPError, IOError, OSError):
  424. grass.fatal(_("Unable to fetch addons metadata file"))
  425. return edict
  426. def get_module_files(mnode):
  427. """Return list of module files
  428. :param mnode: XML node for a module
  429. """
  430. flist = []
  431. if mnode.find('binary') is None:
  432. return flist
  433. for file_node in mnode.find('binary').findall('file'):
  434. filepath = file_node.text
  435. flist.append(filepath)
  436. return flist
  437. def get_module_executables(mnode):
  438. """Return list of module executables
  439. :param mnode: XML node for a module
  440. """
  441. flist = []
  442. for filepath in get_module_files(mnode):
  443. if filepath.startswith(options['prefix'] + os.path.sep + 'bin') or \
  444. (sys.platform != 'win32' and
  445. filepath.startswith(options['prefix'] + os.path.sep + 'scripts')):
  446. filename = os.path.basename(filepath)
  447. if sys.platform == 'win32':
  448. filename = os.path.splitext(filename)[0]
  449. flist.append(filename)
  450. return flist
  451. def get_optional_params(mnode):
  452. """Return description and keywords of a module as a tuple
  453. :param mnode: XML node for a module
  454. """
  455. try:
  456. desc = mnode.find('description').text
  457. except AttributeError:
  458. desc = ''
  459. if desc is None:
  460. desc = ''
  461. try:
  462. keyw = mnode.find('keywords').text
  463. except AttributeError:
  464. keyw = ''
  465. if keyw is None:
  466. keyw = ''
  467. return desc, keyw
  468. def list_available_modules(url, mlist=None):
  469. """List modules available in the repository
  470. Tries to use XML metadata file first. Fallbacks to HTML page with a list.
  471. :param url: URL of the directory (file name will be attached)
  472. :param mlist: list only modules in this list
  473. """
  474. file_url = url + "modules.xml"
  475. grass.debug("url=%s" % file_url, 1)
  476. try:
  477. tree = etree_fromurl(file_url)
  478. except ETREE_EXCEPTIONS:
  479. grass.warning(_("Unable to parse '%s'. Trying to scan"
  480. " SVN repository (may take some time)...") % file_url)
  481. list_available_extensions_svn(url)
  482. return
  483. except (HTTPError, URLError, IOError, OSError):
  484. list_available_extensions_svn(url)
  485. return
  486. for mnode in tree.findall('task'):
  487. name = mnode.get('name').strip()
  488. if mlist and name not in mlist:
  489. continue
  490. if flags['c'] or flags['g']:
  491. desc, keyw = get_optional_params(mnode)
  492. if flags['g']:
  493. print('name=' + name)
  494. print('description=' + desc)
  495. print('keywords=' + keyw)
  496. elif flags['c']:
  497. if mlist:
  498. print('*', end='')
  499. print(name + ' - ' + desc)
  500. else:
  501. print(name)
  502. # TODO: this is now broken/dead code, SVN is basically not used
  503. # fallback for Trac should parse Trac HTML page
  504. # this might be useful for potential SVN repos or anything
  505. # which would list the extensions/addons as list
  506. # TODO: fail when nothing is accessible
  507. def list_available_extensions_svn(url):
  508. """List available extensions from HTML given by URL
  509. Filename is generated based on the module class/family.
  510. This works well for the structure which is in grass-addons repository.
  511. ``<li><a href=...`` is parsed to find module names.
  512. This works well for HTML page generated by Subversion.
  513. :param url: a directory URL (filename will be attached)
  514. """
  515. gscript.debug("list_available_extensions_svn(url=%s)" % url, 2)
  516. grass.message(_('Fetching list of extensions from'
  517. ' GRASS-Addons SVN repository (be patient)...'))
  518. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  519. if flags['c']:
  520. grass.warning(
  521. _("Flag 'c' ignored, addons metadata file not available"))
  522. if flags['g']:
  523. grass.warning(
  524. _("Flag 'g' ignored, addons metadata file not available"))
  525. prefixes = ['d', 'db', 'g', 'i', 'm', 'ps',
  526. 'p', 'r', 'r3', 's', 't', 'v']
  527. for prefix in prefixes:
  528. modclass = expand_module_class_name(prefix)
  529. grass.verbose(_("Checking for '%s' modules...") % modclass)
  530. # construct a full URL of a file
  531. file_url = '%s/%s' % (url, modclass)
  532. grass.debug("url = %s" % file_url, debug=2)
  533. try:
  534. file_ = urlopen(url)
  535. except (HTTPError, IOError, OSError):
  536. grass.debug(_("Unable to fetch '%s'") % file_url, debug=1)
  537. continue
  538. for line in file_.readlines():
  539. # list extensions
  540. sline = pattern.search(line)
  541. if not sline:
  542. continue
  543. name = sline.group(2).rstrip('/')
  544. if name.split('.', 1)[0] == prefix:
  545. print(name)
  546. # get_wxgui_extensions(url)
  547. # TODO: this is a dead code, not clear why not used, but seems not needed
  548. def get_wxgui_extensions(url):
  549. """Return list of extensions/addons in wxGUI directory at given URL
  550. :param url: a directory URL (filename will be attached)
  551. """
  552. mlist = list()
  553. grass.debug('Fetching list of wxGUI extensions from '
  554. 'GRASS-Addons SVN repository (be patient)...')
  555. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  556. grass.verbose(_("Checking for '%s' modules...") % 'gui/wxpython')
  557. # construct a full URL of a file
  558. url = '%s/%s' % (url, 'gui/wxpython')
  559. grass.debug("url = %s" % url, debug=2)
  560. file_ = urlopen(url)
  561. if not file_:
  562. grass.warning(_("Unable to fetch '%s'") % url)
  563. return
  564. for line in file.readlines():
  565. # list extensions
  566. sline = pattern.search(line)
  567. if not sline:
  568. continue
  569. name = sline.group(2).rstrip('/')
  570. if name not in ('..', 'Makefile'):
  571. mlist.append(name)
  572. return mlist
  573. def cleanup():
  574. """Cleanup after the downloads and copilation"""
  575. if REMOVE_TMPDIR:
  576. try_rmdir(TMPDIR)
  577. else:
  578. grass.message("\n%s\n" % _("Path to the source code:"))
  579. sys.stderr.write('%s\n' % os.path.join(TMPDIR, options['extension']))
  580. def write_xml_modules(name, tree=None):
  581. """Write element tree as a modules matadata file
  582. If the *tree* is not given, an empty file is created.
  583. :param name: file name
  584. :param tree: XML element tree
  585. """
  586. file_ = open(name, 'w')
  587. file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  588. file_.write('<!DOCTYPE task SYSTEM "grass-addons.dtd">\n')
  589. file_.write('<addons version="%s">\n' % version[0])
  590. libgis_revison = grass.version()['libgis_revision']
  591. if tree is not None:
  592. for tnode in tree.findall('task'):
  593. indent = 4
  594. file_.write('%s<task name="%s">\n' %
  595. (' ' * indent, tnode.get('name')))
  596. indent += 4
  597. file_.write('%s<description>%s</description>\n' %
  598. (' ' * indent, tnode.find('description').text))
  599. file_.write('%s<keywords>%s</keywords>\n' %
  600. (' ' * indent, tnode.find('keywords').text))
  601. bnode = tnode.find('binary')
  602. if bnode is not None:
  603. file_.write('%s<binary>\n' % (' ' * indent))
  604. indent += 4
  605. for fnode in bnode.findall('file'):
  606. file_.write('%s<file>%s</file>\n' %
  607. (' ' * indent, os.path.join(options['prefix'],
  608. fnode.text)))
  609. indent -= 4
  610. file_.write('%s</binary>\n' % (' ' * indent))
  611. file_.write('%s<libgis revision="%s" />\n' %
  612. (' ' * indent, libgis_revison))
  613. indent -= 4
  614. file_.write('%s</task>\n' % (' ' * indent))
  615. file_.write('</addons>\n')
  616. file_.close()
  617. def write_xml_extensions(name, tree=None):
  618. """Write element tree as a modules matadata file
  619. If the *tree* is not given, an empty file is created.
  620. :param name: file name
  621. :param tree: XML element tree
  622. """
  623. file_ = open(name, 'w')
  624. file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  625. file_.write('<!DOCTYPE task SYSTEM "grass-addons.dtd">\n')
  626. file_.write('<addons version="%s">\n' % version[0])
  627. libgis_revison = grass.version()['libgis_revision']
  628. if tree is not None:
  629. for tnode in tree.findall('task'):
  630. indent = 4
  631. # extension name
  632. file_.write('%s<task name="%s">\n' %
  633. (' ' * indent, tnode.get('name')))
  634. indent += 4
  635. """
  636. file_.write('%s<description>%s</description>\n' %
  637. (' ' * indent, tnode.find('description').text))
  638. file_.write('%s<keywords>%s</keywords>\n' %
  639. (' ' * indent, tnode.find('keywords').text))
  640. """
  641. # extension files
  642. bnode = tnode.find('binary')
  643. if bnode is not None:
  644. file_.write('%s<binary>\n' % (' ' * indent))
  645. indent += 4
  646. for fnode in bnode.findall('file'):
  647. file_.write('%s<file>%s</file>\n' %
  648. (' ' * indent, os.path.join(options['prefix'],
  649. fnode.text)))
  650. indent -= 4
  651. file_.write('%s</binary>\n' % (' ' * indent))
  652. # extension modules
  653. mnode = tnode.find('modules')
  654. if mnode is not None:
  655. file_.write('%s<modules>\n' % (' ' * indent))
  656. indent += 4
  657. for fnode in mnode.findall('module'):
  658. file_.write('%s<module>%s</module>\n' %
  659. (' ' * indent, fnode.text))
  660. indent -= 4
  661. file_.write('%s</modules>\n' % (' ' * indent))
  662. file_.write('%s<libgis revision="%s" />\n' %
  663. (' ' * indent, libgis_revison))
  664. indent -= 4
  665. file_.write('%s</task>\n' % (' ' * indent))
  666. file_.write('</addons>\n')
  667. file_.close()
  668. def write_xml_toolboxes(name, tree=None):
  669. """Write element tree as a toolboxes matadata file
  670. If the *tree* is not given, an empty file is created.
  671. :param name: file name
  672. :param tree: XML element tree
  673. """
  674. file_ = open(name, 'w')
  675. file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  676. file_.write('<!DOCTYPE toolbox SYSTEM "grass-addons.dtd">\n')
  677. file_.write('<addons version="%s">\n' % version[0])
  678. if tree is not None:
  679. for tnode in tree.findall('toolbox'):
  680. indent = 4
  681. file_.write('%s<toolbox name="%s" code="%s">\n' %
  682. (' ' * indent, tnode.get('name'), tnode.get('code')))
  683. indent += 4
  684. for cnode in tnode.findall('correlate'):
  685. file_.write('%s<correlate code="%s" />\n' %
  686. (' ' * indent, tnode.get('code')))
  687. for mnode in tnode.findall('task'):
  688. file_.write('%s<task name="%s" />\n' %
  689. (' ' * indent, mnode.get('name')))
  690. indent -= 4
  691. file_.write('%s</toolbox>\n' % (' ' * indent))
  692. file_.write('</addons>\n')
  693. file_.close()
  694. def install_extension(source, url, xmlurl):
  695. """Install extension (e.g. one module) or a toolbox (list of modules)"""
  696. gisbase = os.getenv('GISBASE')
  697. if not gisbase:
  698. grass.fatal(_('$GISBASE not defined'))
  699. if options['extension'] in get_installed_extensions(force=True):
  700. grass.warning(_("Extension <%s> already installed. Re-installing...") %
  701. options['extension'])
  702. # create a dictionary of extensions
  703. # for each extension
  704. # - a list of modules installed by this extension
  705. # - a list of files installed by this extension
  706. edict = None
  707. if flags['t']:
  708. grass.message(_("Installing toolbox <%s>...") % options['extension'])
  709. edict = get_toolbox_extensions(xmlurl, options['extension'])
  710. else:
  711. edict = dict()
  712. edict[options['extension']] = dict()
  713. # list of modules installed by this extension
  714. edict[options['extension']]['mlist'] = list()
  715. # list of files installed by this extension
  716. edict[options['extension']]['flist'] = list()
  717. if not edict:
  718. grass.warning(_("Nothing to install"))
  719. return
  720. ret = 0
  721. tmp_dir = None
  722. new_modules = list()
  723. for extension in edict:
  724. ret1 = 0
  725. new_modules_ext = None
  726. if sys.platform == "win32":
  727. ret1, new_modules_ext, new_files_ext = install_extension_win(extension)
  728. else:
  729. ret1, new_modules_ext, new_files_ext, tmp_dir = install_extension_std_platforms(extension,
  730. source=source, url=url)
  731. if not flags['d'] and not flags['i']:
  732. edict[extension]['mlist'].extend(new_modules_ext)
  733. edict[extension]['flist'].extend(new_files_ext)
  734. new_modules.extend(new_modules_ext)
  735. ret += ret1
  736. if len(edict) > 1:
  737. print('-' * 60)
  738. if flags['d'] or flags['i']:
  739. return
  740. if ret != 0:
  741. grass.warning(_('Installation failed, sorry.'
  742. ' Please check above error messages.'))
  743. else:
  744. # update extensions metadata file
  745. grass.message(_("Updating extensions metadata file..."))
  746. install_extension_xml(edict)
  747. # update modules metadata file
  748. grass.message(_("Updating extension modules metadata file..."))
  749. install_module_xml(new_modules)
  750. for module in new_modules:
  751. update_manual_page(module)
  752. grass.message(_("Installation of <%s> successfully finished") %
  753. options['extension'])
  754. if not os.getenv('GRASS_ADDON_BASE'):
  755. grass.warning(_('This add-on module will not function until'
  756. ' you set the GRASS_ADDON_BASE environment'
  757. ' variable (see "g.manual variables")'))
  758. def get_toolboxes_metadata(url):
  759. """Return metadata for all toolboxes from given URL
  760. :param url: URL of a modules matadata file
  761. :param mlist: list of modules to get metadata for
  762. :returns: tuple where first item is dictionary with module names as keys
  763. and dictionary with dest, keyw, files keys as value, the second item
  764. is list of 'binary' files (installation files)
  765. """
  766. data = dict()
  767. try:
  768. tree = etree_fromurl(url)
  769. for tnode in tree.findall('toolbox'):
  770. clist = list()
  771. for cnode in tnode.findall('correlate'):
  772. clist.append(cnode.get('code'))
  773. mlist = list()
  774. for mnode in tnode.findall('task'):
  775. mlist.append(mnode.get('name'))
  776. code = tnode.get('code')
  777. data[code] = {
  778. 'name': tnode.get('name'),
  779. 'correlate': clist,
  780. 'modules': mlist,
  781. }
  782. except (HTTPError, IOError, OSError):
  783. grass.error(_("Unable to read addons metadata file "
  784. "from the remote server"))
  785. return data
  786. def install_toolbox_xml(url, name):
  787. """Update local toolboxes metadata file"""
  788. # read metadata from remote server (toolboxes)
  789. url = url + "toolboxes.xml"
  790. data = get_toolboxes_metadata(url)
  791. if not data:
  792. grass.warning(_("No addons metadata available"))
  793. return
  794. if name not in data:
  795. grass.warning(_("No addons metadata available for <%s>") % name)
  796. return
  797. xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
  798. # create an empty file if not exists
  799. if not os.path.exists(xml_file):
  800. write_xml_modules(xml_file)
  801. # read XML file
  802. with open(xml_file, 'r') as xml:
  803. tree = etree.fromstring(xml.read())
  804. # update tree
  805. tnode = None
  806. for node in tree.findall('toolbox'):
  807. if node.get('code') == name:
  808. tnode = node
  809. break
  810. tdata = data[name]
  811. if tnode is not None:
  812. # update existing node
  813. for cnode in tnode.findall('correlate'):
  814. tnode.remove(cnode)
  815. for mnode in tnode.findall('task'):
  816. tnode.remove(mnode)
  817. else:
  818. # create new node for task
  819. tnode = etree.Element(
  820. 'toolbox', attrib={'name': tdata['name'], 'code': name})
  821. tree.append(tnode)
  822. for cname in tdata['correlate']:
  823. cnode = etree.Element('correlate', attrib={'code': cname})
  824. tnode.append(cnode)
  825. for tname in tdata['modules']:
  826. mnode = etree.Element('task', attrib={'name': tname})
  827. tnode.append(mnode)
  828. write_xml_toolboxes(xml_file, tree)
  829. def get_addons_metadata(url, mlist):
  830. """Return metadata for list of modules from given URL
  831. :param url: URL of a modules matadata file
  832. :param mlist: list of modules to get metadata for
  833. :returns: tuple where first item is dictionary with module names as keys
  834. and dictionary with dest, keyw, files keys as value, the second item
  835. is list of 'binary' files (installation files)
  836. """
  837. # TODO: extensions with multiple modules
  838. data = {}
  839. bin_list = []
  840. try:
  841. tree = etree_fromurl(url)
  842. except (HTTPError, URLError, IOError, OSError) as error:
  843. grass.error(_("Unable to read addons metadata file"
  844. " from the remote server: {0}").format(error))
  845. return data, bin_list
  846. except ETREE_EXCEPTIONS as error:
  847. grass.warning(_("Unable to parse '%s': {0}").format(error) % url)
  848. return data, bin_list
  849. for mnode in tree.findall('task'):
  850. name = mnode.get('name')
  851. if name not in mlist:
  852. continue
  853. file_list = list()
  854. bnode = mnode.find('binary')
  855. windows = sys.platform == 'win32'
  856. if bnode is not None:
  857. for fnode in bnode.findall('file'):
  858. path = fnode.text.split('/')
  859. if path[0] == 'bin':
  860. bin_list.append(path[-1])
  861. if windows:
  862. path[-1] += '.exe'
  863. elif path[0] == 'scripts':
  864. bin_list.append(path[-1])
  865. if windows:
  866. path[-1] += '.py'
  867. file_list.append(os.path.sep.join(path))
  868. desc, keyw = get_optional_params(mnode)
  869. data[name] = {
  870. 'desc': desc,
  871. 'keyw': keyw,
  872. 'files': file_list,
  873. }
  874. return data, bin_list
  875. def install_extension_xml(edict):
  876. """Update XML files with metadata about installed modules and toolbox
  877. of an private addon
  878. """
  879. # TODO toolbox
  880. # if len(mlist) > 1:
  881. # # read metadata from remote server (toolboxes)
  882. # install_toolbox_xml(url, options['extension'])
  883. xml_file = os.path.join(options['prefix'], 'extensions.xml')
  884. # create an empty file if not exists
  885. if not os.path.exists(xml_file):
  886. write_xml_extensions(xml_file)
  887. # read XML file
  888. tree = etree_fromfile(xml_file)
  889. # update tree
  890. for name in edict:
  891. # so far extensions do not have description or keywords
  892. # only modules have
  893. """
  894. try:
  895. desc = gtask.parse_interface(name).description
  896. # mname = gtask.parse_interface(name).name
  897. keywords = gtask.parse_interface(name).keywords
  898. except Exception as e:
  899. grass.warning(_("No addons metadata available."
  900. " Addons metadata file not updated."))
  901. return []
  902. """
  903. tnode = None
  904. for node in tree.findall('task'):
  905. if node.get('name') == name:
  906. tnode = node
  907. break
  908. if tnode is None:
  909. # create new node for task
  910. tnode = etree.Element('task', attrib={'name': name})
  911. """
  912. dnode = etree.Element('description')
  913. dnode.text = desc
  914. tnode.append(dnode)
  915. knode = etree.Element('keywords')
  916. knode.text = (',').join(keywords)
  917. tnode.append(knode)
  918. """
  919. # create binary
  920. bnode = etree.Element('binary')
  921. # list of all installed files for this extension
  922. for file_name in edict[name]['flist']:
  923. fnode = etree.Element('file')
  924. fnode.text = file_name
  925. bnode.append(fnode)
  926. tnode.append(bnode)
  927. # create modules
  928. msnode = etree.Element('modules')
  929. # list of all installed modules for this extension
  930. for module_name in edict[name]['mlist']:
  931. mnode = etree.Element('module')
  932. mnode.text = module_name
  933. msnode.append(mnode)
  934. tnode.append(msnode)
  935. tree.append(tnode)
  936. else:
  937. grass.verbose("Extension already listed in metadata file; metadata not updated!")
  938. write_xml_extensions(xml_file, tree)
  939. return None
  940. def install_module_xml(mlist):
  941. """Update XML files with metadata about installed modules and toolbox
  942. of an private addon
  943. """
  944. xml_file = os.path.join(options['prefix'], 'modules.xml')
  945. # create an empty file if not exists
  946. if not os.path.exists(xml_file):
  947. write_xml_modules(xml_file)
  948. # read XML file
  949. tree = etree_fromfile(xml_file)
  950. # update tree
  951. for name in mlist:
  952. try:
  953. desc = gtask.parse_interface(name).description
  954. # mname = gtask.parse_interface(name).name
  955. keywords = gtask.parse_interface(name).keywords
  956. except Exception as e:
  957. grass.warning(_("No metadata available for module '%s'.")
  958. % name)
  959. continue
  960. tnode = None
  961. for node in tree.findall('task'):
  962. if node.get('name') == name:
  963. tnode = node
  964. break
  965. if tnode is None:
  966. # create new node for task
  967. tnode = etree.Element('task', attrib={'name': name})
  968. dnode = etree.Element('description')
  969. dnode.text = desc
  970. tnode.append(dnode)
  971. knode = etree.Element('keywords')
  972. knode.text = (',').join(keywords)
  973. tnode.append(knode)
  974. # binary files installed with an extension are now
  975. # listed in extensions.xml
  976. """
  977. # create binary
  978. bnode = etree.Element('binary')
  979. list_of_binary_files = []
  980. for file_name in os.listdir(url):
  981. file_type = os.path.splitext(file_name)[-1]
  982. file_n = os.path.splitext(file_name)[0]
  983. html_path = os.path.join(options['prefix'], 'docs', 'html')
  984. c_path = os.path.join(options['prefix'], 'bin')
  985. py_path = os.path.join(options['prefix'], 'scripts')
  986. # html or image file
  987. if file_type in ['.html', '.jpg', '.png'] \
  988. and file_n in os.listdir(html_path):
  989. list_of_binary_files.append(os.path.join(html_path, file_name))
  990. # c file
  991. elif file_type in ['.c'] and file_name in os.listdir(c_path):
  992. list_of_binary_files.append(os.path.join(c_path, file_n))
  993. # python file
  994. elif file_type in ['.py'] and file_name in os.listdir(py_path):
  995. list_of_binary_files.append(os.path.join(py_path, file_n))
  996. # man file
  997. man_path = os.path.join(options['prefix'], 'docs', 'man', 'man1')
  998. if name + '.1' in os.listdir(man_path):
  999. list_of_binary_files.append(os.path.join(man_path, name + '.1'))
  1000. # add binaries to xml file
  1001. for binary_file_name in list_of_binary_files:
  1002. fnode = etree.Element('file')
  1003. fnode.text = binary_file_name
  1004. bnode.append(fnode)
  1005. tnode.append(bnode)
  1006. """
  1007. tree.append(tnode)
  1008. else:
  1009. grass.verbose("Extension module already listed in metadata file; metadata not updated!")
  1010. write_xml_modules(xml_file, tree)
  1011. return mlist
  1012. def install_extension_win(name):
  1013. """Install extension on MS Windows"""
  1014. grass.message(_("Downloading precompiled GRASS Addons <%s>...") %
  1015. options['extension'])
  1016. # build base URL
  1017. if build_platform == 'x86_64':
  1018. platform = build_platform
  1019. else:
  1020. platform = 'x86'
  1021. base_url = "http://wingrass.fsv.cvut.cz/" \
  1022. "grass%(major)s%(minor)s/%(platform)s/addons/" \
  1023. "grass-%(major)s.%(minor)s.%(patch)s" % \
  1024. {'platform': platform,
  1025. 'major': version[0], 'minor': version[1],
  1026. 'patch': version[2]}
  1027. # resolve ZIP URL
  1028. source, url = resolve_source_code(url='{0}/{1}.zip'.format(base_url, name))
  1029. # to hide non-error messages from subprocesses
  1030. if grass.verbosity() <= 2:
  1031. outdev = open(os.devnull, 'w')
  1032. else:
  1033. outdev = sys.stdout
  1034. # download Addons ZIP file
  1035. os.chdir(TMPDIR) # this is just to not leave something behind
  1036. srcdir = os.path.join(TMPDIR, name)
  1037. download_source_code(source=source, url=url, name=name,
  1038. outdev=outdev, directory=srcdir, tmpdir=TMPDIR)
  1039. # collect module names and file names
  1040. module_list = list()
  1041. for r, d, f in os.walk(srcdir):
  1042. for file in f:
  1043. if file.endswith('.py'):
  1044. modulename = file.rstrip(".py")
  1045. module_list.append(modulename)
  1046. if file.endswith('.exe'):
  1047. modulename = file.rstrip(".exe")
  1048. module_list.append(modulename)
  1049. # remove duplicates in case there are .exe wrappers for python scripts
  1050. module_list = set(module_list)
  1051. # change shebang from python to python3
  1052. pyfiles = []
  1053. for r, d, f in os.walk(srcdir):
  1054. for file in f:
  1055. if file.endswith('.py'):
  1056. pyfiles.append(os.path.join(r, file))
  1057. for filename in pyfiles:
  1058. with fileinput.FileInput(filename, inplace=True) as file:
  1059. for line in file:
  1060. print(line.replace(
  1061. "#!/usr/bin/env python\n",
  1062. "#!/usr/bin/env python3\n"
  1063. ), end='')
  1064. # collect old files
  1065. old_file_list = list()
  1066. for r, d, f in os.walk(options['prefix']):
  1067. for filename in f:
  1068. fullname = os.path.join(r, filename)
  1069. old_file_list.append(fullname)
  1070. # copy Addons copy tree to destination directory
  1071. move_extracted_files(extract_dir=srcdir, target_dir=options['prefix'],
  1072. files=os.listdir(srcdir))
  1073. # collect new files
  1074. file_list = list()
  1075. for r, d, f in os.walk(options['prefix']):
  1076. for filename in f:
  1077. fullname = os.path.join(r, filename)
  1078. if fullname not in old_file_list:
  1079. file_list.append(fullname)
  1080. return 0, module_list, file_list
  1081. def download_source_code_svn(url, name, outdev, directory=None):
  1082. """Download source code from a Subversion repository
  1083. .. note:
  1084. Stdout is passed to to *outdev* while stderr is will be just printed.
  1085. :param url: URL of the repository
  1086. (module class/family and name are attached)
  1087. :param name: module name
  1088. :param outdev: output divide for the standard output of the svn command
  1089. :param directory: directory where the source code will be downloaded
  1090. (default is the current directory with name attached)
  1091. :returns: full path to the directory with the source code
  1092. (useful when you not specify directory, if *directory* is specified
  1093. the return value is equal to it)
  1094. """
  1095. if not directory:
  1096. directory = os.path.join(os.getcwd, name)
  1097. classchar = name.split('.', 1)[0]
  1098. moduleclass = expand_module_class_name(classchar)
  1099. url = url + '/' + moduleclass + '/' + name
  1100. if grass.call(['svn', 'checkout',
  1101. url, directory], stdout=outdev) != 0:
  1102. grass.fatal(_("GRASS Addons <%s> not found") % name)
  1103. return directory
  1104. def download_source_code_official_github(url, name, outdev, directory=None):
  1105. """Download source code from a official GitHub repository
  1106. .. note:
  1107. Stdout is passed to to *outdev* while stderr is will be just printed.
  1108. :param url: URL of the repository
  1109. (module class/family and name are attached)
  1110. :param name: module name
  1111. :param outdev: output divide for the standard output of the svn command
  1112. :param directory: directory where the source code will be downloaded
  1113. (default is the current directory with name attached)
  1114. :returns: full path to the directory with the source code
  1115. (useful when you not specify directory, if *directory* is specified
  1116. the return value is equal to it)
  1117. """
  1118. if not directory:
  1119. directory = os.path.join(os.getcwd, name)
  1120. classchar = name.split('.', 1)[0]
  1121. moduleclass = expand_module_class_name(classchar)
  1122. if grass.call(['svn', 'export',
  1123. url, directory], stdout=outdev) != 0:
  1124. grass.fatal(_("GRASS Addons <%s> not found") % name)
  1125. return directory
  1126. def move_extracted_files(extract_dir, target_dir, files):
  1127. """Fix state of extracted files by moving them to different directory
  1128. When extracting, it is not clear what will be the root directory
  1129. or if there will be one at all. So this function moves the files to
  1130. a different directory in the way that if there was one directory extracted,
  1131. the contained files are moved.
  1132. """
  1133. gscript.debug("move_extracted_files({0})".format(locals()))
  1134. if len(files) == 1:
  1135. shutil.copytree(os.path.join(extract_dir, files[0]), target_dir)
  1136. else:
  1137. if not os.path.exists(target_dir):
  1138. os.mkdir(target_dir)
  1139. for file_name in files:
  1140. actual_file = os.path.join(extract_dir, file_name)
  1141. if os.path.isdir(actual_file):
  1142. # shutil.copytree() replaced by copy_tree() because
  1143. # shutil's copytree() fails when subdirectory exists
  1144. copy_tree(actual_file,
  1145. os.path.join(target_dir, file_name))
  1146. else:
  1147. shutil.copy(actual_file, os.path.join(target_dir, file_name))
  1148. # Original copyright and license of the original version of the CRLF function
  1149. # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010
  1150. # Python Software Foundation; All Rights Reserved
  1151. # Python Software Foundation License Version 2
  1152. # http://svn.python.org/projects/python/trunk/Tools/scripts/crlf.py
  1153. def fix_newlines(directory):
  1154. """Replace CRLF with LF in all files in the directory
  1155. Binary files are ignored. Recurses into subdirectories.
  1156. """
  1157. # skip binary files
  1158. # see https://stackoverflow.com/a/7392391
  1159. textchars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7f})
  1160. def is_binary_string(bytes):
  1161. return bool(bytes.translate(None, textchars))
  1162. for root, unused, files in os.walk(directory):
  1163. for name in files:
  1164. filename = os.path.join(root, name)
  1165. if is_binary_string(open(filename, 'rb').read(1024)):
  1166. continue # ignore binary files
  1167. # read content of text file
  1168. with open(filename, 'rb') as fd:
  1169. data = fd.read()
  1170. # we don't expect there would be CRLF file by
  1171. # purpose if we want to allow CRLF files we would
  1172. # have to whitelite .py etc
  1173. newdata = data.replace(b'\r\n', b'\n')
  1174. if newdata != data:
  1175. with open(filename, 'wb') as newfile:
  1176. newfile.write(newdata)
  1177. def extract_zip(name, directory, tmpdir):
  1178. """Extract a ZIP file into a directory"""
  1179. gscript.debug("extract_zip(name={name}, directory={directory},"
  1180. " tmpdir={tmpdir})".format(name=name, directory=directory,
  1181. tmpdir=tmpdir), 3)
  1182. try:
  1183. zip_file = zipfile.ZipFile(name, mode='r')
  1184. file_list = zip_file.namelist()
  1185. # we suppose we can write to parent of the given dir
  1186. # (supposing a tmp dir)
  1187. extract_dir = os.path.join(tmpdir, 'extract_dir')
  1188. os.mkdir(extract_dir)
  1189. for subfile in file_list:
  1190. # this should be safe in Python 2.7.4
  1191. zip_file.extract(subfile, extract_dir)
  1192. files = os.listdir(extract_dir)
  1193. move_extracted_files(extract_dir=extract_dir,
  1194. target_dir=directory, files=files)
  1195. except zipfile.BadZipfile as error:
  1196. gscript.fatal(_("ZIP file is unreadable: {0}").format(error))
  1197. # TODO: solve the other related formats
  1198. def extract_tar(name, directory, tmpdir):
  1199. """Extract a TAR or a similar file into a directory"""
  1200. gscript.debug("extract_tar(name={name}, directory={directory},"
  1201. " tmpdir={tmpdir})".format(name=name, directory=directory,
  1202. tmpdir=tmpdir), 3)
  1203. try:
  1204. import tarfile # we don't need it anywhere else
  1205. tar = tarfile.open(name)
  1206. extract_dir = os.path.join(tmpdir, 'extract_dir')
  1207. os.mkdir(extract_dir)
  1208. tar.extractall(path=extract_dir)
  1209. files = os.listdir(extract_dir)
  1210. move_extracted_files(extract_dir=extract_dir,
  1211. target_dir=directory, files=files)
  1212. except tarfile.TarError as error:
  1213. gscript.fatal(_("Archive file is unreadable: {0}").format(error))
  1214. extract_tar.supported_formats = ['tar.gz', 'gz', 'bz2', 'tar', 'gzip', 'targz']
  1215. def download_source_code(source, url, name, outdev,
  1216. directory=None, tmpdir=None):
  1217. """Get source code to a local directory for compilation"""
  1218. gscript.verbose("Downloading source code for <{name}> from <{url}>"
  1219. " which is identified as '{source}' type of source..."
  1220. .format(source=source, url=url, name=name))
  1221. if source == 'official':
  1222. download_source_code_official_github(url, name, outdev, directory)
  1223. elif source == 'svn':
  1224. download_source_code_svn(url, name, outdev, directory)
  1225. elif source in ['remote_zip']: # , 'official'
  1226. # we expect that the module.zip file is not by chance in the archive
  1227. zip_name = os.path.join(tmpdir, 'extension.zip')
  1228. try:
  1229. response = urlopen(url)
  1230. except URLError:
  1231. grass.fatal(_("Extension <%s> not found") % name)
  1232. with open(zip_name, 'wb') as out_file:
  1233. shutil.copyfileobj(response, out_file)
  1234. extract_zip(name=zip_name, directory=directory, tmpdir=tmpdir)
  1235. fix_newlines(directory)
  1236. elif source.startswith('remote_') and \
  1237. source.split('_')[1] in extract_tar.supported_formats:
  1238. # we expect that the module.tar.gz file is not by chance in the archive
  1239. archive_name = os.path.join(tmpdir,
  1240. 'extension.' + source.split('_')[1])
  1241. urlretrieve(url, archive_name)
  1242. extract_tar(name=archive_name, directory=directory, tmpdir=tmpdir)
  1243. fix_newlines(directory)
  1244. elif source == 'zip':
  1245. extract_zip(name=url, directory=directory, tmpdir=tmpdir)
  1246. fix_newlines(directory)
  1247. elif source in extract_tar.supported_formats:
  1248. extract_tar(name=url, directory=directory, tmpdir=tmpdir)
  1249. fix_newlines(directory)
  1250. elif source == 'dir':
  1251. shutil.copytree(url, directory)
  1252. fix_newlines(directory)
  1253. else:
  1254. # probably programmer error
  1255. grass.fatal(_("Unknown extension (addon) source type '{0}'."
  1256. " Please report this to the grass-user mailing list.")
  1257. .format(source))
  1258. assert os.path.isdir(directory)
  1259. def install_extension_std_platforms(name, source, url):
  1260. """Install extension on standard platforms"""
  1261. gisbase = os.getenv('GISBASE')
  1262. source_url = 'https://github.com/OSGeo/grass-addons/tree/master/grass7/'
  1263. if source == 'official':
  1264. gscript.message(_("Fetching <%s> from "
  1265. "GRASS GIS Addons repository (be patient)...") % name)
  1266. else:
  1267. gscript.message(_("Fetching <{name}> from "
  1268. "<{url}> (be patient)...").format(name=name, url=url))
  1269. # to hide non-error messages from subprocesses
  1270. if grass.verbosity() <= 2:
  1271. outdev = open(os.devnull, 'w')
  1272. else:
  1273. outdev = sys.stdout
  1274. os.chdir(TMPDIR) # this is just to not leave something behind
  1275. srcdir = os.path.join(TMPDIR, name)
  1276. download_source_code(source=source, url=url, name=name,
  1277. outdev=outdev, directory=srcdir, tmpdir=TMPDIR)
  1278. os.chdir(srcdir)
  1279. # collect module names
  1280. module_list = list()
  1281. for r, d, f in os.walk(srcdir):
  1282. for filename in f:
  1283. if filename == "Makefile":
  1284. # get the module name: PGM = <module name>
  1285. with open(os.path.join(r, 'Makefile')) as fp:
  1286. for line in fp.readlines():
  1287. if "PGM =" in line:
  1288. modulename = line.split('=')[1].strip()
  1289. module_list.append(modulename)
  1290. # change shebang from python to python3
  1291. pyfiles = []
  1292. # r=root, d=directories, f = files
  1293. for r, d, f in os.walk(srcdir):
  1294. for file in f:
  1295. if file.endswith('.py'):
  1296. pyfiles.append(os.path.join(r, file))
  1297. for filename in pyfiles:
  1298. with fileinput.FileInput(filename, inplace=True) as file:
  1299. for line in file:
  1300. print(line.replace(
  1301. "#!/usr/bin/env python\n",
  1302. "#!/usr/bin/env python3\n"
  1303. ), end='')
  1304. dirs = {
  1305. 'bin': os.path.join(TMPDIR, name, 'bin'),
  1306. 'docs': os.path.join(TMPDIR, name, 'docs'),
  1307. 'html': os.path.join(TMPDIR, name, 'docs', 'html'),
  1308. 'rest': os.path.join(TMPDIR, name, 'docs', 'rest'),
  1309. 'man': os.path.join(TMPDIR, name, 'docs', 'man'),
  1310. 'script': os.path.join(TMPDIR, name, 'scripts'),
  1311. # TODO: handle locales also for addons
  1312. # 'string' : os.path.join(TMPDIR, name, 'locale'),
  1313. 'string': os.path.join(TMPDIR, name),
  1314. 'etc': os.path.join(TMPDIR, name, 'etc'),
  1315. }
  1316. make_cmd = [
  1317. 'make',
  1318. 'MODULE_TOPDIR=%s' % gisbase.replace(' ', r'\ '),
  1319. 'RUN_GISRC=%s' % os.environ['GISRC'],
  1320. 'BIN=%s' % dirs['bin'],
  1321. 'HTMLDIR=%s' % dirs['html'],
  1322. 'RESTDIR=%s' % dirs['rest'],
  1323. 'MANBASEDIR=%s' % dirs['man'],
  1324. 'SCRIPTDIR=%s' % dirs['script'],
  1325. 'STRINGDIR=%s' % dirs['string'],
  1326. 'ETC=%s' % os.path.join(dirs['etc']),
  1327. 'SOURCE_URL=%s' % source_url
  1328. ]
  1329. install_cmd = [
  1330. 'make',
  1331. 'MODULE_TOPDIR=%s' % gisbase,
  1332. 'ARCH_DISTDIR=%s' % os.path.join(TMPDIR, name),
  1333. 'INST_DIR=%s' % options['prefix'],
  1334. 'install'
  1335. ]
  1336. if flags['d']:
  1337. grass.message("\n%s\n" % _("To compile run:"))
  1338. sys.stderr.write(' '.join(make_cmd) + '\n')
  1339. grass.message("\n%s\n" % _("To install run:"))
  1340. sys.stderr.write(' '.join(install_cmd) + '\n')
  1341. return 0, None, None, None
  1342. os.chdir(os.path.join(TMPDIR, name))
  1343. grass.message(_("Compiling..."))
  1344. if not os.path.exists(os.path.join(gisbase, 'include',
  1345. 'Make', 'Module.make')):
  1346. grass.fatal(_("Please install GRASS development package"))
  1347. if 0 != grass.call(make_cmd,
  1348. stdout=outdev):
  1349. grass.fatal(_('Compilation failed, sorry.'
  1350. ' Please check above error messages.'))
  1351. if flags['i']:
  1352. return 0, None, None, None
  1353. # collect old files
  1354. old_file_list = list()
  1355. for r, d, f in os.walk(options['prefix']):
  1356. for filename in f:
  1357. fullname = os.path.join(r, filename)
  1358. old_file_list.append(fullname)
  1359. grass.message(_("Installing..."))
  1360. ret = grass.call(install_cmd, stdout=outdev)
  1361. # collect new files
  1362. file_list = list()
  1363. for r, d, f in os.walk(options['prefix']):
  1364. for filename in f:
  1365. fullname = os.path.join(r, filename)
  1366. if fullname not in old_file_list:
  1367. file_list.append(fullname)
  1368. return ret, module_list, file_list, os.path.join(TMPDIR, name)
  1369. def remove_extension(force=False):
  1370. """Remove existing extension
  1371. extension or toolbox with extensions if -t is given)"""
  1372. if flags['t']:
  1373. edict = get_toolbox_extensions(options['prefix'], options['extension'])
  1374. else:
  1375. edict = dict()
  1376. edict[options['extension']] = dict()
  1377. # list of modules installed by this extension
  1378. edict[options['extension']]['mlist'] = list()
  1379. # list of files installed by this extension
  1380. edict[options['extension']]['flist'] = list()
  1381. # collect modules and files installed by these extensions
  1382. mlist = list()
  1383. xml_file = os.path.join(options['prefix'], 'extensions.xml')
  1384. if os.path.exists(xml_file):
  1385. # read XML file
  1386. tree = None
  1387. try:
  1388. tree = etree_fromfile(xml_file)
  1389. except ETREE_EXCEPTIONS + (OSError, IOError):
  1390. os.remove(xml_file)
  1391. write_xml_extensions(xml_file)
  1392. if tree is not None:
  1393. for tnode in tree.findall('task'):
  1394. ename = tnode.get('name').strip()
  1395. if ename in edict:
  1396. # modules installed by this extension
  1397. mnode = tnode.find('modules')
  1398. if mnode:
  1399. for fnode in mnode.findall('module'):
  1400. mname = fnode.text.strip()
  1401. edict[ename]['mlist'].append(mname)
  1402. mlist.append(mname)
  1403. # files installed by this extension
  1404. bnode = tnode.find('binary')
  1405. if bnode:
  1406. for fnode in bnode.findall('file'):
  1407. bname = fnode.text.strip()
  1408. edict[ename]['flist'].append(bname)
  1409. else:
  1410. if force:
  1411. write_xml_extensions(xml_file)
  1412. xml_file = os.path.join(options['prefix'], 'modules.xml')
  1413. if not os.path.exists(xml_file):
  1414. if force:
  1415. write_xml_modules(xml_file)
  1416. else:
  1417. grass.debug("No addons metadata file available", 1)
  1418. # read XML file
  1419. tree = None
  1420. try:
  1421. tree = etree_fromfile(xml_file)
  1422. except ETREE_EXCEPTIONS + (OSError, IOError):
  1423. os.remove(xml_file)
  1424. write_xml_modules(xml_file)
  1425. return []
  1426. if tree is not None:
  1427. for tnode in tree.findall('task'):
  1428. ename = tnode.get('name').strip()
  1429. if ename in edict:
  1430. # assume extension name == module name
  1431. edict[ename]['mlist'].append(ename)
  1432. mlist.append(ename)
  1433. # files installed by this extension
  1434. bnode = tnode.find('binary')
  1435. if bnode:
  1436. for fnode in bnode.findall('file'):
  1437. bname = fnode.text.strip()
  1438. edict[ename]['flist'].append(bname)
  1439. if force:
  1440. grass.verbose(_("List of removed files:"))
  1441. else:
  1442. grass.info(_("Files to be removed:"))
  1443. eremoved = remove_extension_files(edict, force)
  1444. if force:
  1445. if len(eremoved) > 0:
  1446. grass.message(_("Updating addons metadata file..."))
  1447. remove_extension_xml(mlist, edict)
  1448. for ename in edict:
  1449. if ename in eremoved:
  1450. grass.message(_("Extension <%s> successfully uninstalled.") %
  1451. ename)
  1452. else:
  1453. if flags['t']:
  1454. grass.warning(_("Toolbox <%s> not removed. "
  1455. "Re-run '%s' with '-f' flag to force removal")
  1456. % (options['extension'], 'g.extension'))
  1457. else:
  1458. grass.warning(_("Extension <%s> not removed. "
  1459. "Re-run '%s' with '-f' flag to force removal")
  1460. % (options['extension'], 'g.extension'))
  1461. # remove existing extension(s) (reading XML file)
  1462. def remove_extension_files(edict, force=False):
  1463. """Remove extensions specified in a dictionary
  1464. Uses the file names from the file list of the dictionary
  1465. Fallbacks to standard layout of files on prefix path on error.
  1466. """
  1467. # try to read XML metadata file first
  1468. xml_file = os.path.join(options['prefix'], 'extensions.xml')
  1469. einstalled = list()
  1470. eremoved = list()
  1471. if os.path.exists(xml_file):
  1472. tree = etree_fromfile(xml_file)
  1473. if tree is not None:
  1474. for task in tree.findall('task'):
  1475. ename = task.get('name').strip()
  1476. einstalled.append(ename)
  1477. else:
  1478. tree = None
  1479. for name in edict:
  1480. removed = True
  1481. if len(edict[name]['flist']) > 0:
  1482. err = list()
  1483. for fpath in edict[name]['flist']:
  1484. grass.verbose(fpath)
  1485. if force:
  1486. try:
  1487. os.remove(fpath)
  1488. except OSError:
  1489. msg = "Unable to remove file '%s'"
  1490. err.append((_(msg) % fpath))
  1491. removed = False
  1492. if len(err) > 0:
  1493. for error_line in err:
  1494. grass.error(error_line)
  1495. else:
  1496. if name not in einstalled:
  1497. # try even if module does not seem to be available,
  1498. # as the user may be trying to get rid of left over cruft
  1499. grass.warning(_("Extension <%s> not found") % name)
  1500. remove_extension_std(name, force)
  1501. removed = False
  1502. if removed is True:
  1503. eremoved.append(name)
  1504. return eremoved
  1505. def remove_extension_std(name, force=False):
  1506. """Remove extension/module expecting the standard layout
  1507. Any images for manuals or files installed in etc will not be
  1508. removed
  1509. """
  1510. for fpath in [os.path.join(options['prefix'], 'bin', name),
  1511. os.path.join(options['prefix'], 'scripts', name),
  1512. os.path.join(
  1513. options['prefix'], 'docs', 'html', name + '.html'),
  1514. os.path.join(
  1515. options['prefix'], 'docs', 'rest', name + '.txt'),
  1516. os.path.join(options['prefix'], 'docs', 'man', 'man1',
  1517. name + '.1')]:
  1518. if os.path.isfile(fpath):
  1519. grass.verbose(fpath)
  1520. if force:
  1521. os.remove(fpath)
  1522. # remove module libraries under GRASS_ADDONS/etc/{name}/*
  1523. libpath = os.path.join(options['prefix'], 'etc', name)
  1524. if os.path.isdir(libpath):
  1525. grass.verbose(libpath)
  1526. if force:
  1527. shutil.rmtree(libpath)
  1528. def remove_from_toolbox_xml(name):
  1529. """Update local meta-file when removing existing toolbox"""
  1530. xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
  1531. if not os.path.exists(xml_file):
  1532. return
  1533. # read XML file
  1534. tree = etree_fromfile(xml_file)
  1535. for node in tree.findall('toolbox'):
  1536. if node.get('code') != name:
  1537. continue
  1538. tree.remove(node)
  1539. write_xml_toolboxes(xml_file, tree)
  1540. def remove_extension_xml(mlist, edict):
  1541. """Update local meta-file when removing existing extension"""
  1542. if len(edict) > 1:
  1543. # update also toolboxes metadata
  1544. remove_from_toolbox_xml(options['extension'])
  1545. # modules
  1546. xml_file = os.path.join(options['prefix'], 'modules.xml')
  1547. if os.path.exists(xml_file):
  1548. # read XML file
  1549. tree = etree_fromfile(xml_file)
  1550. for name in mlist:
  1551. for node in tree.findall('task'):
  1552. if node.get('name') != name:
  1553. continue
  1554. tree.remove(node)
  1555. write_xml_modules(xml_file, tree)
  1556. # extensions
  1557. xml_file = os.path.join(options['prefix'], 'extensions.xml')
  1558. if os.path.exists(xml_file):
  1559. # read XML file
  1560. tree = etree_fromfile(xml_file)
  1561. for name in edict:
  1562. for node in tree.findall('task'):
  1563. if node.get('name') != name:
  1564. continue
  1565. tree.remove(node)
  1566. write_xml_extensions(xml_file, tree)
  1567. # check links in CSS
  1568. def check_style_file(name):
  1569. """Ensures that a specified HTML documentation support file exists
  1570. If the file, e.g. a CSS file does not exist, the file is copied from
  1571. the distribution.
  1572. If the files are missing, a warning is issued.
  1573. """
  1574. dist_file = os.path.join(os.getenv('GISBASE'), 'docs', 'html', name)
  1575. addons_file = os.path.join(options['prefix'], 'docs', 'html', name)
  1576. if os.path.isfile(addons_file):
  1577. return
  1578. try:
  1579. shutil.copyfile(dist_file, addons_file)
  1580. except OSError as error:
  1581. grass.warning(
  1582. _("Unable to create '{filename}': {error}."
  1583. " Is the GRASS GIS documentation package installed?"
  1584. " Installation continues,"
  1585. " but documentation may not look right.").format(
  1586. filename=addons_file, error=error))
  1587. def create_dir(path):
  1588. """Creates the specified directory (with all dirs in between)
  1589. NOOP for existing directory.
  1590. """
  1591. if os.path.isdir(path):
  1592. return
  1593. try:
  1594. os.makedirs(path)
  1595. except OSError as error:
  1596. grass.fatal(_("Unable to create '%s': %s") % (path, error))
  1597. grass.debug("'%s' created" % path)
  1598. def check_dirs():
  1599. """Ensure that the necessary directories in prefix path exist"""
  1600. create_dir(os.path.join(options['prefix'], 'bin'))
  1601. create_dir(os.path.join(options['prefix'], 'docs', 'html'))
  1602. create_dir(os.path.join(options['prefix'], 'docs', 'rest'))
  1603. check_style_file('grass_logo.png')
  1604. check_style_file('grassdocs.css')
  1605. create_dir(os.path.join(options['prefix'], 'etc'))
  1606. create_dir(os.path.join(options['prefix'], 'docs', 'man', 'man1'))
  1607. create_dir(os.path.join(options['prefix'], 'scripts'))
  1608. # fix file URI in manual page
  1609. def update_manual_page(module):
  1610. """Fix manual page for addons which are at different directory
  1611. than core modules"""
  1612. if module.split('.', 1)[0] == 'wx':
  1613. return # skip for GUI modules
  1614. grass.verbose(_("Manual page for <%s> updated") % module)
  1615. # read original html file
  1616. htmlfile = os.path.join(
  1617. options['prefix'], 'docs', 'html', module + '.html')
  1618. try:
  1619. oldfile = open(htmlfile)
  1620. shtml = oldfile.read()
  1621. except IOError as error:
  1622. gscript.fatal(_("Unable to read manual page: %s") % error)
  1623. else:
  1624. oldfile.close()
  1625. pos = []
  1626. # fix logo URL
  1627. pattern = r'''<a href="([^"]+)"><img src="grass_logo.png"'''
  1628. for match in re.finditer(pattern, shtml):
  1629. pos.append(match.start(1))
  1630. # find URIs
  1631. pattern = r'''<a href="([^"]+)">([^>]+)</a>'''
  1632. addons = get_installed_extensions(force=True)
  1633. for match in re.finditer(pattern, shtml):
  1634. if match.group(1)[:4] == 'http':
  1635. continue
  1636. if match.group(1).replace('.html', '') in addons:
  1637. continue
  1638. pos.append(match.start(1))
  1639. if not pos:
  1640. return # no match
  1641. # replace file URIs
  1642. prefix = 'file://' + '/'.join([os.getenv('GISBASE'), 'docs', 'html'])
  1643. ohtml = shtml[:pos[0]]
  1644. for i in range(1, len(pos)):
  1645. ohtml += prefix + '/' + shtml[pos[i - 1]:pos[i]]
  1646. ohtml += prefix + '/' + shtml[pos[-1]:]
  1647. # write updated html file
  1648. try:
  1649. newfile = open(htmlfile, 'w')
  1650. newfile.write(ohtml)
  1651. except IOError as error:
  1652. gscript.fatal(_("Unable for write manual page: %s") % error)
  1653. else:
  1654. newfile.close()
  1655. def resolve_install_prefix(path, to_system):
  1656. """Determine and check the path for installation"""
  1657. if to_system:
  1658. path = os.environ['GISBASE']
  1659. if path == '$GRASS_ADDON_BASE':
  1660. if not os.getenv('GRASS_ADDON_BASE'):
  1661. grass.warning(_("GRASS_ADDON_BASE is not defined, "
  1662. "installing to ~/.grass%s/addons") % version[0])
  1663. path = os.path.join(
  1664. os.environ['HOME'], '.grass%s' % version[0], 'addons')
  1665. else:
  1666. path = os.environ['GRASS_ADDON_BASE']
  1667. if os.path.exists(path) and \
  1668. not os.access(path, os.W_OK):
  1669. grass.fatal(_("You don't have permission to install extension to <{0}>."
  1670. " Try to run {1} with administrator rights"
  1671. " (su or sudo).")
  1672. .format(path, 'g.extension'))
  1673. # ensure dir sep at the end for cases where path is used as URL and pasted
  1674. # together with file names
  1675. if not path.endswith(os.path.sep):
  1676. path = path + os.path.sep
  1677. return os.path.abspath(path) # make likes absolute paths
  1678. def resolve_xmlurl_prefix(url, source=None):
  1679. """Determine and check the URL where the XML metadata files are stored
  1680. It ensures that there is a single slash at the end of URL, so we can attach
  1681. file name easily:
  1682. >>> resolve_xmlurl_prefix('https://grass.osgeo.org/addons')
  1683. 'https://grass.osgeo.org/addons/'
  1684. >>> resolve_xmlurl_prefix('https://grass.osgeo.org/addons/')
  1685. 'https://grass.osgeo.org/addons/'
  1686. """
  1687. gscript.debug("resolve_xmlurl_prefix(url={0}, source={1})".format(url, source))
  1688. if source == 'official':
  1689. # use pregenerated modules XML file
  1690. url = 'https://grass.osgeo.org/addons/grass%s/' % version[0]
  1691. # else try to get extensions XMl from SVN repository (provided URL)
  1692. # the exact action depends on subsequent code (somewhere)
  1693. if not url.endswith('/'):
  1694. url = url + '/'
  1695. return url
  1696. KNOWN_HOST_SERVICES_INFO = {
  1697. 'OSGeo Trac': {
  1698. 'domain': 'trac.osgeo.org',
  1699. 'ignored_suffixes': ['format=zip'],
  1700. 'possible_starts': ['', 'https://', 'http://'],
  1701. 'url_start': 'https://',
  1702. 'url_end': '?format=zip',
  1703. },
  1704. 'GitHub': {
  1705. 'domain': 'github.com',
  1706. 'ignored_suffixes': ['.zip', '.tar.gz'],
  1707. 'possible_starts': ['', 'https://', 'http://'],
  1708. 'url_start': 'https://',
  1709. 'url_end': '/archive/master.zip',
  1710. },
  1711. 'GitLab': {
  1712. 'domain': 'gitlab.com',
  1713. 'ignored_suffixes': ['.zip', '.tar.gz', '.tar.bz2', '.tar'],
  1714. 'possible_starts': ['', 'https://', 'http://'],
  1715. 'url_start': 'https://',
  1716. 'url_end': '/-/archive/master/{name}-master.zip',
  1717. },
  1718. 'Bitbucket': {
  1719. 'domain': 'bitbucket.org',
  1720. 'ignored_suffixes': ['.zip', '.tar.gz', '.gz', '.bz2'],
  1721. 'possible_starts': ['', 'https://', 'http://'],
  1722. 'url_start': 'https://',
  1723. 'url_end': '/get/master.zip',
  1724. },
  1725. }
  1726. # TODO: support ZIP URLs which don't end with zip
  1727. # https://gitlab.com/user/reponame/repository/archive.zip?ref=b%C3%A9po
  1728. def resolve_known_host_service(url, name):
  1729. """Determine source type and full URL for known hosting service
  1730. If the service is not determined from the provided URL, tuple with
  1731. is two ``None`` values is returned.
  1732. :param url: URL
  1733. :param name: module name
  1734. """
  1735. match = None
  1736. actual_start = None
  1737. for key, value in KNOWN_HOST_SERVICES_INFO.items():
  1738. for start in value['possible_starts']:
  1739. if url.startswith(start + value['domain']):
  1740. match = value
  1741. actual_start = start
  1742. gscript.verbose(_("Identified {0} as known hosting service")
  1743. .format(key))
  1744. for suffix in value['ignored_suffixes']:
  1745. if url.endswith(suffix):
  1746. gscript.verbose(
  1747. _("Not using {service} as known hosting service"
  1748. " because the URL ends with '{suffix}'")
  1749. .format(service=key, suffix=suffix))
  1750. return None, None
  1751. if match:
  1752. if not actual_start:
  1753. actual_start = match['url_start']
  1754. else:
  1755. actual_start = ''
  1756. url = '{prefix}{base}{suffix}'.format(prefix=actual_start,
  1757. base=url.rstrip('/'),
  1758. suffix=match['url_end'].format(
  1759. name=name))
  1760. gscript.verbose(_("Will use the following URL for download: {0}")
  1761. .format(url))
  1762. return 'remote_zip', url
  1763. else:
  1764. return None, None
  1765. # TODO: add also option to enforce the source type
  1766. def resolve_source_code(url=None, name=None):
  1767. """Return type and URL or path of the source code
  1768. Local paths are not presented as URLs to be usable in standard functions.
  1769. Path is identified as local path if the directory of file exists which
  1770. has the unfortunate consequence that the not existing files are evaluated
  1771. as remote URLs. When path is not evaluated, Subversion is assumed for
  1772. backwards compatibility. When GitHub repository is specified, ZIP file
  1773. link is returned. The ZIP is for master branch, not the default one because
  1774. GitHub does not provide the default branch in the URL (July 2015).
  1775. :returns: tuple with type of source and full URL or path
  1776. Official repository:
  1777. >>> resolve_source_code(name='g.example') # doctest: +SKIP
  1778. ('official', 'https://trac.osgeo.org/.../general/g.example')
  1779. Subversion:
  1780. >>> resolve_source_code('https://svn.osgeo.org/grass/grass-addons/grass7')
  1781. ('svn', 'https://svn.osgeo.org/grass/grass-addons/grass7')
  1782. ZIP files online:
  1783. >>> resolve_source_code('https://trac.osgeo.org/.../r.modis?format=zip') # doctest: +SKIP
  1784. ('remote_zip', 'https://trac.osgeo.org/.../r.modis?format=zip')
  1785. Local directories and ZIP files:
  1786. >>> resolve_source_code(os.path.expanduser("~")) # doctest: +ELLIPSIS
  1787. ('dir', '...')
  1788. >>> resolve_source_code('/local/directory/downloaded.zip') # doctest: +SKIP
  1789. ('zip', '/local/directory/downloaded.zip')
  1790. OSGeo Trac:
  1791. >>> resolve_source_code('trac.osgeo.org/.../r.agent.aco') # doctest: +SKIP
  1792. ('remote_zip', 'https://trac.osgeo.org/.../r.agent.aco?format=zip')
  1793. >>> resolve_source_code('https://trac.osgeo.org/.../r.agent.aco') # doctest: +SKIP
  1794. ('remote_zip', 'https://trac.osgeo.org/.../r.agent.aco?format=zip')
  1795. GitHub:
  1796. >>> resolve_source_code('github.com/user/g.example') # doctest: +SKIP
  1797. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1798. >>> resolve_source_code('github.com/user/g.example/') # doctest: +SKIP
  1799. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1800. >>> resolve_source_code('https://github.com/user/g.example') # doctest: +SKIP
  1801. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1802. >>> resolve_source_code('https://github.com/user/g.example/') # doctest: +SKIP
  1803. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1804. GitLab:
  1805. >>> resolve_source_code('gitlab.com/JoeUser/GrassModule') # doctest: +SKIP
  1806. ('remote_zip', 'https://gitlab.com/JoeUser/GrassModule/-/archive/master/GrassModule-master.zip')
  1807. >>> resolve_source_code('https://gitlab.com/JoeUser/GrassModule') # doctest: +SKIP
  1808. ('remote_zip', 'https://gitlab.com/JoeUser/GrassModule/-/archive/master/GrassModule-master.zip')
  1809. Bitbucket:
  1810. >>> resolve_source_code('bitbucket.org/joe-user/grass-module') # doctest: +SKIP
  1811. ('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
  1812. >>> resolve_source_code('https://bitbucket.org/joe-user/grass-module') # doctest: +SKIP
  1813. ('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
  1814. """
  1815. if not url and name:
  1816. module_class = get_module_class_name(name)
  1817. # note: 'trunk' is required to make URL usable for 'svn export' call
  1818. git_url = 'https://github.com/OSGeo/grass-addons/trunk/' \
  1819. 'grass{version}/{module_class}/{module_name}' \
  1820. .format(version=version[0],
  1821. module_class=module_class, module_name=name)
  1822. # trac_url = 'https://trac.osgeo.org/grass/browser/grass-addons/' \
  1823. # 'grass{version}/{module_class}/{module_name}?format=zip' \
  1824. # .format(version=version[0],
  1825. # module_class=module_class, module_name=name)
  1826. # return 'official', trac_url
  1827. return 'official', git_url
  1828. # Check if URL can be found
  1829. # Catch corner case if local URL is given starting with file://
  1830. url = url[6:] if url.startswith('file://') else url
  1831. if not os.path.exists(url):
  1832. url_validated = False
  1833. if url.startswith('http'):
  1834. try:
  1835. open_url = urlopen(url)
  1836. open_url.close()
  1837. url_validated = True
  1838. except:
  1839. pass
  1840. else:
  1841. try:
  1842. open_url = urlopen('http://' + url)
  1843. open_url.close()
  1844. url_validated = True
  1845. except:
  1846. pass
  1847. try:
  1848. open_url = urlopen('https://' + url)
  1849. open_url.close()
  1850. url_validated = True
  1851. except:
  1852. pass
  1853. if not url_validated:
  1854. grass.fatal(_('Cannot open URL: {}'.format(url)))
  1855. # Handle local URLs
  1856. if os.path.isdir(url):
  1857. return 'dir', os.path.abspath(url)
  1858. elif os.path.exists(url):
  1859. if url.endswith('.zip'):
  1860. return 'zip', os.path.abspath(url)
  1861. for suffix in extract_tar.supported_formats:
  1862. if url.endswith('.' + suffix):
  1863. return suffix, os.path.abspath(url)
  1864. # Handle remote URLs
  1865. else:
  1866. source, resolved_url = resolve_known_host_service(url, name)
  1867. if source:
  1868. return source, resolved_url
  1869. # we allow URL to end with =zip or ?zip and not only .zip
  1870. # unfortunately format=zip&version=89612 would require something else
  1871. # special option to force the source type would solve it
  1872. if url.endswith('zip'):
  1873. return 'remote_zip', url
  1874. for suffix in extract_tar.supported_formats:
  1875. if url.endswith(suffix):
  1876. return 'remote_' + suffix, url
  1877. # fallback to the classic behavior
  1878. return 'svn', url
  1879. def get_addons_paths(gg_addons_base_dir):
  1880. """Get and save extensions paths as 'extensions_paths.json' json file
  1881. in the $GRASS_ADDON_BASE dir. The file serves as a list of all addons,
  1882. and their paths (mkhmtl.py tool)
  1883. """
  1884. get_addons_paths.json_file = 'addons_paths.json'
  1885. url = 'https://api.github.com/repos/OSGeo/grass-addons/git/trees/'\
  1886. 'master?recursive=1'
  1887. addons_paths = json.loads(
  1888. gscript.decode(
  1889. download_addons_paths_file(
  1890. url=url,
  1891. response_format='application/json',
  1892. ).read(),
  1893. )
  1894. )
  1895. with open(os.path.join(gg_addons_base_dir, get_addons_paths.json_file),
  1896. 'w') as f:
  1897. json.dump(addons_paths, f)
  1898. def main():
  1899. # check dependencies
  1900. if not flags['a'] and sys.platform != "win32":
  1901. check_progs()
  1902. original_url = options['url']
  1903. # manage proxies
  1904. global PROXIES
  1905. if options['proxy']:
  1906. PROXIES = {}
  1907. for ptype, purl in (p.split('=') for p in options['proxy'].split(',')):
  1908. PROXIES[ptype] = purl
  1909. proxy = urlrequest.ProxyHandler(PROXIES)
  1910. opener = urlrequest.build_opener(proxy)
  1911. urlrequest.install_opener(opener)
  1912. # define path
  1913. options['prefix'] = resolve_install_prefix(path=options['prefix'],
  1914. to_system=flags['s'])
  1915. if flags['j']:
  1916. get_addons_paths(gg_addons_base_dir=options['prefix'])
  1917. return 0
  1918. # list available extensions
  1919. if flags['l'] or flags['c'] or (flags['g'] and not flags['a']):
  1920. # using dummy extension, we don't need any extension URL now,
  1921. # but will work only as long as the function does not check
  1922. # if the URL is actually valid or something
  1923. source, url = resolve_source_code(name='dummy',
  1924. url=original_url)
  1925. xmlurl = resolve_xmlurl_prefix(original_url, source=source)
  1926. list_available_extensions(xmlurl)
  1927. return 0
  1928. elif flags['a']:
  1929. list_installed_extensions(toolboxes=flags['t'])
  1930. return 0
  1931. if flags['d'] or flags['i']:
  1932. flag = 'd' if flags['d'] else 'i'
  1933. if options['operation'] != 'add':
  1934. grass.warning(_("Flag '{}' is relevant only to"
  1935. " 'operation=add'. Ignoring this flag.").format(
  1936. flag))
  1937. else:
  1938. global REMOVE_TMPDIR
  1939. REMOVE_TMPDIR = False
  1940. if options['operation'] == 'add':
  1941. check_dirs()
  1942. get_addons_paths(gg_addons_base_dir=options['prefix'])
  1943. source, url = resolve_source_code(name=options['extension'],
  1944. url=original_url)
  1945. xmlurl = resolve_xmlurl_prefix(original_url, source=source)
  1946. install_extension(source=source, url=url, xmlurl=xmlurl)
  1947. else: # remove
  1948. remove_extension(force=flags['f'])
  1949. return 0
  1950. if __name__ == "__main__":
  1951. if len(sys.argv) == 2 and sys.argv[1] == '--doctest':
  1952. import doctest
  1953. sys.exit(doctest.testmod().failed)
  1954. options, flags = grass.parser()
  1955. global TMPDIR
  1956. TMPDIR = tempfile.mkdtemp()
  1957. atexit.register(cleanup)
  1958. grass_version = grass.version()
  1959. version = grass_version['version'].split('.')
  1960. build_platform = grass_version['build_platform'].split('-', 1)[0]
  1961. sys.exit(main())