g.extension.py 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130
  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. import os
  118. import sys
  119. import re
  120. import atexit
  121. import shutil
  122. import zipfile
  123. import tempfile
  124. from urllib2 import HTTPError
  125. from urllib import urlopen
  126. try:
  127. import xml.etree.ElementTree as etree
  128. except ImportError:
  129. import elementtree.ElementTree as etree # Python <= 2.4
  130. from grass.script.utils import try_rmdir
  131. from grass.script import core as grass
  132. # temp dir
  133. REMOVE_TMPDIR = True
  134. PROXIES = {}
  135. # check requirements
  136. def check_progs():
  137. for prog in ('svn', 'make', 'gcc'):
  138. if not grass.find_program(prog, '--help'):
  139. grass.fatal(_("'%s' required. Please install '%s' first.") % (prog, prog))
  140. # expand prefix to class name
  141. def expand_module_class_name(c):
  142. name = { 'd' : 'display',
  143. 'db' : 'database',
  144. 'g' : 'general',
  145. 'i' : 'imagery',
  146. 'm' : 'misc',
  147. 'ps' : 'postscript',
  148. 'p' : 'paint',
  149. 'r' : 'raster',
  150. 'r3' : 'raster3d',
  151. 's' : 'sites',
  152. 'v' : 'vector',
  153. 'wx' : 'gui/wxpython'
  154. }
  155. return name.get(c, c)
  156. # list installed extensions
  157. def get_installed_extensions(force = False):
  158. if flags['t']:
  159. return get_installed_toolboxes(force)
  160. return get_installed_modules(force)
  161. def get_installed_toolboxes(force = False):
  162. fXML = os.path.join(options['prefix'], 'toolboxes.xml')
  163. if not os.path.exists(fXML):
  164. write_xml_toolboxes(fXML)
  165. # read XML file
  166. fo = open(fXML, 'r')
  167. try:
  168. tree = etree.fromstring(fo.read())
  169. except:
  170. os.remove(fXML)
  171. write_xml_toolboxes(fXML)
  172. return []
  173. fo.close()
  174. ret = list()
  175. for tnode in tree.findall('toolbox'):
  176. ret.append(tnode.get('code'))
  177. return ret
  178. def get_installed_modules(force = False):
  179. fXML = os.path.join(options['prefix'], 'modules.xml')
  180. if not os.path.exists(fXML):
  181. if force:
  182. write_xml_modules(fXML)
  183. else:
  184. grass.debug(1, "No addons metadata file available")
  185. return []
  186. # read XML file
  187. fo = open(fXML, 'r')
  188. try:
  189. tree = etree.fromstring(fo.read())
  190. except:
  191. os.remove(fXML)
  192. write_xml_modules(fXML)
  193. return []
  194. fo.close()
  195. ret = list()
  196. for tnode in tree.findall('task'):
  197. ret.append(tnode.get('name').strip())
  198. return ret
  199. # list extensions (read XML file from grass.osgeo.org/addons)
  200. def list_available_extensions(url):
  201. if flags['t']:
  202. grass.message(_("List of available extensions (toolboxes):"))
  203. tlist = list_available_toolboxes(url)
  204. for toolbox_code, toolbox_data in tlist.iteritems():
  205. if flags['g']:
  206. print 'toolbox_name=' + toolbox_data['name']
  207. print 'toolbox_code=' + toolbox_code
  208. else:
  209. print '%s (%s)' % (toolbox_data['name'], toolbox_code)
  210. if flags['c'] or flags['g']:
  211. list_available_modules(url, toolbox_data['modules'])
  212. else:
  213. if toolbox_data['modules']:
  214. print os.linesep.join(map(lambda x: '* ' + x, toolbox_data['modules']))
  215. else:
  216. grass.message(_("List of available extensions (modules):"))
  217. list_available_modules(url)
  218. def list_available_toolboxes(url):
  219. tdict = dict()
  220. url = url + "toolboxes.xml"
  221. try:
  222. f = urlopen(url, proxies=PROXIES)
  223. tree = etree.fromstring(f.read())
  224. for tnode in tree.findall('toolbox'):
  225. mlist = list()
  226. clist = list()
  227. tdict[tnode.get('code')] = { 'name' : tnode.get('name'),
  228. 'correlate' : clist,
  229. 'modules' : mlist }
  230. for cnode in tnode.findall('correlate'):
  231. clist.append(cnode.get('name'))
  232. for mnode in tnode.findall('task'):
  233. mlist.append(mnode.get('name'))
  234. except HTTPError:
  235. grass.fatal(_("Unable to fetch addons metadata file"))
  236. return tdict
  237. def get_toolbox_modules(url, name):
  238. tlist = list()
  239. url = url + "toolboxes.xml"
  240. try:
  241. f = urlopen(url, proxies=PROXIES)
  242. tree = etree.fromstring(f.read())
  243. for tnode in tree.findall('toolbox'):
  244. if name == tnode.get('code'):
  245. for mnode in tnode.findall('task'):
  246. tlist.append(mnode.get('name'))
  247. break
  248. except HTTPError:
  249. grass.fatal(_("Unable to fetch addons metadata file"))
  250. return tlist
  251. def get_optional_params(mnode):
  252. try:
  253. desc = mnode.find('description').text
  254. except AttributeError:
  255. desc = ''
  256. if desc is None:
  257. desc = ''
  258. try:
  259. keyw = mnode.find('keywords').text
  260. except AttributeError:
  261. keyw = ''
  262. if keyw is None:
  263. keyw = ''
  264. return desc, keyw
  265. def list_available_modules(url, mlist = None):
  266. # try to download XML metadata file first
  267. url = url + "modules.xml"
  268. grass.debug("url=%s" % url, 1)
  269. try:
  270. f = urlopen(url, proxies=PROXIES)
  271. try:
  272. tree = etree.fromstring(f.read())
  273. except:
  274. grass.warning(_("Unable to parse '%s'. Trying to scan SVN repository (may take some time)...") % url)
  275. list_available_extensions_svn()
  276. return
  277. for mnode in tree.findall('task'):
  278. name = mnode.get('name').strip()
  279. if mlist and name not in mlist:
  280. continue
  281. if flags['c'] or flags['g']:
  282. desc, keyw = get_optional_params(mnode)
  283. if flags['g']:
  284. print 'name=' + name
  285. print 'description=' + desc
  286. print 'keywords=' + keyw
  287. elif flags['c']:
  288. if mlist:
  289. print '*',
  290. print name + ' - ' + desc
  291. else:
  292. print name
  293. except HTTPError:
  294. list_available_extensions_svn()
  295. # list extensions (scan SVN repo)
  296. def list_available_extensions_svn():
  297. grass.message(_('Fetching list of extensions from GRASS-Addons SVN repository (be patient)...'))
  298. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  299. if flags['c']:
  300. grass.warning(_("Flag 'c' ignored, addons metadata file not available"))
  301. if flags['g']:
  302. grass.warning(_("Flag 'g' ignored, addons metadata file not available"))
  303. prefix = ['d', 'db', 'g', 'i', 'm', 'ps',
  304. 'p', 'r', 'r3', 's', 'v']
  305. for d in prefix:
  306. modclass = expand_module_class_name(d)
  307. grass.verbose(_("Checking for '%s' modules...") % modclass)
  308. url = '%s/%s' % (options['svnurl'], modclass)
  309. grass.debug("url = %s" % url, debug = 2)
  310. try:
  311. f = urlopen(url, proxies=PROXIES)
  312. except HTTPError:
  313. grass.debug(_("Unable to fetch '%s'") % url, debug = 1)
  314. continue
  315. for line in f.readlines():
  316. # list extensions
  317. sline = pattern.search(line)
  318. if not sline:
  319. continue
  320. name = sline.group(2).rstrip('/')
  321. if name.split('.', 1)[0] == d:
  322. print name
  323. # get_wxgui_extensions()
  324. # list wxGUI extensions
  325. def get_wxgui_extensions():
  326. mlist = list()
  327. grass.debug('Fetching list of wxGUI extensions from GRASS-Addons SVN repository (be patient)...')
  328. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  329. grass.verbose(_("Checking for '%s' modules...") % 'gui/wxpython')
  330. url = '%s/%s' % (options['svnurl'], 'gui/wxpython')
  331. grass.debug("url = %s" % url, debug = 2)
  332. f = urlopen(url, proxies=PROXIES)
  333. if not f:
  334. grass.warning(_("Unable to fetch '%s'") % url)
  335. return
  336. for line in f.readlines():
  337. # list extensions
  338. sline = pattern.search(line)
  339. if not sline:
  340. continue
  341. name = sline.group(2).rstrip('/')
  342. if name not in ('..', 'Makefile'):
  343. mlist.append(name)
  344. return mlist
  345. def cleanup():
  346. if REMOVE_TMPDIR:
  347. try_rmdir(TMPDIR)
  348. else:
  349. grass.message("\n%s\n" % _("Path to the source code:"))
  350. sys.stderr.write('%s\n' % os.path.join(TMPDIR, options['extension']))
  351. # write out meta-file
  352. def write_xml_modules(name, tree = None):
  353. fo = open(name, 'w')
  354. fo.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  355. fo.write('<!DOCTYPE task SYSTEM "grass-addons.dtd">\n')
  356. fo.write('<addons version="%s">\n' % version[0])
  357. libgisRev = grass.version()['libgis_revision']
  358. if tree is not None:
  359. for tnode in tree.findall('task'):
  360. indent = 4
  361. fo.write('%s<task name="%s">\n' % (' ' * indent, tnode.get('name')))
  362. indent += 4
  363. fo.write('%s<description>%s</description>\n' % \
  364. (' ' * indent, tnode.find('description').text))
  365. fo.write('%s<keywords>%s</keywords>\n' % \
  366. (' ' * indent, tnode.find('keywords').text))
  367. bnode = tnode.find('binary')
  368. if bnode is not None:
  369. fo.write('%s<binary>\n' % (' ' * indent))
  370. indent += 4
  371. for fnode in bnode.findall('file'):
  372. fo.write('%s<file>%s</file>\n' % \
  373. (' ' * indent, os.path.join(options['prefix'], fnode.text)))
  374. indent -= 4
  375. fo.write('%s</binary>\n' % (' ' * indent))
  376. fo.write('%s<libgis revision="%s" />\n' % \
  377. (' ' * indent, libgisRev))
  378. indent -= 4
  379. fo.write('%s</task>\n' % (' ' * indent))
  380. fo.write('</addons>\n')
  381. fo.close()
  382. def write_xml_toolboxes(name, tree = None):
  383. fo = open(name, 'w')
  384. fo.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  385. fo.write('<!DOCTYPE toolbox SYSTEM "grass-addons.dtd">\n')
  386. fo.write('<addons version="%s">\n' % version[0])
  387. if tree is not None:
  388. for tnode in tree.findall('toolbox'):
  389. indent = 4
  390. fo.write('%s<toolbox name="%s" code="%s">\n' % \
  391. (' ' * indent, tnode.get('name'), tnode.get('code')))
  392. indent += 4
  393. for cnode in tnode.findall('correlate'):
  394. fo.write('%s<correlate code="%s" />\n' % \
  395. (' ' * indent, tnode.get('code')))
  396. for mnode in tnode.findall('task'):
  397. fo.write('%s<task name="%s" />\n' % \
  398. (' ' * indent, mnode.get('name')))
  399. indent -= 4
  400. fo.write('%s</toolbox>\n' % (' ' * indent))
  401. fo.write('</addons>\n')
  402. fo.close()
  403. # install extension - toolbox or module
  404. def install_extension(url):
  405. gisbase = os.getenv('GISBASE')
  406. if not gisbase:
  407. grass.fatal(_('$GISBASE not defined'))
  408. if options['extension'] in get_installed_extensions(force = True):
  409. grass.warning(_("Extension <%s> already installed. Re-installing...") % options['extension'])
  410. if flags['t']:
  411. grass.message(_("Installing toolbox <%s>...") % options['extension'])
  412. mlist = get_toolbox_modules(url, options['extension'])
  413. else:
  414. mlist = [options['extension']]
  415. if not mlist:
  416. grass.warning(_("Nothing to install"))
  417. return
  418. ret = 0
  419. for module in mlist:
  420. if sys.platform == "win32":
  421. ret += install_extension_win(module)
  422. else:
  423. ret += install_extension_other(module)
  424. if len(mlist) > 1:
  425. print '-' * 60
  426. if flags['d']:
  427. return
  428. if ret != 0:
  429. grass.warning(_('Installation failed, sorry. Please check above error messages.'))
  430. else:
  431. grass.message(_("Updating addons metadata file..."))
  432. blist = install_extension_xml(url, mlist)
  433. for module in blist:
  434. update_manual_page(module)
  435. grass.message(_("Installation of <%s> successfully finished") % options['extension'])
  436. if not os.getenv('GRASS_ADDON_BASE'):
  437. grass.warning(_('This add-on module will not function until you set the '
  438. 'GRASS_ADDON_BASE environment variable (see "g.manual variables")'))
  439. # update local meta-file when installing new extension (toolbox / modules)
  440. def install_toolbox_xml(url, name):
  441. # read metadata from remote server (toolboxes)
  442. url = url + "toolboxes.xml"
  443. data = dict()
  444. try:
  445. f = urlopen(url, proxies=PROXIES)
  446. tree = etree.fromstring(f.read())
  447. for tnode in tree.findall('toolbox'):
  448. clist = list()
  449. for cnode in tnode.findall('correlate'):
  450. clist.append(cnode.get('code'))
  451. mlist = list()
  452. for mnode in tnode.findall('task'):
  453. mlist.append(mnode.get('name'))
  454. code = tnode.get('code')
  455. data[code] = {
  456. 'name' : tnode.get('name'),
  457. 'correlate' : clist,
  458. 'modules' : mlist,
  459. }
  460. except HTTPError:
  461. grass.error(_("Unable to read addons metadata file from the remote server"))
  462. if not data:
  463. grass.warning(_("No addons metadata available"))
  464. return
  465. if name not in data:
  466. grass.warning(_("No addons metadata available for <%s>") % name)
  467. return
  468. fXML = os.path.join(options['prefix'], 'toolboxes.xml')
  469. # create an empty file if not exists
  470. if not os.path.exists(fXML):
  471. write_xml_modules(fXML)
  472. # read XML file
  473. fo = open(fXML, 'r')
  474. tree = etree.fromstring(fo.read())
  475. fo.close()
  476. # update tree
  477. tnode = None
  478. for node in tree.findall('toolbox'):
  479. if node.get('code') == name:
  480. tnode = node
  481. break
  482. tdata = data[name]
  483. if tnode is not None:
  484. # update existing node
  485. for cnode in tnode.findall('correlate'):
  486. tnode.remove(cnode)
  487. for mnode in tnode.findall('task'):
  488. tnode.remove(mnode)
  489. else:
  490. # create new node for task
  491. tnode = etree.Element('toolbox', attrib = { 'name' : tdata['name'], 'code' : name })
  492. tree.append(tnode)
  493. for cname in tdata['correlate']:
  494. cnode = etree.Element('correlate', attrib = { 'code' : cname })
  495. tnode.append(cnode)
  496. for tname in tdata['modules']:
  497. mnode = etree.Element('task', attrib = { 'name' : tname })
  498. tnode.append(mnode)
  499. write_xml_toolboxes(fXML, tree)
  500. # return list of executables for update_manual_page()
  501. def install_extension_xml(url, mlist):
  502. if len(mlist) > 1:
  503. # read metadata from remote server (toolboxes)
  504. install_toolbox_xml(url, options['extension'])
  505. # read metadata from remote server (modules)
  506. url = url + "modules.xml"
  507. data = {}
  508. bList = []
  509. try:
  510. f = urlopen(url, proxies=PROXIES)
  511. try:
  512. tree = etree.fromstring(f.read())
  513. except:
  514. grass.warning(_("Unable to parse '%s'. Addons metadata file not updated.") % url)
  515. return bList
  516. for mnode in tree.findall('task'):
  517. name = mnode.get('name')
  518. if name not in mlist:
  519. continue
  520. fList = list()
  521. bnode = mnode.find('binary')
  522. windows = sys.platform == 'win32'
  523. if bnode is not None:
  524. for fnode in bnode.findall('file'):
  525. path = fnode.text.split('/')
  526. if path[0] == 'bin':
  527. bList.append(path[-1])
  528. if windows:
  529. path[-1] += '.exe'
  530. elif path[0] == 'scripts':
  531. bList.append(path[-1])
  532. if windows:
  533. path[-1] += '.py'
  534. fList.append(os.path.sep.join(path))
  535. desc, keyw = get_optional_params(mnode)
  536. data[name] = {
  537. 'desc' : desc,
  538. 'keyw' : keyw,
  539. 'files' : fList,
  540. }
  541. except:
  542. grass.error(_("Unable to read addons metadata file from the remote server"))
  543. if not data:
  544. grass.warning(_("No addons metadata available"))
  545. return []
  546. fXML = os.path.join(options['prefix'], 'modules.xml')
  547. # create an empty file if not exists
  548. if not os.path.exists(fXML):
  549. write_xml_modules(fXML)
  550. # read XML file
  551. fo = open(fXML, 'r')
  552. tree = etree.fromstring(fo.read())
  553. fo.close()
  554. # update tree
  555. for name in mlist:
  556. tnode = None
  557. for node in tree.findall('task'):
  558. if node.get('name') == name:
  559. tnode = node
  560. break
  561. if name not in data:
  562. grass.warning(_("No addons metadata found for <%s>") % name)
  563. continue
  564. ndata = data[name]
  565. if tnode is not None:
  566. # update existing node
  567. dnode = tnode.find('description')
  568. if dnode is not None:
  569. dnode.text = ndata['desc']
  570. knode = tnode.find('keywords')
  571. if knode is not None:
  572. knode.text = ndata['keyw']
  573. bnode = tnode.find('binary')
  574. if bnode is not None:
  575. tnode.remove(bnode)
  576. bnode = etree.Element('binary')
  577. for f in ndata['files']:
  578. fnode = etree.Element('file')
  579. fnode.text = f
  580. bnode.append(fnode)
  581. tnode.append(bnode)
  582. else:
  583. # create new node for task
  584. tnode = etree.Element('task', attrib = { 'name' : name })
  585. dnode = etree.Element('description')
  586. dnode.text = ndata['desc']
  587. tnode.append(dnode)
  588. knode = etree.Element('keywords')
  589. knode.text = ndata['keyw']
  590. tnode.append(knode)
  591. bnode = etree.Element('binary')
  592. for f in ndata['files']:
  593. fnode = etree.Element('file')
  594. fnode.text = f
  595. bnode.append(fnode)
  596. tnode.append(bnode)
  597. tree.append(tnode)
  598. write_xml_modules(fXML, tree)
  599. return bList
  600. # install extension on MS Windows
  601. def install_extension_win(name):
  602. """Install extension on MS Windows"""
  603. # do not use hardcoded url -
  604. # http://wingrass.fsv.cvut.cz/platform/grassXX/addonsX.X.X
  605. grass.message(_("Downloading precompiled GRASS Addons <%s>...") %
  606. options['extension'])
  607. if build_platform == 'x86_64':
  608. platform = build_platform
  609. else:
  610. platform = 'x86'
  611. url = "http://wingrass.fsv.cvut.cz/%(platform)s/" \
  612. "grass%(major)s%(minor)s/addons/" \
  613. "grass-%(major)s.%(minor)s.%(patch)s/" % \
  614. {'platform' : platform, 'major': version[0],
  615. 'minor': version[1], 'patch': version[2]}
  616. grass.debug("url=%s" % url, 1)
  617. try:
  618. zfile = url + name + '.zip'
  619. f = urlopen(zfile, proxies=PROXIES)
  620. # create addons dir if not exists
  621. if not os.path.exists(options['prefix']):
  622. try:
  623. os.mkdir(options['prefix'])
  624. except OSError as e:
  625. grass.fatal(_("Unable to create <{}>. {}").format(options['prefix'], e))
  626. # download data
  627. fo = tempfile.TemporaryFile()
  628. fo.write(f.read())
  629. try:
  630. zfobj = zipfile.ZipFile(fo)
  631. except zipfile.BadZipfile as e:
  632. grass.fatal('%s: %s' % (e, zfile))
  633. for name in zfobj.namelist():
  634. if name.endswith('/'):
  635. d = os.path.join(options['prefix'], name)
  636. if not os.path.exists(d):
  637. os.mkdir(d)
  638. else:
  639. outfile = open(os.path.join(options['prefix'], name), 'wb')
  640. outfile.write(zfobj.read(name))
  641. outfile.close()
  642. fo.close()
  643. except HTTPError:
  644. grass.fatal(_("GRASS Addons <%s> not found") % name)
  645. return 0
  646. # install extension on other plaforms
  647. def install_extension_other(name):
  648. gisbase = os.getenv('GISBASE')
  649. classchar = name.split('.', 1)[0]
  650. moduleclass = expand_module_class_name(classchar)
  651. url = options['svnurl'] + '/' + moduleclass + '/' + name
  652. grass.message(_("Fetching <%s> from GRASS-Addons SVN repository (be patient)...") % name)
  653. os.chdir(TMPDIR)
  654. if grass.verbosity() <= 2:
  655. outdev = open(os.devnull, 'w')
  656. else:
  657. outdev = sys.stdout
  658. if grass.call(['svn', 'checkout',
  659. url], stdout = outdev) != 0:
  660. grass.fatal(_("GRASS Addons <%s> not found") % name)
  661. dirs = { 'bin' : os.path.join(TMPDIR, name, 'bin'),
  662. 'docs' : os.path.join(TMPDIR, name, 'docs'),
  663. 'html' : os.path.join(TMPDIR, name, 'docs', 'html'),
  664. 'rest' : os.path.join(TMPDIR, name, 'docs', 'rest'),
  665. 'man' : os.path.join(TMPDIR, name, 'docs', 'man'),
  666. 'script' : os.path.join(TMPDIR, name, 'scripts'),
  667. ### TODO: handle locales also for addons
  668. # 'string' : os.path.join(TMPDIR, name, 'locale'),
  669. 'string' : os.path.join(TMPDIR, name),
  670. 'etc' : os.path.join(TMPDIR, name, 'etc'),
  671. }
  672. makeCmd = ['make',
  673. 'MODULE_TOPDIR=%s' % gisbase.replace(' ', '\ '),
  674. 'RUN_GISRC=%s' % os.environ['GISRC'],
  675. 'BIN=%s' % dirs['bin'],
  676. 'HTMLDIR=%s' % dirs['html'],
  677. 'RESTDIR=%s' % dirs['rest'],
  678. 'MANBASEDIR=%s' % dirs['man'],
  679. 'SCRIPTDIR=%s' % dirs['script'],
  680. 'STRINGDIR=%s' % dirs['string'],
  681. 'ETC=%s' % os.path.join(dirs['etc'])
  682. ]
  683. installCmd = ['make',
  684. 'MODULE_TOPDIR=%s' % gisbase,
  685. 'ARCH_DISTDIR=%s' % os.path.join(TMPDIR, name),
  686. 'INST_DIR=%s' % options['prefix'],
  687. 'install'
  688. ]
  689. if flags['d']:
  690. grass.message("\n%s\n" % _("To compile run:"))
  691. sys.stderr.write(' '.join(makeCmd) + '\n')
  692. grass.message("\n%s\n" % _("To install run:"))
  693. sys.stderr.write(' '.join(installCmd) + '\n')
  694. return 0
  695. os.chdir(os.path.join(TMPDIR, name))
  696. grass.message(_("Compiling..."))
  697. if not os.path.exists(os.path.join(gisbase, 'include',
  698. 'Make', 'Module.make')):
  699. grass.fatal(_("Please install GRASS development package"))
  700. if 0 != grass.call(makeCmd,
  701. stdout = outdev):
  702. grass.fatal(_('Compilation failed, sorry. Please check above error messages.'))
  703. if flags['i']:
  704. return 0
  705. grass.message(_("Installing..."))
  706. return grass.call(installCmd,
  707. stdout = outdev)
  708. # remove existing extension - toolbox or module
  709. def remove_extension(force = False):
  710. if flags['t']:
  711. mlist = get_toolbox_modules(options['extension'])
  712. else:
  713. mlist = [options['extension']]
  714. if force:
  715. grass.verbose(_("List of removed files:"))
  716. else:
  717. grass.info(_("Files to be removed:"))
  718. remove_modules(mlist, force)
  719. if force:
  720. grass.message(_("Updating addons metadata file..."))
  721. remove_extension_xml(mlist)
  722. grass.message(_("Extension <%s> successfully uninstalled.") % options['extension'])
  723. else:
  724. grass.warning(_("Extension <%s> not removed. "
  725. "Re-run '%s' with '-f' flag to force removal") % (options['extension'], 'g.extension'))
  726. # remove existing extension(s) (reading XML file)
  727. def remove_modules(mlist, force = False):
  728. # try to read XML metadata file first
  729. fXML = os.path.join(options['prefix'], 'modules.xml')
  730. installed = get_installed_modules()
  731. if os.path.exists(fXML):
  732. f = open(fXML, 'r')
  733. tree = etree.fromstring(f.read())
  734. f.close()
  735. else:
  736. tree = None
  737. for name in mlist:
  738. if name not in installed:
  739. # try even if module does not seem to be available,
  740. # as the user may be trying to get rid of left over cruft
  741. grass.warning(_("Extension <%s> not found") % name)
  742. if tree is not None:
  743. flist = []
  744. for task in tree.findall('task'):
  745. if name == task.get('name') and \
  746. task.find('binary') is not None:
  747. for f in task.find('binary').findall('file'):
  748. flist.append(f.text)
  749. break
  750. if flist:
  751. removed = False
  752. err = list()
  753. for fpath in flist:
  754. try:
  755. if force:
  756. grass.verbose(fpath)
  757. removed = True
  758. os.remove(fpath)
  759. else:
  760. print fpath
  761. except OSError:
  762. err.append((_("Unable to remove file '%s'") % fpath))
  763. if force and not removed:
  764. grass.fatal(_("Extension <%s> not found") % name)
  765. if err:
  766. for e in err:
  767. grass.error(e)
  768. else:
  769. remove_extension_std(name, force)
  770. else:
  771. remove_extension_std(name, force)
  772. # remove exising extension (using standard files layout)
  773. def remove_extension_std(name, force = False):
  774. for fpath in [os.path.join(options['prefix'], 'bin', name),
  775. os.path.join(options['prefix'], 'scripts', name),
  776. os.path.join(options['prefix'], 'docs', 'html', name + '.html'),
  777. os.path.join(options['prefix'], 'docs', 'rest', name + '.txt'),
  778. os.path.join(options['prefix'], 'docs', 'man', 'man1', name + '.1')]:
  779. if os.path.isfile(fpath):
  780. if force:
  781. grass.verbose(fpath)
  782. os.remove(fpath)
  783. else:
  784. print fpath
  785. # update local meta-file when removing existing extension
  786. def remove_toolbox_xml(name):
  787. fXML = os.path.join(options['prefix'], 'toolboxes.xml')
  788. if not os.path.exists(fXML):
  789. return
  790. # read XML file
  791. fo = open(fXML, 'r')
  792. tree = etree.fromstring(fo.read())
  793. fo.close()
  794. for node in tree.findall('toolbox'):
  795. if node.get('code') != name:
  796. continue
  797. tree.remove(node)
  798. write_xml_toolboxes(fXML, tree)
  799. def remove_extension_xml(modules):
  800. if len(modules) > 1:
  801. # update also toolboxes metadata
  802. remove_toolbox_xml(options['extension'])
  803. fXML = os.path.join(options['prefix'], 'modules.xml')
  804. if not os.path.exists(fXML):
  805. return
  806. # read XML file
  807. fo = open(fXML, 'r')
  808. tree = etree.fromstring(fo.read())
  809. fo.close()
  810. for name in modules:
  811. for node in tree.findall('task'):
  812. if node.get('name') != name:
  813. continue
  814. tree.remove(node)
  815. write_xml_modules(fXML, tree)
  816. # check links in CSS
  817. def check_style_files(fil):
  818. dist_file = os.path.join(os.getenv('GISBASE'), 'docs', 'html', fil)
  819. addons_file = os.path.join(options['prefix'], 'docs', 'html', fil)
  820. if os.path.isfile(addons_file):
  821. return
  822. try:
  823. shutil.copyfile(dist_file, addons_file)
  824. except OSError as e:
  825. grass.fatal(_("Unable to create '%s': %s") % (addons_file, e))
  826. def create_dir(path):
  827. if os.path.isdir(path):
  828. return
  829. try:
  830. os.makedirs(path)
  831. except OSError as e:
  832. grass.fatal(_("Unable to create '%s': %s") % (path, e))
  833. grass.debug("'%s' created" % path)
  834. def check_dirs():
  835. create_dir(os.path.join(options['prefix'], 'bin'))
  836. create_dir(os.path.join(options['prefix'], 'docs', 'html'))
  837. create_dir(os.path.join(options['prefix'], 'docs', 'rest'))
  838. check_style_files('grass_logo.png')
  839. check_style_files('grassdocs.css')
  840. create_dir(os.path.join(options['prefix'], 'etc'))
  841. create_dir(os.path.join(options['prefix'], 'docs', 'man', 'man1'))
  842. create_dir(os.path.join(options['prefix'], 'scripts'))
  843. # fix file URI in manual page
  844. def update_manual_page(module):
  845. if module.split('.', 1)[0] == 'wx':
  846. return # skip for GUI modules
  847. grass.verbose(_("Manual page for <%s> updated") % module)
  848. # read original html file
  849. htmlfile = os.path.join(options['prefix'], 'docs', 'html', module + '.html')
  850. try:
  851. f = open(htmlfile)
  852. shtml = f.read()
  853. except IOError as e:
  854. grass.fatal(_("Unable to read manual page: %s") % e)
  855. else:
  856. f.close()
  857. pos = []
  858. # fix logo URL
  859. pattern = r'''<a href="([^"]+)"><img src="grass_logo.png"'''
  860. for match in re.finditer(pattern, shtml):
  861. pos.append(match.start(1))
  862. # find URIs
  863. pattern = r'''<a href="([^"]+)">([^>]+)</a>'''
  864. addons = get_installed_extensions(force = True)
  865. for match in re.finditer(pattern, shtml):
  866. if match.group(1)[:4] == 'http':
  867. continue
  868. if match.group(1).replace('.html', '') in addons:
  869. continue
  870. pos.append(match.start(1))
  871. if not pos:
  872. return # no match
  873. # replace file URIs
  874. prefix = 'file://' + '/'.join([os.getenv('GISBASE'), 'docs', 'html'])
  875. ohtml = shtml[:pos[0]]
  876. for i in range(1, len(pos)):
  877. ohtml += prefix + '/' + shtml[pos[i-1]:pos[i]]
  878. ohtml += prefix + '/' + shtml[pos[-1]:]
  879. # write updated html file
  880. try:
  881. f = open(htmlfile, 'w')
  882. f.write(ohtml)
  883. except IOError as e:
  884. grass.fatal(_("Unable for write manual page: %s") % e)
  885. else:
  886. f.close()
  887. def main():
  888. # check dependecies
  889. if not flags['a'] and sys.platform != "win32":
  890. check_progs()
  891. # manage proxies
  892. global PROXIES
  893. if options['proxy']:
  894. PROXIES = {}
  895. for ptype, purl in (p.split('=') for p in options['proxy'].split(',')):
  896. PROXIES[ptype] = purl
  897. # define path
  898. if flags['s']:
  899. options['prefix'] = os.environ['GISBASE']
  900. if options['prefix'] == '$GRASS_ADDON_BASE':
  901. if not os.getenv('GRASS_ADDON_BASE'):
  902. grass.warning(_("GRASS_ADDON_BASE is not defined, "
  903. "installing to ~/.grass%s/addons") % version[0])
  904. options['prefix'] = os.path.join(os.environ['HOME'], '.grass%s' % version[0], 'addons')
  905. else:
  906. options['prefix'] = os.environ['GRASS_ADDON_BASE']
  907. if os.path.exists(options['prefix']) and \
  908. not os.access(options['prefix'], os.W_OK):
  909. grass.fatal(_("You don't have permission to install extension to <{}>. "
  910. "Try to run {} with administrator rights "
  911. "(su or sudo).").format(options['prefix'], 'g.extension'))
  912. if 'svn.osgeo.org/grass/grass-addons/grass7' in options['svnurl']:
  913. # use pregenerated modules XML file
  914. xmlurl = "http://grass.osgeo.org/addons/grass%s" % version[0]
  915. else:
  916. # try to get modules XMl from SVN repository
  917. xmlurl = options['svnurl']
  918. if not xmlurl.endswith('/'):
  919. xmlurl = xmlurl + "/"
  920. # list available extensions
  921. if flags['l'] or flags['c'] or flags['g']:
  922. list_available_extensions(xmlurl)
  923. return 0
  924. elif flags['a']:
  925. elist = get_installed_extensions()
  926. if elist:
  927. if flags['t']:
  928. grass.message(_("List of installed extensions (toolboxes):"))
  929. else:
  930. grass.message(_("List of installed extensions (modules):"))
  931. sys.stdout.write('\n'.join(elist))
  932. sys.stdout.write('\n')
  933. else:
  934. if flags['t']:
  935. grass.info(_("No extension (toolbox) installed"))
  936. else:
  937. grass.info(_("No extension (module) installed"))
  938. return 0
  939. else:
  940. if not options['extension']:
  941. grass.fatal(_('You need to define an extension name or use -l/c/g/a'))
  942. if flags['d']:
  943. if options['operation'] != 'add':
  944. grass.warning(_("Flag 'd' is relevant only to 'operation=add'. Ignoring this flag."))
  945. else:
  946. global REMOVE_TMPDIR
  947. REMOVE_TMPDIR = False
  948. if options['operation'] == 'add':
  949. check_dirs()
  950. install_extension(xmlurl)
  951. else: # remove
  952. remove_extension(flags['f'])
  953. return 0
  954. if __name__ == "__main__":
  955. options, flags = grass.parser()
  956. global TMPDIR
  957. TMPDIR = tempfile.mkdtemp()
  958. atexit.register(cleanup)
  959. grass_version = grass.version()
  960. version = grass_version['version'].split('.')
  961. build_platform = grass_version['build_platform'].split('-', 1)[0]
  962. sys.exit(main())