g.extension.py 19 KB

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