g.extension.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  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-2010 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: no
  31. #%end
  32. #%option
  33. #% key: operation
  34. #% type: string
  35. #% description: Operation to be performed
  36. #% required: no
  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: https://svn.osgeo.org/grass/grass-addons
  47. #%end
  48. #%option
  49. #% key: prefix
  50. #% type: string
  51. #% key_desc: path
  52. #% description: Prefix where to install extension
  53. #% answer: $(HOME)/.grass7/addons
  54. #% required: yes
  55. #%end
  56. #%option
  57. #% key: menuitem
  58. #% type: string
  59. #% label: Menu item in wxGUI
  60. #% description: Given as string, e.g. 'Imagery;Filter image'
  61. #% required: no
  62. #%end
  63. #%flag
  64. #% key: l
  65. #% description: List available modules in the add-ons repository
  66. #% guisection: Print
  67. #%end
  68. #%flag
  69. #% key: f
  70. #% description: List available modules in the add-ons repository including modules description
  71. #% guisection: Print
  72. #%end
  73. #%flag
  74. #% key: g
  75. #% description: List available modules in the add-ons repository in shell script style
  76. #% guisection: Print
  77. #%end
  78. #%flag
  79. #% key: d
  80. #% description: Don't deleted downloaded source code when installing new extension
  81. #%end
  82. import os
  83. import sys
  84. import re
  85. import atexit
  86. import urllib
  87. from grass.script import core as grass
  88. # temp dir
  89. remove_tmpdir = True
  90. tmpdir = grass.tempfile()
  91. grass.try_remove(tmpdir)
  92. os.mkdir(tmpdir)
  93. def check():
  94. # check if we have the svn client
  95. if not grass.find_program('svn'):
  96. grass.fatal(_('svn client required. Please install subversion first.'))
  97. # probably test here if we have "make" and "install" programs as well. how about gcc?
  98. def expand_module_class_name(c):
  99. name = { 'd' : 'display',
  100. 'db' : 'database',
  101. 'g' : 'general',
  102. 'i' : 'imagery',
  103. 'm' : 'misc',
  104. 'ps' : 'postscript',
  105. 'p' : 'paint',
  106. 'r' : 'raster',
  107. 'r3' : 'raster3D',
  108. 's' : 'sites',
  109. 'v' : 'vector' }
  110. if name.has_key(c):
  111. return name[c]
  112. return c
  113. def list_available_modules(svnurl, full = False, shell = False):
  114. grass.message(_('Fetching list of modules from GRASS-Addons SVN (be patient)...'))
  115. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  116. i = 0
  117. prefix = ['d', 'db', 'g', 'i', 'ps',
  118. 'p', 'r', 'r3', 'v']
  119. nprefix = len(prefix)
  120. for d in prefix:
  121. if shell:
  122. grass.percent(i, nprefix, 1)
  123. i += 1
  124. modclass = expand_module_class_name(d)
  125. url = svnurl + '/' + modclass
  126. f = urllib.urlopen(url)
  127. if not f:
  128. grass.warning(_("Unable to fetch '%s'") % url)
  129. continue
  130. for line in f.readlines():
  131. # list modules
  132. sline = pattern.search(line)
  133. if sline and sline.group(2).split('.', 1)[0] == d:
  134. name = sline.group(2).rstrip('/')
  135. print_module_desc(name, url, full, shell)
  136. if shell:
  137. grass.percent(1, 1, 1)
  138. def print_module_desc(name, url, full = False, shell = False):
  139. if not full and not shell:
  140. print name
  141. return
  142. if shell:
  143. print 'name=' + name
  144. # check main.c first
  145. desc = get_module_desc(url + '/' + name + '/' + name)
  146. if not desc:
  147. desc = get_module_desc(url + '/' + name + '/main.c', script = False)
  148. if not desc:
  149. if not shell:
  150. print name + '-'
  151. return
  152. if shell:
  153. print 'description=' + desc.get('description', '')
  154. print 'keywords=' + ','.join(desc.get('keywords', list()))
  155. else:
  156. print name + ' - ' + desc.get('description', '')
  157. def get_module_desc(url, script = True):
  158. grass.debug('url=%s' % url)
  159. f = urllib.urlopen(url)
  160. if script:
  161. ret = get_module_script(f)
  162. else:
  163. ret = get_module_main(f)
  164. return ret
  165. def get_module_main(f):
  166. if not f:
  167. return dict()
  168. ret = { 'keyword' : list() }
  169. pattern = re.compile(r'(module.*->)(.+)(=)(.*)', re.IGNORECASE)
  170. keyword = re.compile(r'(G_add_keyword\()(.+)(\);)', re.IGNORECASE)
  171. key = ''
  172. value = ''
  173. for line in f.readlines():
  174. line = line.strip()
  175. find = pattern.search(line)
  176. if find:
  177. key = find.group(2).strip()
  178. line = find.group(4).strip()
  179. else:
  180. find = keyword.search(line)
  181. if find:
  182. ret['keyword'].append(find.group(2).replace('"', '').replace('_(', '').replace(')', ''))
  183. if key:
  184. value += line
  185. if line[-2:] == ');':
  186. value = value.replace('"', '').replace('_(', '').replace(');', '')
  187. if key == 'keywords':
  188. ret[key] = map(lambda x: x.strip(), value.split(','))
  189. else:
  190. ret[key] = value
  191. key = value = ''
  192. return ret
  193. def get_module_script(f):
  194. ret = dict()
  195. if not f:
  196. return ret
  197. begin = re.compile(r'#%.*module', re.IGNORECASE)
  198. end = re.compile(r'#%.*end', re.IGNORECASE)
  199. mline = None
  200. for line in f.readlines():
  201. if not mline:
  202. mline = begin.search(line)
  203. if mline:
  204. if end.search(line):
  205. break
  206. try:
  207. key, value = line.split(':', 1)
  208. key = key.replace('#%', '').strip()
  209. value = value.strip()
  210. if key == 'keywords':
  211. ret[key] = map(lambda x: x.strip(), value.split(','))
  212. else:
  213. ret[key] = value
  214. except ValueError:
  215. pass
  216. return ret
  217. def cleanup():
  218. global tmpdir, remove_tmpdir
  219. if remove_tmpdir:
  220. grass.try_rmdir(tmpdir)
  221. else:
  222. grass.info(_("Path to the source code: '%s'") % tmpdir)
  223. def install_extension(svnurl, prefix, module):
  224. gisbase = os.getenv('GISBASE')
  225. if not gisbase:
  226. grass.fatal(_('$GISBASE not defined'))
  227. if grass.find_program(module):
  228. grass.warning(_("Extension '%s' already installed. Will be updated...") % module)
  229. classchar = module.split('.', 1)[0]
  230. moduleclass = expand_module_class_name(classchar)
  231. url = svnurl + '/' + moduleclass + '/' + module
  232. grass.message(_("Fetching '%s' from GRASS-Addons SVN (be patient)...") % module)
  233. global tmpdir
  234. os.chdir(tmpdir)
  235. if grass.call(['svn', 'checkout',
  236. url]) != 0:
  237. grass.fatal(_("GRASS Addons '%s' not found in repository") % module)
  238. os.chdir(os.path.join(tmpdir, module))
  239. if os.path.exists(os.path.join(tmpdir, module, 'grass7.patch')):
  240. grass.message(_("Patch for GRASS 7 detected. Applying..."))
  241. if not grass.find_program('patch'):
  242. grass.fatal(_("Program 'patch' required. Exiting."))
  243. stdin = open(os.path.join(tmpdir, module, 'grass7.patch'))
  244. grass.call(['patch',
  245. '-p0'],
  246. stdin = stdin)
  247. # rename manual page
  248. os.rename('description.html', module + '.html')
  249. grass.verbose(_("Manual renamed from 'description.html' to '%s.html'") % module)
  250. grass.message(_("Compiling '%s'...") % module)
  251. if grass.call(['make',
  252. 'MODULE_TOPDIR=%s' % gisbase]) != 0:
  253. grass.fatal(_('Compilation failed, sorry. Please check above error messages.'))
  254. grass.message(_("Installing '%s'...") % module)
  255. # can we write ?
  256. try:
  257. # replace with something better
  258. file = os.path.join(prefix, 'test')
  259. f = open(file, "w")
  260. f.close()
  261. os.remove(file)
  262. ret = grass.call(['make',
  263. 'MODULE_TOPDIR=%s' % gisbase,
  264. 'INST_DIR=%s' % prefix,
  265. 'install'])
  266. except IOError:
  267. ret = grass.call(['sudo', 'make',
  268. 'MODULE_TOPDIR=%s' % gisbase,
  269. 'INST_DIR=%s' % prefix,
  270. 'install'])
  271. if ret != 0:
  272. grass.warning(_('Installation failed, sorry. Please check above error messages.'))
  273. else:
  274. grass.message(_("Installation of '%s' successfully finished.") % module)
  275. def remove_extension(prefix, module):
  276. # is module available?
  277. if not os.path.exists(os.path.join(prefix, 'bin', module)):
  278. grass.fatal(_("Module '%s' not found") % module)
  279. for file in [os.path.join(prefix, 'bin', module),
  280. os.path.join(prefix, 'scripts', module),
  281. os.path.join(prefix, 'docs', 'html', module + '.html')]:
  282. if os.path.isfile(file):
  283. os.remove(file)
  284. grass.message(_("'%s' successfully uninstalled.") % module)
  285. def update_menu(menuitem, module, operation):
  286. grass.warning(_('Not implemented'))
  287. if operation == 'add':
  288. pass
  289. else: # remove
  290. pass
  291. def main():
  292. # check dependecies
  293. check()
  294. # list available modules
  295. if flags['l'] or flags['f'] or flags['g']:
  296. list_available_modules(options['svnurl'], full = flags['f'], shell = flags['g'])
  297. return 0
  298. else:
  299. if not options['extension']:
  300. grass.fatal(_('You need to define an extension name or use -l'))
  301. # TODO: check more variable
  302. if '$(HOME)' in options['prefix']:
  303. options['prefix'] = options['prefix'].replace('$(HOME)', os.environ['HOME'])
  304. if not os.path.isdir(options['prefix']):
  305. try:
  306. os.makedirs(options['prefix'])
  307. except OSError, e:
  308. grass.fatal(_("Unable to create '%s'\n%s") % (options['prefix'], e))
  309. grass.warning(_("'%s' created") % options['prefix'])
  310. if flags['d']:
  311. if options['operation'] != 'add':
  312. grass.warning(_("Flag 'd' is relevant only to 'operation=add'. Ignoring this flag."))
  313. else:
  314. global remove_tmpdir
  315. remove_tmpdir = False
  316. if options['operation'] == 'add':
  317. install_extension(options['svnurl'], options['prefix'], options['extension'])
  318. else: # remove
  319. remove_extension(options['prefix'], options['extension'])
  320. if options['menuitem']:
  321. update_menu(options['menuitem'], options['extension'], options['operation'])
  322. return 0
  323. if __name__ == "__main__":
  324. options, flags = grass.parser()
  325. atexit.register(cleanup)
  326. sys.exit(main())