g.extension.py 33 KB

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