g.extension.py 53 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607
  1. #!/usr/bin/env python
  2. ############################################################################
  3. #
  4. # MODULE: g.extension
  5. # AUTHOR(S): Markus Neteler
  6. # Pythonized & upgraded for GRASS 7 by Martin Landa <landa.martin gmail.com>
  7. # PURPOSE: Tool to download and install extensions from GRASS Addons SVN into
  8. # local GRASS installation
  9. # COPYRIGHT: (C) 2009-2014 by Markus Neteler, and the GRASS Development Team
  10. #
  11. # This program is free software under the GNU General
  12. # Public License (>=v2). Read the file COPYING that
  13. # comes with GRASS for details.
  14. #
  15. # TODO: add sudo support where needed (i.e. check first permission to write into
  16. # $GISBASE directory)
  17. #############################################################################
  18. #%module
  19. #% label: Maintains GRASS Addons extensions in local GRASS installation.
  20. #% description: Downloads, installs extensions from GRASS Addons SVN repository into local GRASS installation or removes installed extensions.
  21. #% keyword: general
  22. #% keyword: installation
  23. #% keyword: extensions
  24. #%end
  25. #%option
  26. #% key: extension
  27. #% type: string
  28. #% key_desc: name
  29. #% label: Name of extension to install or remove
  30. #% description: Name of toolbox (set of extensions) when -t flag is given
  31. #% required: yes
  32. #%end
  33. #%option
  34. #% key: operation
  35. #% type: string
  36. #% description: Operation to be performed
  37. #% required: yes
  38. #% options: add,remove
  39. #% answer: add
  40. #%end
  41. #%option
  42. #% key: svnurl
  43. #% type: string
  44. #% key_desc: url
  45. #% description: SVN Addons repository URL
  46. #% answer: http://svn.osgeo.org/grass/grass-addons/grass7
  47. #%end
  48. #%option
  49. #% key: prefix
  50. #% type: string
  51. #% key_desc: path
  52. #% description: Prefix where to install extension (ignored when flag -s is given)
  53. #% answer: $GRASS_ADDON_BASE
  54. #% required: no
  55. #%end
  56. #%option
  57. #% key: proxy
  58. #% type: string
  59. #% key_desc: proxy
  60. #% description: Set the proxy with: "http=<value>,ftp=<value>"
  61. #% required: no
  62. #% multiple: yes
  63. #%end
  64. #%flag
  65. #% key: l
  66. #% description: List available extensions in the GRASS Addons SVN repository
  67. #% guisection: Print
  68. #% suppress_required: yes
  69. #%end
  70. #%flag
  71. #% key: c
  72. #% description: List available extensions in the GRASS Addons SVN repository including module description
  73. #% guisection: Print
  74. #% suppress_required: yes
  75. #%end
  76. #%flag
  77. #% key: g
  78. #% description: List available extensions in the GRASS Addons SVN repository (shell script style)
  79. #% guisection: Print
  80. #% suppress_required: yes
  81. #%end
  82. #%flag
  83. #% key: a
  84. #% description: List locally installed extensions
  85. #% guisection: Print
  86. #% suppress_required: yes
  87. #%end
  88. #%flag
  89. #% key: s
  90. #% description: Install system-wide (may need system administrator rights)
  91. #% guisection: Install
  92. #%end
  93. #%flag
  94. #% key: d
  95. #% description: Download source code and exit
  96. #% guisection: Install
  97. #%end
  98. #%flag
  99. #% key: i
  100. #% description: Do not install new extension, just compile it
  101. #% guisection: Install
  102. #%end
  103. #%flag
  104. #% key: f
  105. #% description: Force removal when uninstalling extension (operation=remove)
  106. #% guisection: Remove
  107. #%end
  108. #%flag
  109. #% key: t
  110. #% description: Operate on toolboxes instead of single modules (experimental)
  111. #% suppress_required: yes
  112. #%end
  113. #%rules
  114. #% required: extension, -l, -c, -g, -a
  115. #% exclusive: extension, -l, -c, -g, -a
  116. #%end
  117. from __future__ import print_function
  118. import os
  119. import sys
  120. import re
  121. import atexit
  122. import shutil
  123. import zipfile
  124. import tempfile
  125. try:
  126. from urllib2 import HTTPError
  127. from urllib import urlopen, urlretrieve
  128. except ImportError:
  129. from urllib.request import HTTPError, urlopen, urlretrieve
  130. try:
  131. import xml.etree.ElementTree as etree
  132. except ImportError:
  133. import elementtree.ElementTree as etree # Python <= 2.4
  134. import grass.script as gscript
  135. from grass.script.utils import try_rmdir
  136. from grass.script import core as grass
  137. # temp dir
  138. REMOVE_TMPDIR = True
  139. PROXIES = {}
  140. # check requirements
  141. def check_progs():
  142. """Check if the necessary programs are available"""
  143. for prog in ('svn', 'make', 'gcc'):
  144. if not grass.find_program(prog, '--help'):
  145. grass.fatal(_("'%s' required. Please install '%s' first.")
  146. % (prog, prog))
  147. # expand prefix to class name
  148. def expand_module_class_name(class_letters):
  149. """Convert module class (family) letter or letters to class (family) name
  150. The letter or letters are used in module names, e.g. r.slope.aspect.
  151. The names are used in directories in Addons but also in the source code.
  152. >>> expand_module_class_name('r')
  153. 'raster'
  154. >>> expand_module_class_name('v')
  155. 'vector'
  156. """
  157. name = {'d': 'display',
  158. 'db': 'database',
  159. 'g': 'general',
  160. 'i': 'imagery',
  161. 'm': 'misc',
  162. 'ps': 'postscript',
  163. 'p': 'paint',
  164. 'r': 'raster',
  165. 'r3': 'raster3d',
  166. 's': 'sites',
  167. 'v': 'vector',
  168. 'wx': 'gui/wxpython'
  169. }
  170. return name.get(class_letters, class_letters)
  171. def get_installed_extensions(force=False):
  172. """Get list of installed extensions or toolboxes (if -t is set)"""
  173. if flags['t']:
  174. return get_installed_toolboxes(force)
  175. return get_installed_modules(force)
  176. def list_installed_extensions(toolboxes=False):
  177. """List installed extensions"""
  178. elist = get_installed_extensions()
  179. if elist:
  180. if toolboxes:
  181. grass.message(_("List of installed extensions (toolboxes):"))
  182. else:
  183. grass.message(_("List of installed extensions (modules):"))
  184. sys.stdout.write('\n'.join(elist))
  185. sys.stdout.write('\n')
  186. else:
  187. if toolboxes:
  188. grass.info(_("No extension (toolbox) installed"))
  189. else:
  190. grass.info(_("No extension (module) installed"))
  191. def get_installed_toolboxes(force=False):
  192. """Get list of installed toolboxes
  193. Writes toolboxes file if it does not exist.
  194. Creates a new toolboxes file if it is not possible
  195. to read the current one.
  196. """
  197. xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
  198. if not os.path.exists(xml_file):
  199. write_xml_toolboxes(xml_file)
  200. # read XML file
  201. fo = open(xml_file, 'r')
  202. try:
  203. tree = etree.fromstring(fo.read())
  204. except:
  205. os.remove(xml_file)
  206. write_xml_toolboxes(xml_file)
  207. return []
  208. fo.close()
  209. ret = list()
  210. for tnode in tree.findall('toolbox'):
  211. ret.append(tnode.get('code'))
  212. return ret
  213. def get_installed_modules(force=False):
  214. """Get list of installed modules.
  215. Writes modules file if it does not exist and *force* is set to ``True``.
  216. Creates a new modules file if it is not possible
  217. to read the current one.
  218. """
  219. xml_file = os.path.join(options['prefix'], 'modules.xml')
  220. if not os.path.exists(xml_file):
  221. if force:
  222. write_xml_modules(xml_file)
  223. else:
  224. grass.debug(1, "No addons metadata file available")
  225. return []
  226. # read XML file
  227. fo = open(xml_file, 'r')
  228. try:
  229. tree = etree.fromstring(fo.read())
  230. except:
  231. os.remove(xml_file)
  232. write_xml_modules(xml_file)
  233. return []
  234. fo.close()
  235. ret = list()
  236. for tnode in tree.findall('task'):
  237. ret.append(tnode.get('name').strip())
  238. return ret
  239. # list extensions (read XML file from grass.osgeo.org/addons)
  240. def list_available_extensions(url):
  241. """List available extensions/modules or toolboxes (if -t is given)
  242. For toolboxes it lists also all modules.
  243. """
  244. if flags['t']:
  245. grass.message(_("List of available extensions (toolboxes):"))
  246. tlist = get_available_toolboxes(url)
  247. for toolbox_code, toolbox_data in tlist.iteritems():
  248. if flags['g']:
  249. print('toolbox_name=' + toolbox_data['name'])
  250. print('toolbox_code=' + toolbox_code)
  251. else:
  252. print('%s (%s)' % (toolbox_data['name'], toolbox_code))
  253. if flags['c'] or flags['g']:
  254. list_available_modules(url, toolbox_data['modules'])
  255. else:
  256. if toolbox_data['modules']:
  257. print(os.linesep.join(map(lambda x: '* ' + x,
  258. toolbox_data['modules'])))
  259. else:
  260. grass.message(_("List of available extensions (modules):"))
  261. list_available_modules(url)
  262. def get_available_toolboxes(url):
  263. """Return toolboxes available in the repository"""
  264. tdict = dict()
  265. url = url + "toolboxes.xml"
  266. try:
  267. f = urlopen(url, proxies=PROXIES)
  268. tree = etree.fromstring(f.read())
  269. for tnode in tree.findall('toolbox'):
  270. mlist = list()
  271. clist = list()
  272. tdict[tnode.get('code')] = {'name': tnode.get('name'),
  273. 'correlate': clist,
  274. 'modules': mlist}
  275. for cnode in tnode.findall('correlate'):
  276. clist.append(cnode.get('name'))
  277. for mnode in tnode.findall('task'):
  278. mlist.append(mnode.get('name'))
  279. except HTTPError:
  280. grass.fatal(_("Unable to fetch addons metadata file"))
  281. return tdict
  282. def get_toolbox_modules(url, name):
  283. """Get modules inside a toolbox in toolbox file at given URL
  284. :param url: URL of the directory (file name will be attached)
  285. :param name: toolbox name
  286. """
  287. tlist = list()
  288. url = url + "toolboxes.xml"
  289. try:
  290. f = urlopen(url, proxies=PROXIES)
  291. tree = etree.fromstring(f.read())
  292. for tnode in tree.findall('toolbox'):
  293. if name == tnode.get('code'):
  294. for mnode in tnode.findall('task'):
  295. tlist.append(mnode.get('name'))
  296. break
  297. except HTTPError:
  298. grass.fatal(_("Unable to fetch addons metadata file"))
  299. return tlist
  300. def get_optional_params(mnode):
  301. """Return description and keywords as a tuple
  302. :param mnode: XML node for a module
  303. """
  304. try:
  305. desc = mnode.find('description').text
  306. except AttributeError:
  307. desc = ''
  308. if desc is None:
  309. desc = ''
  310. try:
  311. keyw = mnode.find('keywords').text
  312. except AttributeError:
  313. keyw = ''
  314. if keyw is None:
  315. keyw = ''
  316. return desc, keyw
  317. def list_available_modules(url, mlist=None):
  318. """List modules available in the repository
  319. Tries to use XML metadata file first. Fallbacks to HTML page with a list.
  320. """
  321. url = url + "modules.xml"
  322. grass.debug("url=%s" % url, 1)
  323. try:
  324. f = urlopen(url, proxies=PROXIES)
  325. try:
  326. tree = etree.fromstring(f.read())
  327. except:
  328. grass.warning(_("Unable to parse '%s'. Trying to scan"
  329. " SVN repository (may take some time)...") % url)
  330. list_available_extensions_svn()
  331. return
  332. for mnode in tree.findall('task'):
  333. name = mnode.get('name').strip()
  334. if mlist and name not in mlist:
  335. continue
  336. if flags['c'] or flags['g']:
  337. desc, keyw = get_optional_params(mnode)
  338. if flags['g']:
  339. print('name=' + name)
  340. print('description=' + desc)
  341. print('keywords=' + keyw)
  342. elif flags['c']:
  343. if mlist:
  344. print('*', end='')
  345. print(name + ' - ' + desc)
  346. else:
  347. print(name)
  348. except HTTPError:
  349. list_available_extensions_svn()
  350. # list extensions (scan SVN repo)
  351. def list_available_extensions_svn():
  352. """List available extensions from HTML given by URL
  353. ``<li><a href=...`` is parsed to find module names.
  354. """
  355. grass.message(_('Fetching list of extensions from'
  356. ' GRASS-Addons SVN repository (be patient)...'))
  357. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  358. if flags['c']:
  359. grass.warning(
  360. _("Flag 'c' ignored, addons metadata file not available"))
  361. if flags['g']:
  362. grass.warning(
  363. _("Flag 'g' ignored, addons metadata file not available"))
  364. prefixes = ['d', 'db', 'g', 'i', 'm', 'ps',
  365. 'p', 'r', 'r3', 's', 'v']
  366. for prefix in prefixes:
  367. modclass = expand_module_class_name(prefix)
  368. grass.verbose(_("Checking for '%s' modules...") % modclass)
  369. url = '%s/%s' % (options['svnurl'], modclass)
  370. grass.debug("url = %s" % url, debug=2)
  371. try:
  372. f = urlopen(url, proxies=PROXIES)
  373. except HTTPError:
  374. grass.debug(_("Unable to fetch '%s'") % url, debug=1)
  375. continue
  376. for line in f.readlines():
  377. # list extensions
  378. sline = pattern.search(line)
  379. if not sline:
  380. continue
  381. name = sline.group(2).rstrip('/')
  382. if name.split('.', 1)[0] == prefix:
  383. print(name)
  384. # get_wxgui_extensions()
  385. def get_wxgui_extensions():
  386. """Return list of extensions/addons in wxGUI directory at given URL"""
  387. mlist = list()
  388. grass.debug('Fetching list of wxGUI extensions from '
  389. 'GRASS-Addons SVN repository (be patient)...')
  390. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  391. grass.verbose(_("Checking for '%s' modules...") % 'gui/wxpython')
  392. url = '%s/%s' % (options['svnurl'], 'gui/wxpython')
  393. grass.debug("url = %s" % url, debug=2)
  394. f = urlopen(url, proxies=PROXIES)
  395. if not f:
  396. grass.warning(_("Unable to fetch '%s'") % url)
  397. return
  398. for line in f.readlines():
  399. # list extensions
  400. sline = pattern.search(line)
  401. if not sline:
  402. continue
  403. name = sline.group(2).rstrip('/')
  404. if name not in ('..', 'Makefile'):
  405. mlist.append(name)
  406. return mlist
  407. def cleanup():
  408. """Cleanup after the downloads and copilation"""
  409. if REMOVE_TMPDIR:
  410. try_rmdir(TMPDIR)
  411. else:
  412. grass.message("\n%s\n" % _("Path to the source code:"))
  413. sys.stderr.write('%s\n' % os.path.join(TMPDIR, options['extension']))
  414. def write_xml_modules(name, tree=None):
  415. """Write element tree as a modules matadata file
  416. If the *tree* is not given, an empty file is created.
  417. :param name: file name
  418. :param tree: XML element tree
  419. """
  420. fo = open(name, 'w')
  421. fo.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  422. fo.write('<!DOCTYPE task SYSTEM "grass-addons.dtd">\n')
  423. fo.write('<addons version="%s">\n' % version[0])
  424. libgis_revison = grass.version()['libgis_revision']
  425. if tree is not None:
  426. for tnode in tree.findall('task'):
  427. indent = 4
  428. fo.write('%s<task name="%s">\n' %
  429. (' ' * indent, tnode.get('name')))
  430. indent += 4
  431. fo.write('%s<description>%s</description>\n' %
  432. (' ' * indent, tnode.find('description').text))
  433. fo.write('%s<keywords>%s</keywords>\n' %
  434. (' ' * indent, tnode.find('keywords').text))
  435. bnode = tnode.find('binary')
  436. if bnode is not None:
  437. fo.write('%s<binary>\n' % (' ' * indent))
  438. indent += 4
  439. for fnode in bnode.findall('file'):
  440. fo.write('%s<file>%s</file>\n' %
  441. (' ' * indent, os.path.join(options['prefix'],
  442. fnode.text)))
  443. indent -= 4
  444. fo.write('%s</binary>\n' % (' ' * indent))
  445. fo.write('%s<libgis revision="%s" />\n' %
  446. (' ' * indent, libgis_revison))
  447. indent -= 4
  448. fo.write('%s</task>\n' % (' ' * indent))
  449. fo.write('</addons>\n')
  450. fo.close()
  451. def write_xml_toolboxes(name, tree=None):
  452. """Write element tree as a toolboxes matadata file
  453. If the *tree* is not given, an empty file is created.
  454. :param name: file name
  455. :param tree: XML element tree
  456. """
  457. fo = open(name, 'w')
  458. fo.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  459. fo.write('<!DOCTYPE toolbox SYSTEM "grass-addons.dtd">\n')
  460. fo.write('<addons version="%s">\n' % version[0])
  461. if tree is not None:
  462. for tnode in tree.findall('toolbox'):
  463. indent = 4
  464. fo.write('%s<toolbox name="%s" code="%s">\n' %
  465. (' ' * indent, tnode.get('name'), tnode.get('code')))
  466. indent += 4
  467. for cnode in tnode.findall('correlate'):
  468. fo.write('%s<correlate code="%s" />\n' %
  469. (' ' * indent, tnode.get('code')))
  470. for mnode in tnode.findall('task'):
  471. fo.write('%s<task name="%s" />\n' %
  472. (' ' * indent, mnode.get('name')))
  473. indent -= 4
  474. fo.write('%s</toolbox>\n' % (' ' * indent))
  475. fo.write('</addons>\n')
  476. fo.close()
  477. def install_extension(source, url, xmlurl):
  478. """Install extension (e.g. one module) or a toolbox (list of modules)"""
  479. gisbase = os.getenv('GISBASE')
  480. if not gisbase:
  481. grass.fatal(_('$GISBASE not defined'))
  482. if options['extension'] in get_installed_extensions(force=True):
  483. grass.warning(_("Extension <%s> already installed. Re-installing...") %
  484. options['extension'])
  485. if flags['t']:
  486. grass.message(_("Installing toolbox <%s>...") % options['extension'])
  487. mlist = get_toolbox_modules(xmlurl, options['extension'])
  488. else:
  489. mlist = [options['extension']]
  490. if not mlist:
  491. grass.warning(_("Nothing to install"))
  492. return
  493. ret = 0
  494. for module in mlist:
  495. if sys.platform == "win32":
  496. ret += install_extension_win(module)
  497. else:
  498. ret += install_extension_std_platforms(module,
  499. source=source, url=url)
  500. if len(mlist) > 1:
  501. print('-' * 60)
  502. if flags['d']:
  503. return
  504. if ret != 0:
  505. grass.warning(_('Installation failed, sorry.'
  506. ' Please check above error messages.'))
  507. else:
  508. grass.message(_("Updating addons metadata file..."))
  509. blist = install_extension_xml(xmlurl, mlist)
  510. for module in blist:
  511. update_manual_page(module)
  512. grass.message(_("Installation of <%s> successfully finished") %
  513. options['extension'])
  514. if not os.getenv('GRASS_ADDON_BASE'):
  515. grass.warning(_('This add-on module will not function until'
  516. ' you set the GRASS_ADDON_BASE environment'
  517. ' variable (see "g.manual variables")'))
  518. def get_toolboxes_metadata(url):
  519. """Return metadata for all toolboxes from given URL
  520. :param url: URL of a modules matadata file
  521. :param mlist: list of modules to get metadata for
  522. :returns: tuple where first item is dictionary with module names as keys
  523. and dictionary with dest, keyw, files keys as value, the second item
  524. is list of 'binary' files (installation files)
  525. """
  526. data = dict()
  527. try:
  528. f = urlopen(url, proxies=PROXIES)
  529. tree = etree.fromstring(f.read())
  530. for tnode in tree.findall('toolbox'):
  531. clist = list()
  532. for cnode in tnode.findall('correlate'):
  533. clist.append(cnode.get('code'))
  534. mlist = list()
  535. for mnode in tnode.findall('task'):
  536. mlist.append(mnode.get('name'))
  537. code = tnode.get('code')
  538. data[code] = {
  539. 'name': tnode.get('name'),
  540. 'correlate': clist,
  541. 'modules': mlist,
  542. }
  543. except HTTPError:
  544. grass.error(_("Unable to read addons metadata file "
  545. "from the remote server"))
  546. return data
  547. def install_toolbox_xml(url, name):
  548. """Update local toolboxes metadata file"""
  549. # read metadata from remote server (toolboxes)
  550. url = url + "toolboxes.xml"
  551. data = get_toolboxes_metadata(url)
  552. if not data:
  553. grass.warning(_("No addons metadata available"))
  554. return
  555. if name not in data:
  556. grass.warning(_("No addons metadata available for <%s>") % name)
  557. return
  558. xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
  559. # create an empty file if not exists
  560. if not os.path.exists(xml_file):
  561. write_xml_modules(xml_file)
  562. # read XML file
  563. with open(xml_file, 'r') as xml:
  564. tree = etree.fromstring(xml.read())
  565. # update tree
  566. tnode = None
  567. for node in tree.findall('toolbox'):
  568. if node.get('code') == name:
  569. tnode = node
  570. break
  571. tdata = data[name]
  572. if tnode is not None:
  573. # update existing node
  574. for cnode in tnode.findall('correlate'):
  575. tnode.remove(cnode)
  576. for mnode in tnode.findall('task'):
  577. tnode.remove(mnode)
  578. else:
  579. # create new node for task
  580. tnode = etree.Element(
  581. 'toolbox', attrib={'name': tdata['name'], 'code': name})
  582. tree.append(tnode)
  583. for cname in tdata['correlate']:
  584. cnode = etree.Element('correlate', attrib={'code': cname})
  585. tnode.append(cnode)
  586. for tname in tdata['modules']:
  587. mnode = etree.Element('task', attrib={'name': tname})
  588. tnode.append(mnode)
  589. write_xml_toolboxes(xml_file, tree)
  590. def get_addons_metadata(url, mlist):
  591. """Return metadata for list of modules from given URL
  592. :param url: URL of a modules matadata file
  593. :param mlist: list of modules to get metadata for
  594. :returns: tuple where first item is dictionary with module names as keys
  595. and dictionary with dest, keyw, files keys as value, the second item
  596. is list of 'binary' files (installation files)
  597. """
  598. data = {}
  599. bin_list = []
  600. try:
  601. f = urlopen(url, proxies=PROXIES)
  602. try:
  603. tree = etree.fromstring(f.read())
  604. except:
  605. grass.warning(_("Unable to parse '%s'.") % url)
  606. return data, bin_list
  607. for mnode in tree.findall('task'):
  608. name = mnode.get('name')
  609. if name not in mlist:
  610. continue
  611. file_list = list()
  612. bnode = mnode.find('binary')
  613. windows = sys.platform == 'win32'
  614. if bnode is not None:
  615. for fnode in bnode.findall('file'):
  616. path = fnode.text.split('/')
  617. if path[0] == 'bin':
  618. bin_list.append(path[-1])
  619. if windows:
  620. path[-1] += '.exe'
  621. elif path[0] == 'scripts':
  622. bin_list.append(path[-1])
  623. if windows:
  624. path[-1] += '.py'
  625. file_list.append(os.path.sep.join(path))
  626. desc, keyw = get_optional_params(mnode)
  627. data[name] = {
  628. 'desc': desc,
  629. 'keyw': keyw,
  630. 'files': file_list,
  631. }
  632. except:
  633. grass.error(
  634. _("Unable to read addons metadata file from the remote server"))
  635. return data, bin_list
  636. def install_extension_xml(url, mlist):
  637. """Update XML files with metadata about installed modules and toolbox
  638. Uses the remote/repository XML files for modules to obtain the metadata.
  639. :returns: list of executables (useable for ``update_manual_page()``)
  640. """
  641. if len(mlist) > 1:
  642. # read metadata from remote server (toolboxes)
  643. install_toolbox_xml(url, options['extension'])
  644. # read metadata from remote server (modules)
  645. url = url + "modules.xml"
  646. data, bin_list = get_addons_metadata(url, mlist)
  647. if not data:
  648. grass.warning(_("No addons metadata available."
  649. " Addons metadata file not updated."))
  650. return []
  651. xml_file = os.path.join(options['prefix'], 'modules.xml')
  652. # create an empty file if not exists
  653. if not os.path.exists(xml_file):
  654. write_xml_modules(xml_file)
  655. # read XML file
  656. fo = open(xml_file, 'r')
  657. tree = etree.fromstring(fo.read())
  658. fo.close()
  659. # update tree
  660. for name in mlist:
  661. tnode = None
  662. for node in tree.findall('task'):
  663. if node.get('name') == name:
  664. tnode = node
  665. break
  666. if name not in data:
  667. grass.warning(_("No addons metadata found for <%s>") % name)
  668. continue
  669. ndata = data[name]
  670. if tnode is not None:
  671. # update existing node
  672. dnode = tnode.find('description')
  673. if dnode is not None:
  674. dnode.text = ndata['desc']
  675. knode = tnode.find('keywords')
  676. if knode is not None:
  677. knode.text = ndata['keyw']
  678. bnode = tnode.find('binary')
  679. if bnode is not None:
  680. tnode.remove(bnode)
  681. bnode = etree.Element('binary')
  682. for file_name in ndata['files']:
  683. fnode = etree.Element('file')
  684. fnode.text = file_name
  685. bnode.append(fnode)
  686. tnode.append(bnode)
  687. else:
  688. # create new node for task
  689. tnode = etree.Element('task', attrib={'name': name})
  690. dnode = etree.Element('description')
  691. dnode.text = ndata['desc']
  692. tnode.append(dnode)
  693. knode = etree.Element('keywords')
  694. knode.text = ndata['keyw']
  695. tnode.append(knode)
  696. bnode = etree.Element('binary')
  697. for file_name in ndata['files']:
  698. fnode = etree.Element('file')
  699. fnode.text = file_name
  700. bnode.append(fnode)
  701. tnode.append(bnode)
  702. tree.append(tnode)
  703. write_xml_modules(xml_file, tree)
  704. return bin_list
  705. def install_extension_win(name):
  706. """Install extension on MS Windows"""
  707. # do not use hardcoded url -
  708. # http://wingrass.fsv.cvut.cz/grassXX/addonsX.X.X
  709. grass.message(_("Downloading precompiled GRASS Addons <%s>...") %
  710. options['extension'])
  711. url = "http://wingrass.fsv.cvut.cz/" \
  712. "grass%(major)s%(minor)s/addons/" \
  713. "grass-%(major)s.%(minor)s.%(patch)s/" % \
  714. {'major': version[0], 'minor': version[1], 'patch': version[2]}
  715. grass.debug("url=%s" % url, 1)
  716. try:
  717. zfile = url + name + '.zip'
  718. f = urlopen(zfile, proxies=PROXIES)
  719. # create addons dir if not exists
  720. if not os.path.exists(options['prefix']):
  721. try:
  722. os.mkdir(options['prefix'])
  723. except OSError as error:
  724. grass.fatal(_("Unable to create <{}>. {}")
  725. .format(options['prefix'], error))
  726. # download data
  727. fo = tempfile.TemporaryFile()
  728. fo.write(f.read())
  729. try:
  730. zfobj = zipfile.ZipFile(fo)
  731. except zipfile.BadZipfile as error:
  732. grass.fatal('%s: %s' % (error, zfile))
  733. for name in zfobj.namelist():
  734. if name.endswith('/'):
  735. directory = os.path.join(options['prefix'], name)
  736. if not os.path.exists(directory):
  737. os.mkdir(directory)
  738. else:
  739. outfile = open(os.path.join(options['prefix'], name), 'wb')
  740. outfile.write(zfobj.read(name))
  741. outfile.close()
  742. fo.close()
  743. except HTTPError:
  744. grass.fatal(_("GRASS Addons <%s> not found") % name)
  745. return 0
  746. def download_source_code_svn(url, name, outdev, directory=None):
  747. """Download source code from a Subversion reporsitory
  748. .. note:
  749. Stdout is passed to to *outdev* while stderr is will be just printed.
  750. :param url: URL of the repository
  751. (module class/family and name are attached)
  752. :param name: module name
  753. :param outdev: output devide for the standard output of the svn command
  754. :param directory: directory where the source code will be downloaded
  755. (default is the current directory with name attached)
  756. :returns: full path to the directory with the source code
  757. (useful when you not specify directory, if *directory* is specified
  758. the return value is equal to it)
  759. """
  760. if not directory:
  761. directory = os.path.join(os.getcwd, name)
  762. classchar = name.split('.', 1)[0]
  763. moduleclass = expand_module_class_name(classchar)
  764. url = url + '/' + moduleclass + '/' + name
  765. if grass.call(['svn', 'checkout',
  766. url, directory], stdout=outdev) != 0:
  767. grass.fatal(_("GRASS Addons <%s> not found") % name)
  768. return directory
  769. def move_extracted_files(extract_dir, target_dir, files):
  770. """Fix state of extracted file by moving them to different diretcory
  771. When extracting, it is not clear what will be the root directory
  772. or if there will be one at all. So this function moves the files to
  773. a different directory in the way that if there was one direcory extracted,
  774. the contained files are moved.
  775. """
  776. gscript.debug("move_extracted_files({})".format(locals()))
  777. if len(files) == 1:
  778. shutil.copytree(os.path.join(extract_dir, files[0]), target_dir)
  779. else:
  780. for file_name in files:
  781. actual_file = os.path.join(extract_dir, file_name)
  782. if os.path.isdir(actual_file):
  783. shutil.copytree(actual_file,
  784. os.path.join(target_dir, file_name))
  785. else:
  786. shutil.copy(actual_file, target_dir)
  787. # Original copyright and license of the original version of the CRLF function
  788. # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010
  789. # Python Software Foundation; All Rights Reserved
  790. # Python Software Foundation License Version 2
  791. # http://svn.python.org/projects/python/trunk/Tools/scripts/crlf.py
  792. def fix_newlines(directory):
  793. """Replace CRLF with LF in all files in the directory
  794. Binary files are ignored. Recurses into subdirectories.
  795. """
  796. for root, unused, files in os.walk(directory):
  797. for name in files:
  798. filename = os.path.join(root, name)
  799. data = open(filename, 'rb').read()
  800. if '\0' in data:
  801. continue # ignore binary files
  802. # we don't expect there would be CRLF file by purpose
  803. # if we want to allow CRLF files we would have to whitelite .py etc
  804. newdata = data.replace('\r\n', '\n')
  805. if newdata != data:
  806. f = open(filename, 'wb')
  807. f.write(newdata)
  808. f.close()
  809. def extract_zip(name, directory, tmpdir):
  810. """Extract a ZIP file into a directory"""
  811. try:
  812. zip_file = zipfile.ZipFile(name, mode='r')
  813. file_list = zip_file.namelist()
  814. # we suppose we can write to parent of the given dir (supposing a tmp dir)
  815. extract_dir = os.path.join(tmpdir, 'extract_dir')
  816. os.mkdir(extract_dir)
  817. for subfile in file_list:
  818. # this should be safe in Python 2.7.4
  819. zip_file.extract(subfile, extract_dir)
  820. files = os.listdir(extract_dir)
  821. move_extracted_files(extract_dir=extract_dir,
  822. target_dir=directory, files=files)
  823. except zipfile.BadZipfile as error:
  824. gscript.fatal(_("ZIP file is unreadable: {}").format(error))
  825. # TODO: solve the other related formats
  826. def extract_tar(name, directory, tmpdir):
  827. """Extract a TAR or a similar file into a directory"""
  828. try:
  829. import tarfile # we don't need it anywhere else
  830. tar = tarfile.open(name)
  831. extract_dir = os.path.join(tmpdir, 'extract_dir')
  832. os.mkdir(extract_dir)
  833. tar.extractall(path=extract_dir)
  834. files = os.listdir(extract_dir)
  835. move_extracted_files(extract_dir=extract_dir,
  836. target_dir=directory, files=files)
  837. except tarfile.TarError as error:
  838. gscript.fatal(_("Archive file is unreadable: {}").format(error))
  839. extract_tar.supported_formats = ['tar.gz', 'gz', 'bz2', 'tar', 'gzip','targz']
  840. def download_source_code(source, url, name, outdev,
  841. directory=None, tmpdir=None):
  842. """Get source code to a local directory for compilation"""
  843. if source == 'svn':
  844. download_source_code_svn(url, name, outdev, directory)
  845. elif source == 'remote_zip':
  846. # we expect that the module.zip file is not by chance in the archive
  847. zip_name = os.path.join(tmpdir, 'extension.zip')
  848. urlretrieve(url, zip_name)
  849. extract_zip(name=zip_name, directory=directory, tmpdir=tmpdir)
  850. fix_newlines(directory)
  851. elif source.startswith('remote_') and \
  852. source.split('_')[1] in extract_tar.supported_formats:
  853. # we expect that the module.tar.gz file is not by chance in the archive
  854. archive_name = os.path.join(tmpdir,
  855. 'extension.' + source.split('_')[1])
  856. urlretrieve(url, archive_name)
  857. extract_tar(name=archive_name, directory=directory, tmpdir=tmpdir)
  858. fix_newlines(directory)
  859. elif source == 'zip':
  860. extract_zip(name=url, directory=directory, tmpdir=tmpdir)
  861. fix_newlines(directory)
  862. elif source in extract_tar.supported_formats:
  863. extract_tar(name=url, directory=directory, tmpdir=tmpdir)
  864. fix_newlines(directory)
  865. elif source == 'dir':
  866. shutil.copytree(url, directory)
  867. fix_newlines(directory)
  868. else:
  869. # probably programmer error
  870. grass.fatal(_("Unknown extension (addon) source type '{}'."
  871. " Please report this to the grass-user mailing list.")
  872. .format(source))
  873. def install_extension_std_platforms(name, source, url):
  874. """Install extension on standard plaforms"""
  875. gisbase = os.getenv('GISBASE')
  876. grass.message(_("Fetching <%s> from"
  877. " GRASS-Addons SVN repository (be patient)...") % name)
  878. # to hide non-error messages from subprocesses
  879. if grass.verbosity() <= 2:
  880. outdev = open(os.devnull, 'w')
  881. else:
  882. outdev = sys.stdout
  883. os.chdir(TMPDIR) # this is just to not leave something behind
  884. srcdir = os.path.join(TMPDIR, name)
  885. download_source_code(source=source, url=url, name=name,
  886. outdev=outdev, directory=srcdir, tmpdir=TMPDIR)
  887. os.chdir(srcdir)
  888. dirs = {'bin': os.path.join(TMPDIR, name, 'bin'),
  889. 'docs': os.path.join(TMPDIR, name, 'docs'),
  890. 'html': os.path.join(TMPDIR, name, 'docs', 'html'),
  891. 'rest': os.path.join(TMPDIR, name, 'docs', 'rest'),
  892. 'man': os.path.join(TMPDIR, name, 'docs', 'man'),
  893. 'script': os.path.join(TMPDIR, name, 'scripts'),
  894. # TODO: handle locales also for addons
  895. # 'string' : os.path.join(TMPDIR, name, 'locale'),
  896. 'string': os.path.join(TMPDIR, name),
  897. 'etc': os.path.join(TMPDIR, name, 'etc'),
  898. }
  899. make_cmd = ['make',
  900. 'MODULE_TOPDIR=%s' % gisbase.replace(' ', r'\ '),
  901. 'RUN_GISRC=%s' % os.environ['GISRC'],
  902. 'BIN=%s' % dirs['bin'],
  903. 'HTMLDIR=%s' % dirs['html'],
  904. 'RESTDIR=%s' % dirs['rest'],
  905. 'MANBASEDIR=%s' % dirs['man'],
  906. 'SCRIPTDIR=%s' % dirs['script'],
  907. 'STRINGDIR=%s' % dirs['string'],
  908. 'ETC=%s' % os.path.join(dirs['etc'])
  909. ]
  910. install_cmd = ['make',
  911. 'MODULE_TOPDIR=%s' % gisbase,
  912. 'ARCH_DISTDIR=%s' % os.path.join(TMPDIR, name),
  913. 'INST_DIR=%s' % options['prefix'],
  914. 'install'
  915. ]
  916. if flags['d']:
  917. grass.message("\n%s\n" % _("To compile run:"))
  918. sys.stderr.write(' '.join(make_cmd) + '\n')
  919. grass.message("\n%s\n" % _("To install run:"))
  920. sys.stderr.write(' '.join(install_cmd) + '\n')
  921. return 0
  922. os.chdir(os.path.join(TMPDIR, name))
  923. grass.message(_("Compiling..."))
  924. if not os.path.exists(os.path.join(gisbase, 'include',
  925. 'Make', 'Module.make')):
  926. grass.fatal(_("Please install GRASS development package"))
  927. if 0 != grass.call(make_cmd,
  928. stdout=outdev):
  929. grass.fatal(_('Compilation failed, sorry.'
  930. ' Please check above error messages.'))
  931. if flags['i']:
  932. return 0
  933. grass.message(_("Installing..."))
  934. return grass.call(install_cmd,
  935. stdout=outdev)
  936. def remove_extension(force=False):
  937. """Remove existing extension (module or toolbox if -t is given)"""
  938. if flags['t']:
  939. mlist = get_toolbox_modules(options['prefix'], options['extension'])
  940. else:
  941. mlist = [options['extension']]
  942. if force:
  943. grass.verbose(_("List of removed files:"))
  944. else:
  945. grass.info(_("Files to be removed:"))
  946. remove_modules(mlist, force)
  947. if force:
  948. grass.message(_("Updating addons metadata file..."))
  949. remove_extension_xml(mlist)
  950. grass.message(_("Extension <%s> successfully uninstalled.") %
  951. options['extension'])
  952. else:
  953. grass.warning(_("Extension <%s> not removed. "
  954. "Re-run '%s' with '-f' flag to force removal")
  955. % (options['extension'], 'g.extension'))
  956. # remove existing extension(s) (reading XML file)
  957. def remove_modules(mlist, force=False):
  958. """Remove extensions/modules specified in a list
  959. Collects the file names from the file with metadata and fallbacks
  960. to standard layout of files on prefix path on error.
  961. """
  962. # try to read XML metadata file first
  963. xml_file = os.path.join(options['prefix'], 'modules.xml')
  964. installed = get_installed_modules()
  965. if os.path.exists(xml_file):
  966. f = open(xml_file, 'r')
  967. tree = etree.fromstring(f.read())
  968. f.close()
  969. else:
  970. tree = None
  971. for name in mlist:
  972. if name not in installed:
  973. # try even if module does not seem to be available,
  974. # as the user may be trying to get rid of left over cruft
  975. grass.warning(_("Extension <%s> not found") % name)
  976. if tree is not None:
  977. flist = []
  978. for task in tree.findall('task'):
  979. if name == task.get('name') and \
  980. task.find('binary') is not None:
  981. for file_node in task.find('binary').findall('file'):
  982. flist.append(file_node.text)
  983. break
  984. if flist:
  985. removed = False
  986. err = list()
  987. for fpath in flist:
  988. try:
  989. if force:
  990. grass.verbose(fpath)
  991. removed = True
  992. os.remove(fpath)
  993. else:
  994. print(fpath)
  995. except OSError:
  996. err.append((_("Unable to remove file '%s'") % fpath))
  997. if force and not removed:
  998. grass.fatal(_("Extension <%s> not found") % name)
  999. if err:
  1000. for error_line in err:
  1001. grass.error(error_line)
  1002. else:
  1003. remove_extension_std(name, force)
  1004. else:
  1005. remove_extension_std(name, force)
  1006. def remove_extension_std(name, force=False):
  1007. """Remove extension/module expecting the standard layout"""
  1008. for fpath in [os.path.join(options['prefix'], 'bin', name),
  1009. os.path.join(options['prefix'], 'scripts', name),
  1010. os.path.join(
  1011. options['prefix'], 'docs', 'html', name + '.html'),
  1012. os.path.join(
  1013. options['prefix'], 'docs', 'rest', name + '.txt'),
  1014. os.path.join(options['prefix'], 'docs', 'man', 'man1',
  1015. name + '.1')]:
  1016. if os.path.isfile(fpath):
  1017. if force:
  1018. grass.verbose(fpath)
  1019. os.remove(fpath)
  1020. else:
  1021. print(fpath)
  1022. def remove_from_toolbox_xml(name):
  1023. """Update local meta-file when removing existing toolbox"""
  1024. xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
  1025. if not os.path.exists(xml_file):
  1026. return
  1027. # read XML file
  1028. fo = open(xml_file, 'r')
  1029. tree = etree.fromstring(fo.read())
  1030. fo.close()
  1031. for node in tree.findall('toolbox'):
  1032. if node.get('code') != name:
  1033. continue
  1034. tree.remove(node)
  1035. write_xml_toolboxes(xml_file, tree)
  1036. def remove_extension_xml(modules):
  1037. """Update local meta-file when removing existing extension"""
  1038. if len(modules) > 1:
  1039. # update also toolboxes metadata
  1040. remove_from_toolbox_xml(options['extension'])
  1041. xml_file = os.path.join(options['prefix'], 'modules.xml')
  1042. if not os.path.exists(xml_file):
  1043. return
  1044. # read XML file
  1045. fo = open(xml_file, 'r')
  1046. tree = etree.fromstring(fo.read())
  1047. fo.close()
  1048. for name in modules:
  1049. for node in tree.findall('task'):
  1050. if node.get('name') != name:
  1051. continue
  1052. tree.remove(node)
  1053. write_xml_modules(xml_file, tree)
  1054. # check links in CSS
  1055. def check_style_files(fil):
  1056. """Ensures that a specified HTML documentation support file exists
  1057. If the file, e.g. a CSS file does not exist, the file is copied from
  1058. the distribution.
  1059. """
  1060. dist_file = os.path.join(os.getenv('GISBASE'), 'docs', 'html', fil)
  1061. addons_file = os.path.join(options['prefix'], 'docs', 'html', fil)
  1062. if os.path.isfile(addons_file):
  1063. return
  1064. try:
  1065. shutil.copyfile(dist_file, addons_file)
  1066. except OSError as error:
  1067. grass.fatal(_("Unable to create '%s': %s") % (addons_file, error))
  1068. def create_dir(path):
  1069. """Creates the specified directory (with all dirs in between)
  1070. NOOP for existing directory.
  1071. """
  1072. if os.path.isdir(path):
  1073. return
  1074. try:
  1075. os.makedirs(path)
  1076. except OSError as error:
  1077. grass.fatal(_("Unable to create '%s': %s") % (path, error))
  1078. grass.debug("'%s' created" % path)
  1079. def check_dirs():
  1080. """Ensure that the necessary directories in prefix path exist"""
  1081. create_dir(os.path.join(options['prefix'], 'bin'))
  1082. create_dir(os.path.join(options['prefix'], 'docs', 'html'))
  1083. create_dir(os.path.join(options['prefix'], 'docs', 'rest'))
  1084. check_style_files('grass_logo.png')
  1085. check_style_files('grassdocs.css')
  1086. create_dir(os.path.join(options['prefix'], 'etc'))
  1087. create_dir(os.path.join(options['prefix'], 'docs', 'man', 'man1'))
  1088. create_dir(os.path.join(options['prefix'], 'scripts'))
  1089. # fix file URI in manual page
  1090. def update_manual_page(module):
  1091. """Fix manual page for addons which are at different directory then rest"""
  1092. if module.split('.', 1)[0] == 'wx':
  1093. return # skip for GUI modules
  1094. grass.verbose(_("Manual page for <%s> updated") % module)
  1095. # read original html file
  1096. htmlfile = os.path.join(
  1097. options['prefix'], 'docs', 'html', module + '.html')
  1098. try:
  1099. f = open(htmlfile)
  1100. shtml = f.read()
  1101. except IOError as error:
  1102. grass.fatal(_("Unable to read manual page: %s") % error)
  1103. else:
  1104. f.close()
  1105. pos = []
  1106. # fix logo URL
  1107. pattern = r'''<a href="([^"]+)"><img src="grass_logo.png"'''
  1108. for match in re.finditer(pattern, shtml):
  1109. pos.append(match.start(1))
  1110. # find URIs
  1111. pattern = r'''<a href="([^"]+)">([^>]+)</a>'''
  1112. addons = get_installed_extensions(force=True)
  1113. for match in re.finditer(pattern, shtml):
  1114. if match.group(1)[:4] == 'http':
  1115. continue
  1116. if match.group(1).replace('.html', '') in addons:
  1117. continue
  1118. pos.append(match.start(1))
  1119. if not pos:
  1120. return # no match
  1121. # replace file URIs
  1122. prefix = 'file://' + '/'.join([os.getenv('GISBASE'), 'docs', 'html'])
  1123. ohtml = shtml[:pos[0]]
  1124. for i in range(1, len(pos)):
  1125. ohtml += prefix + '/' + shtml[pos[i - 1]:pos[i]]
  1126. ohtml += prefix + '/' + shtml[pos[-1]:]
  1127. # write updated html file
  1128. try:
  1129. f = open(htmlfile, 'w')
  1130. f.write(ohtml)
  1131. except IOError as error:
  1132. grass.fatal(_("Unable for write manual page: %s") % error)
  1133. else:
  1134. f.close()
  1135. def resolve_install_prefix(path, to_system):
  1136. """Determine and check the path for installation"""
  1137. if to_system:
  1138. path = os.environ['GISBASE']
  1139. if path == '$GRASS_ADDON_BASE':
  1140. if not os.getenv('GRASS_ADDON_BASE'):
  1141. grass.warning(_("GRASS_ADDON_BASE is not defined, "
  1142. "installing to ~/.grass%s/addons") % version[0])
  1143. path = os.path.join(
  1144. os.environ['HOME'], '.grass%s' % version[0], 'addons')
  1145. else:
  1146. path = os.environ['GRASS_ADDON_BASE']
  1147. if os.path.exists(path) and \
  1148. not os.access(path, os.W_OK):
  1149. grass.fatal(_("You don't have permission to install extension to <{}>."
  1150. " Try to run {} with administrator rights"
  1151. " (su or sudo).")
  1152. .format(path, 'g.extension'))
  1153. # ensure dir sep at the end for cases where path is used as URL and pasted
  1154. # together with file names
  1155. if not path.endswith(os.path.sep):
  1156. path = path + os.path.sep
  1157. return path
  1158. def resolve_xmlurl_prefix(url):
  1159. """Determine and check the URL where the XML metadata files are stored
  1160. It ensures that there is a single slash at the end of URL, so we can attach
  1161. file name easily:
  1162. >>> resolve_xmlurl_prefix('http://grass.osgeo.org/addons')
  1163. 'http://grass.osgeo.org/addons/'
  1164. >>> resolve_xmlurl_prefix('http://grass.osgeo.org/addons/')
  1165. 'http://grass.osgeo.org/addons/'
  1166. """
  1167. if 'svn.osgeo.org/grass/grass-addons/grass7' in url:
  1168. # use pregenerated modules XML file
  1169. url = 'http://grass.osgeo.org/addons/grass%s' % version[0]
  1170. # else try to get modules XMl from SVN repository (provided URL)
  1171. # the exact action depends on subsequent code (somewhere)
  1172. if not url.endswith('/'):
  1173. url = url + '/'
  1174. return url
  1175. KNOWN_HOST_SERVICES_INFO = {
  1176. 'OSGeo Trac': {
  1177. 'domain': 'trac.osgeo.org',
  1178. 'ignored_suffixes': ['format=zip'],
  1179. 'possible_starts': ['', 'https://', 'http://'],
  1180. 'url_start': 'https://',
  1181. 'url_end': '?format=zip',
  1182. },
  1183. 'GitHub': {
  1184. 'domain': 'github.com',
  1185. 'ignored_suffixes': ['.zip', '.tar.gz'],
  1186. 'possible_starts': ['', 'https://', 'http://'],
  1187. 'url_start': 'https://',
  1188. 'url_end': '/archive/master.zip',
  1189. },
  1190. 'GitLab': {
  1191. 'domain': 'gitlab.com',
  1192. 'ignored_suffixes': ['.zip', '.tar.gz', '.tar.bz2', '.tar'],
  1193. 'possible_starts': ['', 'https://', 'http://'],
  1194. 'url_start': 'https://',
  1195. 'url_end': '/repository/archive.zip',
  1196. },
  1197. 'Bitbucket': {
  1198. 'domain': 'bitbucket.org',
  1199. 'ignored_suffixes': ['.zip', '.tar.gz', '.gz', '.bz2'],
  1200. 'possible_starts': ['', 'https://', 'http://'],
  1201. 'url_start': 'https://',
  1202. 'url_end': '/get/default.zip',
  1203. },
  1204. }
  1205. # TODO: support ZIP URLs which don't end with zip
  1206. # https://gitlab.com/user/reponame/repository/archive.zip?ref=b%C3%A9po
  1207. def resolve_known_host_service(url):
  1208. match = None
  1209. actual_start = None
  1210. for key, value in KNOWN_HOST_SERVICES_INFO.iteritems():
  1211. for start in value['possible_starts']:
  1212. if url.startswith(start + value['domain']):
  1213. match = value
  1214. actual_start = start
  1215. gscript.verbose(_("Indentified {} as known hosting service")
  1216. .format(key))
  1217. for suffix in value['ignored_suffixes']:
  1218. if url.endswith(suffix):
  1219. gscript.verbose(
  1220. _("Not using {service} as known hosting service"
  1221. " because the URL ends with '{suffix}'")
  1222. .format(service=key, suffix=suffix))
  1223. return None
  1224. if match:
  1225. if not actual_start:
  1226. actual_start = match['url_start']
  1227. else:
  1228. actual_start = ''
  1229. url = '{prefix}{base}{suffix}'.format(prefix=actual_start,
  1230. base=url.rstrip('/'),
  1231. suffix=match['url_end'])
  1232. gscript.verbose(_("Will use the following URL for download: {}")
  1233. .format(url))
  1234. return 'remote_zip', url
  1235. else:
  1236. return None
  1237. def resolve_source_code(url):
  1238. """Return type and URL or path of the source code
  1239. Local paths are not presented as URLs to be usable in standard functions.
  1240. Path is identified as local path if the directory of file exists which
  1241. has the unfortunate consequence that the not existing files are evaluated
  1242. as remote URLs. When path is not evaluated, Subversion is assumed for
  1243. backwards compatibility. When GitHub repository is specified, ZIP file
  1244. link is returned. The ZIP is for master branch, not the default one because
  1245. GitHub does not provide the deafult branch in the URL (July 2015).
  1246. :returns: tuple with type of source and full URL or path
  1247. Subversion:
  1248. >>> resolve_source_code('http://svn.osgeo.org/grass/grass-addons/grass7')
  1249. ('svn', 'http://svn.osgeo.org/grass/grass-addons/grass7')
  1250. ZIP files online:
  1251. >>> resolve_source_code('https://trac.osgeo.org/.../r.modis?format=zip')
  1252. ('remote_zip', 'https://trac.osgeo.org/.../r.modis?format=zip')
  1253. Local directories and ZIP files:
  1254. >>> resolve_source_code(os.path.expanduser("~")) # doctest: +ELLIPSIS
  1255. ('dir', '...')
  1256. >>> resolve_source_code('/local/directory/downloaded.zip') # doctest: +SKIP
  1257. ('zip', '/local/directory/downloaded.zip')
  1258. OSGeo Trac:
  1259. >>> resolve_source_code('trac.osgeo.org/.../r.agent.aco')
  1260. ('remote_zip', 'https://trac.osgeo.org/.../r.agent.aco?format=zip')
  1261. >>> resolve_source_code('https://trac.osgeo.org/.../r.agent.aco')
  1262. ('remote_zip', 'https://trac.osgeo.org/.../r.agent.aco?format=zip')
  1263. GitHub:
  1264. >>> resolve_source_code('github.com/user/g.example')
  1265. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1266. >>> resolve_source_code('github.com/user/g.example/')
  1267. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1268. >>> resolve_source_code('https://github.com/user/g.example')
  1269. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1270. >>> resolve_source_code('https://github.com/user/g.example/')
  1271. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1272. GitLab:
  1273. >>> resolve_source_code('gitlab.com/JoeUser/GrassModule')
  1274. ('remote_zip', 'https://gitlab.com/JoeUser/GrassModule/repository/archive.zip')
  1275. >>> resolve_source_code('https://gitlab.com/JoeUser/GrassModule')
  1276. ('remote_zip', 'https://gitlab.com/JoeUser/GrassModule/repository/archive.zip')
  1277. Bitbucket:
  1278. >>> resolve_source_code('bitbucket.org/joe-user/grass-module')
  1279. ('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
  1280. >>> resolve_source_code('https://bitbucket.org/joe-user/grass-module')
  1281. ('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
  1282. """
  1283. if os.path.isdir(url):
  1284. return 'dir', os.path.abspath(url)
  1285. elif os.path.exists(url):
  1286. if url.endswith('.zip'):
  1287. return 'zip', os.path.abspath(url)
  1288. for suffix in extract_tar.supported_formats:
  1289. if url.endswith('.' + suffix):
  1290. return suffix, os.path.abspath(url)
  1291. else:
  1292. result = resolve_known_host_service(url)
  1293. if result:
  1294. return result
  1295. # we allow URL to end with =zip or ?zip and not only .zip
  1296. # unfortunately format=zip&version=89612 would require something else
  1297. # special option to force the source type would solve it
  1298. if url.endswith('zip'):
  1299. return 'remote_zip', url
  1300. for suffix in extract_tar.supported_formats:
  1301. if url.endswith(suffix):
  1302. return 'remote_' + suffix, url
  1303. # fallback to the classic behavior
  1304. return 'svn', url
  1305. def main():
  1306. # check dependecies
  1307. if sys.platform != "win32":
  1308. check_progs()
  1309. # manage proxies
  1310. global PROXIES
  1311. if options['proxy']:
  1312. PROXIES = {}
  1313. for ptype, purl in (p.split('=') for p in options['proxy'].split(',')):
  1314. PROXIES[ptype] = purl
  1315. # define path
  1316. options['prefix'] = resolve_install_prefix(path=options['prefix'],
  1317. to_system=flags['s'])
  1318. # list available extensions
  1319. if flags['l'] or flags['c'] or flags['g']:
  1320. xmlurl = resolve_xmlurl_prefix(options['svnurl'])
  1321. list_available_extensions(xmlurl)
  1322. return 0
  1323. elif flags['a']:
  1324. list_installed_extensions(toolboxes=flags['t'])
  1325. return 0
  1326. if flags['d']:
  1327. if options['operation'] != 'add':
  1328. grass.warning(_("Flag 'd' is relevant only to"
  1329. " 'operation=add'. Ignoring this flag."))
  1330. else:
  1331. global REMOVE_TMPDIR
  1332. REMOVE_TMPDIR = False
  1333. if options['operation'] == 'add':
  1334. check_dirs()
  1335. source, url = resolve_source_code(options['svnurl'])
  1336. xmlurl = resolve_xmlurl_prefix(options['svnurl'])
  1337. install_extension(source=source, url=url, xmlurl=xmlurl)
  1338. else: # remove
  1339. remove_extension(force=flags['f'])
  1340. return 0
  1341. if __name__ == "__main__":
  1342. if len(sys.argv) == 2 and sys.argv[1] == '--doctest':
  1343. import doctest
  1344. _ = str # doctest gettext workaround
  1345. sys.exit(doctest.testmod().failed)
  1346. options, flags = grass.parser()
  1347. global TMPDIR
  1348. TMPDIR = tempfile.mkdtemp()
  1349. atexit.register(cleanup)
  1350. version = grass.version()['version'].split('.')
  1351. sys.exit(main())