g.extension.py 20 KB

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