g.extension.py 35 KB

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