g.extension.py 33 KB

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