g.extension.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. #!/usr/bin/env python
  2. ############################################################################
  3. #
  4. # MODULE: g.extension
  5. # AUTHOR(S): Markus Neteler
  6. # Pythonized by Martin Landa
  7. # PURPOSE: Tool to download and install extensions from GRASS Addons SVN into
  8. # local GRASS installation
  9. # COPYRIGHT: (C) 2009-2011 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: Tool to maintain the 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. #% required: yes
  46. #% answer: http://svn.osgeo.org/grass/grass-addons/grass7
  47. #%end
  48. #%option
  49. #% key: prefix
  50. #% type: string
  51. #% key_desc: path
  52. #% description: Prefix where to install extension (ignored when flag -s is given)
  53. #% answer: $GRASS_ADDON_PATH
  54. #% required: no
  55. #%end
  56. #%flag
  57. #% key: l
  58. #% description: List available modules in the GRASS Addons SVN repository
  59. #% guisection: Print
  60. #% suppress_required: yes
  61. #%end
  62. #%flag
  63. #% key: c
  64. #% description: List available modules in the GRASS Addons SVN repository including complete module description
  65. #% guisection: Print
  66. #% suppress_required: yes
  67. #%end
  68. #%flag
  69. #% key: g
  70. #% description: List available modules in the GRASS Addons SVN repository (shell script style)
  71. #% guisection: Print
  72. #% suppress_required: yes
  73. #%end
  74. #%flag
  75. #% key: s
  76. #% description: Install system-wide (may need system administrator rights)
  77. #%end
  78. #%flag
  79. #% key: d
  80. #% description: Download source code and exit
  81. #%end
  82. #%flag
  83. #% key: i
  84. #% description: Don't install new extension, just compile it
  85. #%end
  86. #%flag
  87. #% key: f
  88. #% description: Force removal (required for actual deletion of files)
  89. #%end
  90. import os
  91. import sys
  92. import re
  93. import atexit
  94. import shutil
  95. import glob
  96. from urllib2 import urlopen, HTTPError
  97. try:
  98. import xml.etree.ElementTree as etree
  99. except ImportError:
  100. import elementtree.ElementTree as etree # Python <= 2.4
  101. from grass.script import core as grass
  102. # temp dir
  103. remove_tmpdir = True
  104. def check():
  105. for prog in ('svn', 'make', 'gcc'):
  106. if not grass.find_program(prog, ['--help']):
  107. grass.fatal(_("'%s' required. Please install '%s' first.") % (prog, prog))
  108. def expand_module_class_name(c):
  109. name = { 'd' : 'display',
  110. 'db' : 'database',
  111. 'g' : 'general',
  112. 'i' : 'imagery',
  113. 'm' : 'misc',
  114. 'ps' : 'postscript',
  115. 'p' : 'paint',
  116. 'r' : 'raster',
  117. 'r3' : 'raster3d',
  118. 's' : 'sites',
  119. 'v' : 'vector',
  120. 'gui' : 'gui/wxpython' }
  121. if name.has_key(c):
  122. return name[c]
  123. return c
  124. def list_available_modules():
  125. mlist = list()
  126. # try to download XML metadata file first
  127. url = "http://grass.osgeo.org/addons/grass%s.xml" % grass.version()['version'].split('.')[0]
  128. try:
  129. f = urlopen(url)
  130. tree = etree.fromstring(f.read())
  131. for mnode in tree.findall('task'):
  132. name = mnode.get('name')
  133. if flags['c'] or flags['g']:
  134. desc = mnode.find('description').text
  135. if not desc:
  136. desc = ''
  137. keyw = mnode.find('keywords').text
  138. if not keyw:
  139. keyw = ''
  140. if flags['g']:
  141. print 'name=' + name
  142. print 'description=' + desc
  143. print 'keywords=' + keyw
  144. elif flags['c']:
  145. print name + ' - ' + desc
  146. else:
  147. print name
  148. except HTTPError:
  149. return list_available_modules_svn()
  150. return mlist
  151. def list_available_modules_svn():
  152. mlist = list()
  153. grass.message(_('Fetching list of modules from GRASS-Addons SVN (be patient)...'))
  154. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  155. i = 0
  156. prefix = ['d', 'db', 'g', 'i', 'm', 'ps',
  157. 'p', 'r', 'r3', 's', 'v']
  158. nprefix = len(prefix)
  159. for d in prefix:
  160. if flags['g']:
  161. grass.percent(i, nprefix, 1)
  162. i += 1
  163. modclass = expand_module_class_name(d)
  164. grass.verbose(_("Checking for '%s' modules...") % modclass)
  165. url = '%s/%s' % (options['svnurl'], modclass)
  166. grass.debug("url = %s" % url, debug = 2)
  167. try:
  168. f = urlopen(url)
  169. except HTTPError:
  170. grass.debug(_("Unable to fetch '%s'") % url, debug = 1)
  171. continue
  172. for line in f.readlines():
  173. # list modules
  174. sline = pattern.search(line)
  175. if not sline:
  176. continue
  177. name = sline.group(2).rstrip('/')
  178. if name.split('.', 1)[0] == d:
  179. print_module_desc(name, url)
  180. mlist.append(name)
  181. mlist += list_wxgui_extensions()
  182. if flags['g']:
  183. grass.percent(1, 1, 1)
  184. return mlist
  185. def list_wxgui_extensions(print_module = True):
  186. mlist = list()
  187. grass.debug('Fetching list of wxGUI extensions from GRASS-Addons SVN (be patient)...')
  188. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  189. grass.verbose(_("Checking for '%s' modules...") % 'gui/wxpython')
  190. url = '%s/%s' % (options['svnurl'], 'gui/wxpython')
  191. grass.debug("url = %s" % url, debug = 2)
  192. f = urlopen(url)
  193. if not f:
  194. grass.warning(_("Unable to fetch '%s'") % url)
  195. return
  196. for line in f.readlines():
  197. # list modules
  198. sline = pattern.search(line)
  199. if not sline:
  200. continue
  201. name = sline.group(2).rstrip('/')
  202. if name not in ('..', 'Makefile'):
  203. if print_module:
  204. print_module_desc(name, url)
  205. mlist.append(name)
  206. return mlist
  207. def print_module_desc(name, url):
  208. if not flags['c'] and not flags['g']:
  209. print name
  210. return
  211. if flags['g']:
  212. print 'name=' + name
  213. # check main.c first
  214. desc = get_module_desc(url + '/' + name + '/' + name)
  215. if not desc:
  216. desc = get_module_desc(url + '/' + name + '/main.c', script = False)
  217. if not desc:
  218. if not flags['g']:
  219. print name + ' - '
  220. return
  221. if flags['g']:
  222. print 'description=' + desc.get('description', '')
  223. print 'keywords=' + ','.join(desc.get('keywords', list()))
  224. else:
  225. print name + ' - ' + desc.get('description', '')
  226. def get_module_desc(url, script = True):
  227. grass.debug('url=%s' % url)
  228. try:
  229. f = urlopen(url)
  230. except HTTPError:
  231. return {}
  232. if script:
  233. ret = get_module_script(f)
  234. else:
  235. ret = get_module_main(f)
  236. return ret
  237. def get_module_main(f):
  238. if not f:
  239. return dict()
  240. ret = { 'keyword' : list() }
  241. pattern = re.compile(r'(module.*->)(.+)(=)(.*)', re.IGNORECASE)
  242. keyword = re.compile(r'(G_add_keyword\()(.+)(\);)', re.IGNORECASE)
  243. key = ''
  244. value = ''
  245. for line in f.readlines():
  246. line = line.strip()
  247. find = pattern.search(line)
  248. if find:
  249. key = find.group(2).strip()
  250. line = find.group(4).strip()
  251. else:
  252. find = keyword.search(line)
  253. if find:
  254. ret['keyword'].append(find.group(2).replace('"', '').replace('_(', '').replace(')', ''))
  255. if key:
  256. value += line
  257. if line[-2:] == ');':
  258. value = value.replace('"', '').replace('_(', '').replace(');', '')
  259. if key == 'keywords':
  260. ret[key] = map(lambda x: x.strip(), value.split(','))
  261. else:
  262. ret[key] = value
  263. key = value = ''
  264. return ret
  265. def get_module_script(f):
  266. ret = dict()
  267. if not f:
  268. return ret
  269. begin = re.compile(r'#%.*module', re.IGNORECASE)
  270. end = re.compile(r'#%.*end', re.IGNORECASE)
  271. mline = None
  272. for line in f.readlines():
  273. if not mline:
  274. mline = begin.search(line)
  275. if mline:
  276. if end.search(line):
  277. break
  278. try:
  279. key, value = line.split(':', 1)
  280. key = key.replace('#%', '').strip()
  281. value = value.strip()
  282. if key == 'keywords':
  283. ret[key] = map(lambda x: x.strip(), value.split(','))
  284. else:
  285. ret[key] = value
  286. except ValueError:
  287. pass
  288. return ret
  289. def cleanup():
  290. if remove_tmpdir:
  291. grass.try_rmdir(tmpdir)
  292. else:
  293. grass.message(_("Path to the source code:"))
  294. sys.stderr.write('%s\n' % os.path.join(tmpdir, options['extension']))
  295. def install_extension_win():
  296. ### TODO: do not use hardcoded url
  297. version = grass.version()['version'].split('.')
  298. url = "http://wingrass.fsv.cvut.cz/grass%s%s/addons" % (version[0], version[1])
  299. success = False
  300. grass.message(_("Downloading precompiled GRASS Addons <%s>...") % options['extension'])
  301. for comp, ext in [(('bin', ), 'exe'),
  302. (('docs', 'html'), 'html'),
  303. (('man', 'man1'), None),
  304. (('scripts', ), 'py')]:
  305. name = options['extension']
  306. if ext:
  307. name += '.' + ext
  308. try:
  309. f = urlopen(url + '/' + '/'.join(comp) + '/' + name)
  310. fo = open(os.path.join(options['prefix'], os.path.sep.join(comp), name), 'wb')
  311. fo.write(f.read())
  312. fo.close()
  313. if comp[0] in ('bin', 'scripts'):
  314. success = True
  315. except HTTPError:
  316. pass
  317. if not success:
  318. grass.fatal(_("GRASS Addons <%s> not found") % options['extension'])
  319. def install_extension():
  320. gisbase = os.getenv('GISBASE')
  321. if not gisbase:
  322. grass.fatal(_('$GISBASE not defined'))
  323. if grass.find_program(options['extension'], ['--help']):
  324. grass.warning(_("Extension '%s' already installed. Will be updated...") % options['extension'])
  325. gui_list = list_wxgui_extensions(print_module = False)
  326. if options['extension'] not in gui_list:
  327. classchar = options['extension'].split('.', 1)[0]
  328. moduleclass = expand_module_class_name(classchar)
  329. url = options['svnurl'] + '/' + moduleclass + '/' + options['extension']
  330. else:
  331. url = options['svnurl'] + '/gui/wxpython/' + options['extension']
  332. if not flags['s']:
  333. grass.fatal(_("Installation of wxGUI extension requires -%s flag.") % 's')
  334. grass.message(_("Fetching '%s' from GRASS-Addons SVN (be patient)...") % options['extension'])
  335. os.chdir(tmpdir)
  336. if grass.verbosity() == 0:
  337. outdev = open(os.devnull, 'w')
  338. else:
  339. outdev = sys.stdout
  340. if grass.call(['svn', 'checkout',
  341. url], stdout = outdev) != 0:
  342. grass.fatal(_("GRASS Addons <%s> not found") % options['extension'])
  343. dirs = { 'bin' : os.path.join(tmpdir, options['extension'], 'bin'),
  344. 'docs' : os.path.join(tmpdir, options['extension'], 'docs'),
  345. 'html' : os.path.join(tmpdir, options['extension'], 'docs', 'html'),
  346. 'man' : os.path.join(tmpdir, options['extension'], 'man'),
  347. 'man1' : os.path.join(tmpdir, options['extension'], 'man', 'man1'),
  348. 'scripts' : os.path.join(tmpdir, options['extension'], 'scripts'),
  349. 'etc' : os.path.join(tmpdir, options['extension'], 'etc'),
  350. }
  351. makeCmd = ['make',
  352. 'MODULE_TOPDIR=%s' % gisbase.replace(' ', '\ '),
  353. 'BIN=%s' % dirs['bin'],
  354. 'HTMLDIR=%s' % dirs['html'],
  355. 'MANDIR=%s' % dirs['man1'],
  356. 'SCRIPTDIR=%s' % dirs['scripts'],
  357. 'ETC=%s' % os.path.join(dirs['etc'],options['extension'])
  358. ]
  359. installCmd = ['make',
  360. 'MODULE_TOPDIR=%s' % gisbase,
  361. 'ARCH_DISTDIR=%s' % os.path.join(tmpdir, options['extension']),
  362. 'INST_DIR=%s' % options['prefix'],
  363. 'install'
  364. ]
  365. if flags['d']:
  366. grass.message(_("To compile run:"))
  367. sys.stderr.write(' '.join(makeCmd) + '\n')
  368. grass.message(_("To install run:\n\n"))
  369. sys.stderr.write(' '.join(installCmd) + '\n')
  370. return
  371. os.chdir(os.path.join(tmpdir, options['extension']))
  372. grass.message(_("Compiling '%s'...") % options['extension'])
  373. if options['extension'] not in gui_list:
  374. ret = grass.call(makeCmd,
  375. stdout = outdev)
  376. else:
  377. ret = grass.call(['make',
  378. 'MODULE_TOPDIR=%s' % gisbase.replace(' ', '\ ')],
  379. stdout = outdev)
  380. if ret != 0:
  381. grass.fatal(_('Compilation failed, sorry. Please check above error messages.'))
  382. if flags['i'] or options['extension'] in gui_list:
  383. return
  384. grass.message(_("Installing '%s'...") % options['extension'])
  385. ret = grass.call(installCmd,
  386. stdout = outdev)
  387. if ret != 0:
  388. grass.warning(_('Installation failed, sorry. Please check above error messages.'))
  389. else:
  390. grass.message(_("Installation of '%s' successfully finished.") % options['extension'])
  391. # manual page: fix href
  392. if os.getenv('GRASS_ADDON_PATH'):
  393. html_man = os.path.join(os.getenv('GRASS_ADDON_PATH'), 'docs', 'html', options['extension'] + '.html')
  394. if os.path.exists(html_man):
  395. fd = open(html_man)
  396. html_str = '\n'.join(fd.readlines())
  397. fd.close()
  398. for rep in ('grassdocs.css', 'grass_logo.png'):
  399. patt = re.compile(rep, re.IGNORECASE)
  400. html_str = patt.sub(os.path.join(gisbase, 'docs', 'html', rep),
  401. html_str)
  402. patt = re.compile(r'(<a href=")(d|db|g|i|m|p|ps|r|r3|s|v|wxGUI)(\.)(.+)(.html">)', re.IGNORECASE)
  403. while True:
  404. m = patt.search(html_str)
  405. if not m:
  406. break
  407. html_str = patt.sub(m.group(1) + os.path.join(gisbase, 'docs', 'html',
  408. m.group(2) + m.group(3) + m.group(4)) + m.group(5),
  409. html_str, count = 1)
  410. fd = open(html_man, "w")
  411. fd.write(html_str)
  412. fd.close()
  413. if not os.environ.has_key('GRASS_ADDON_PATH') or \
  414. not os.environ['GRASS_ADDON_PATH']:
  415. grass.warning(_('This add-on module will not function until you set the '
  416. 'GRASS_ADDON_PATH environment variable (see "g.manual variables")'))
  417. def remove_extension(flags):
  418. #is module available?
  419. bin_dir = os.path.join(options['prefix'], 'bin')
  420. scr_dir = os.path.join(options['prefix'], 'scripts')
  421. #add glob because if install a module with several submodule like r.modis
  422. #or r.pi.* or r.stream.* it was not possible to remove all the module
  423. #but the user has to remove the single command
  424. if glob.glob1(bin_dir,options['extension'] + "*"):
  425. modules = glob.glob1(bin_dir,options['extension'] + "*")
  426. elif glob.glob1(scr_dir,options['extension'] + "*"):
  427. modules = glob.glob1(scr_dir,options['extension'] + "*")
  428. else:
  429. grass.fatal(_("No module <%s> found") % options['extension'])
  430. #the user want really remove the scripts
  431. if flags['f']:
  432. #for each module remove script and documentation files
  433. for mod in modules:
  434. for f in [os.path.join(bin_dir, mod), os.path.join(scr_dir, mod),
  435. os.path.join(options['prefix'], 'docs', 'html', mod + '.html'),
  436. os.path.join(options['prefix'], 'man', 'man1', mod + '.1')]:
  437. grass.try_remove(f)
  438. #add etc for the internal library of a module
  439. grass.try_rmdir(os.path.join(options['prefix'], 'etc', options['extension']))
  440. grass.message(_("Module <%s> successfully uninstalled") % options['extension'])
  441. #print modules that you are going to remove with -f option
  442. else:
  443. for mod in modules:
  444. grass.message(mod)
  445. grass.message(_("You must use the force flag (-%s) to actually remove them. Exiting") % "f")
  446. def create_dir(path):
  447. if os.path.isdir(path):
  448. return
  449. try:
  450. os.makedirs(path)
  451. except OSError, e:
  452. grass.fatal(_("Unable to create '%s': %s") % (path, e))
  453. grass.debug("'%s' created" % path)
  454. def check_style_files(fil):
  455. #check the links to grassdocs.css/grass_logo.png to a correct manual page of addons
  456. dist_file = os.path.join(os.getenv('GISBASE'),'docs','html',fil)
  457. addons_file = os.path.join(options['prefix'],'docs','html',fil)
  458. #check if file already exists in the grass addons docs html path
  459. if os.path.isfile(addons_file):
  460. return
  461. #otherwise copy the file from $GISBASE/docs/html, it doesn't use link
  462. #because os.symlink it work only in Linux
  463. else:
  464. try:
  465. shutil.copyfile(dist_file,addons_file)
  466. except OSError, e:
  467. grass.fatal(_("Unable to create '%s': %s") % (addons_file, e))
  468. def check_dirs():
  469. create_dir(os.path.join(options['prefix'], 'bin'))
  470. create_dir(os.path.join(options['prefix'], 'docs', 'html'))
  471. check_style_files('grass_logo.png')
  472. check_style_files('grassdocs.css')
  473. create_dir(os.path.join(options['prefix'], 'man', 'man1'))
  474. create_dir(os.path.join(options['prefix'], 'scripts'))
  475. def main():
  476. # check dependecies
  477. if sys.platform != "win32":
  478. check()
  479. # list available modules
  480. if flags['l'] or flags['c'] or flags['g']:
  481. list_available_modules()
  482. return 0
  483. else:
  484. if not options['extension']:
  485. grass.fatal(_('You need to define an extension name or use -l'))
  486. # define path
  487. if flags['s']:
  488. options['prefix'] = os.environ['GISBASE']
  489. if options['prefix'] == '$GRASS_ADDON_PATH':
  490. if not os.environ.has_key('GRASS_ADDON_PATH') or \
  491. not os.environ['GRASS_ADDON_PATH']:
  492. major_version = int(grass.version()['version'].split('.', 1)[0])
  493. grass.warning(_("GRASS_ADDON_PATH is not defined, "
  494. "installing to ~/.grass%d/addons/") % major_version)
  495. options['prefix'] = os.path.join(os.environ['HOME'], '.grass%d' % major_version, 'addons')
  496. else:
  497. path_list = os.environ['GRASS_ADDON_PATH'].split(os.pathsep)
  498. if len(path_list) < 1:
  499. grass.fatal(_("Invalid GRASS_ADDON_PATH value - '%s'") % os.environ['GRASS_ADDON_PATH'])
  500. if len(path_list) > 1:
  501. grass.warning(_("GRASS_ADDON_PATH has more items, using first defined - '%s'") % path_list[0])
  502. options['prefix'] = path_list[0]
  503. # check dirs
  504. check_dirs()
  505. if flags['d']:
  506. if options['operation'] != 'add':
  507. grass.warning(_("Flag 'd' is relevant only to 'operation=add'. Ignoring this flag."))
  508. else:
  509. global remove_tmpdir
  510. remove_tmpdir = False
  511. if options['operation'] == 'add':
  512. if sys.platform == "win32":
  513. install_extension_win()
  514. else:
  515. install_extension()
  516. else: # remove
  517. remove_extension(flags)
  518. return 0
  519. if __name__ == "__main__":
  520. options, flags = grass.parser()
  521. global tmpdir
  522. tmpdir = grass.tempdir()
  523. atexit.register(cleanup)
  524. sys.exit(main())