g.extension.py 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897
  1. #!/usr/bin/env python
  2. ############################################################################
  3. #
  4. # MODULE: g.extension
  5. # AUTHOR(S): Markus Neteler (original shell script)
  6. # Martin Landa <landa.martin gmail com> (Pythonized & upgraded for GRASS 7)
  7. # Vaclav Petras <wenzeslaus gmail com> (support for general sources)
  8. # PURPOSE: Tool to download and install extensions into local installation
  9. #
  10. # COPYRIGHT: (C) 2009-2019 by Markus Neteler, and the GRASS Development Team
  11. #
  12. # This program is free software under the GNU General
  13. # Public License (>=v2). Read the file COPYING that
  14. # comes with GRASS for details.
  15. #
  16. # TODO: - add sudo support where needed (i.e. check first permission to write into
  17. # $GISBASE directory)
  18. # - fix toolbox support in install_private_extension_xml()
  19. #############################################################################
  20. #%module
  21. #% label: Maintains GRASS Addons extensions in local GRASS installation.
  22. #% description: Downloads and installs extensions from GRASS Addons repository or other source into the local GRASS installation or removes installed extensions.
  23. #% keyword: general
  24. #% keyword: installation
  25. #% keyword: extensions
  26. #% keyword: addons
  27. #% keyword: download
  28. #%end
  29. #%option
  30. #% key: extension
  31. #% type: string
  32. #% key_desc: name
  33. #% label: Name of extension to install or remove
  34. #% description: Name of toolbox (set of extensions) when -t flag is given
  35. #% required: yes
  36. #%end
  37. #%option
  38. #% key: operation
  39. #% type: string
  40. #% description: Operation to be performed
  41. #% required: yes
  42. #% options: add,remove
  43. #% answer: add
  44. #%end
  45. #%option
  46. #% key: url
  47. #% type: string
  48. #% key_desc: url
  49. #% label: URL or directory to get the extension from (supported only on Linux and Mac)
  50. #% description: The official repository is used by default. User can specify a ZIP file, directory or a repository on common hosting services. If not identified, Subversion repository is assumed. See manual for all options.
  51. #%end
  52. #%option
  53. #% key: prefix
  54. #% type: string
  55. #% key_desc: path
  56. #% description: Prefix where to install extension (ignored when flag -s is given)
  57. #% answer: $GRASS_ADDON_BASE
  58. #% required: no
  59. #%end
  60. #%option
  61. #% key: proxy
  62. #% type: string
  63. #% key_desc: proxy
  64. #% description: Set the proxy with: "http=<value>,ftp=<value>"
  65. #% required: no
  66. #% multiple: yes
  67. #%end
  68. #%flag
  69. #% key: l
  70. #% description: List available extensions in the official GRASS GIS Addons repository
  71. #% guisection: Print
  72. #% suppress_required: yes
  73. #%end
  74. #%flag
  75. #% key: c
  76. #% description: List available extensions in the official GRASS GIS Addons repository including module description
  77. #% guisection: Print
  78. #% suppress_required: yes
  79. #%end
  80. #%flag
  81. #% key: g
  82. #% description: List available extensions in the official GRASS GIS Addons repository (shell script style)
  83. #% guisection: Print
  84. #% suppress_required: yes
  85. #%end
  86. #%flag
  87. #% key: a
  88. #% description: List locally installed extensions
  89. #% guisection: Print
  90. #% suppress_required: yes
  91. #%end
  92. #%flag
  93. #% key: s
  94. #% description: Install system-wide (may need system administrator rights)
  95. #% guisection: Install
  96. #%end
  97. #%flag
  98. #% key: d
  99. #% description: Download source code and exit
  100. #% guisection: Install
  101. #%end
  102. #%flag
  103. #% key: i
  104. #% description: Do not install new extension, just compile it
  105. #% guisection: Install
  106. #%end
  107. #%flag
  108. #% key: f
  109. #% description: Force removal when uninstalling extension (operation=remove)
  110. #% guisection: Remove
  111. #%end
  112. #%flag
  113. #% key: t
  114. #% description: Operate on toolboxes instead of single modules (experimental)
  115. #% suppress_required: yes
  116. #%end
  117. #%rules
  118. #% required: extension, -l, -c, -g, -a
  119. #% exclusive: extension, -l, -c, -g
  120. #% exclusive: extension, -l, -c, -a
  121. #%end
  122. # TODO: solve addon-extension(-module) confusion
  123. from __future__ import print_function
  124. import os
  125. import sys
  126. import re
  127. import atexit
  128. import shutil
  129. import zipfile
  130. import tempfile
  131. import xml.etree.ElementTree as etree
  132. from distutils.dir_util import copy_tree
  133. from six.moves.urllib.request import urlopen, urlretrieve, ProxyHandler, build_opener, install_opener
  134. from six.moves.urllib.error import HTTPError, URLError
  135. # Get the XML parsing exceptions to catch. The behavior changed with Python 2.7
  136. # and ElementTree 1.3.
  137. from xml.parsers import expat # TODO: works for any Python?
  138. if hasattr(etree, 'ParseError'):
  139. ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
  140. else:
  141. ETREE_EXCEPTIONS = (expat.ExpatError)
  142. import grass.script as gscript
  143. from grass.script.utils import try_rmdir
  144. from grass.script import core as grass
  145. from grass.script import task as gtask
  146. # temp dir
  147. REMOVE_TMPDIR = True
  148. PROXIES = {}
  149. def etree_fromfile(filename):
  150. """Create XML element tree from a given file name"""
  151. with open(filename, 'r') as file_:
  152. return etree.fromstring(file_.read())
  153. def etree_fromurl(url):
  154. """Create XML element tree from a given URL"""
  155. file_ = urlopen(url)
  156. return etree.fromstring(file_.read())
  157. def check_progs():
  158. """Check if the necessary programs are available"""
  159. # TODO: we need svn for the Subversion repo downloads
  160. # also git would be tested once supported
  161. for prog in ('make', 'gcc'):
  162. if not grass.find_program(prog, '--help'):
  163. grass.fatal(_("'%s' required. Please install '%s' first.")
  164. % (prog, prog))
  165. # expand prefix to class name
  166. def expand_module_class_name(class_letters):
  167. """Convert module class (family) letter or letters to class (family) name
  168. The letter or letters are used in module names, e.g. r.slope.aspect.
  169. The names are used in directories in Addons but also in the source code.
  170. >>> expand_module_class_name('r')
  171. 'raster'
  172. >>> expand_module_class_name('v')
  173. 'vector'
  174. """
  175. name = {
  176. 'd': 'display',
  177. 'db': 'database',
  178. 'g': 'general',
  179. 'i': 'imagery',
  180. 'm': 'misc',
  181. 'ps': 'postscript',
  182. 'p': 'paint',
  183. 'r': 'raster',
  184. 'r3': 'raster3d',
  185. 's': 'sites',
  186. 't': 'temporal',
  187. 'v': 'vector',
  188. 'wx': 'gui/wxpython'
  189. }
  190. return name.get(class_letters, class_letters)
  191. def get_module_class_name(module_name):
  192. """Return class (family) name for a module
  193. The names are used in directories in Addons but also in the source code.
  194. >>> get_module_class_name('r.slope.aspect')
  195. 'raster'
  196. >>> get_module_class_name('v.to.rast')
  197. 'vector'
  198. """
  199. classchar = module_name.split('.', 1)[0]
  200. return expand_module_class_name(classchar)
  201. def get_installed_extensions(force=False):
  202. """Get list of installed extensions or toolboxes (if -t is set)"""
  203. if flags['t']:
  204. return get_installed_toolboxes(force)
  205. return get_installed_modules(force)
  206. def list_installed_extensions(toolboxes=False):
  207. """List installed extensions"""
  208. elist = get_installed_extensions()
  209. if elist:
  210. if toolboxes:
  211. grass.message(_("List of installed extensions (toolboxes):"))
  212. else:
  213. grass.message(_("List of installed extensions (modules):"))
  214. sys.stdout.write('\n'.join(elist))
  215. sys.stdout.write('\n')
  216. else:
  217. if toolboxes:
  218. grass.info(_("No extension (toolbox) installed"))
  219. else:
  220. grass.info(_("No extension (module) installed"))
  221. def get_installed_toolboxes(force=False):
  222. """Get list of installed toolboxes
  223. Writes toolboxes file if it does not exist.
  224. Creates a new toolboxes file if it is not possible
  225. to read the current one.
  226. """
  227. xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
  228. if not os.path.exists(xml_file):
  229. write_xml_toolboxes(xml_file)
  230. # read XML file
  231. try:
  232. tree = etree_fromfile(xml_file)
  233. except ETREE_EXCEPTIONS + (OSError, IOError):
  234. os.remove(xml_file)
  235. write_xml_toolboxes(xml_file)
  236. return []
  237. ret = list()
  238. for tnode in tree.findall('toolbox'):
  239. ret.append(tnode.get('code'))
  240. return ret
  241. def get_installed_modules(force=False):
  242. """Get list of installed modules.
  243. Writes modules file if it does not exist and *force* is set to ``True``.
  244. Creates a new modules file if it is not possible
  245. to read the current one.
  246. """
  247. xml_file = os.path.join(options['prefix'], 'modules.xml')
  248. if not os.path.exists(xml_file):
  249. if force:
  250. write_xml_modules(xml_file)
  251. else:
  252. grass.debug("No addons metadata file available", 1)
  253. return []
  254. # read XML file
  255. try:
  256. tree = etree_fromfile(xml_file)
  257. except ETREE_EXCEPTIONS + (OSError, IOError):
  258. os.remove(xml_file)
  259. write_xml_modules(xml_file)
  260. return []
  261. ret = list()
  262. for tnode in tree.findall('task'):
  263. if flags['g']:
  264. desc, keyw = get_optional_params(tnode)
  265. ret.append('name={0}'.format(tnode.get('name').strip()))
  266. ret.append('description={0}'.format(desc))
  267. ret.append('keywords={0}'.format(keyw))
  268. ret.append('executables={0}'.format(','.join(
  269. get_module_executables(tnode))
  270. ))
  271. else:
  272. ret.append(tnode.get('name').strip())
  273. return ret
  274. # list extensions (read XML file from grass.osgeo.org/addons)
  275. def list_available_extensions(url):
  276. """List available extensions/modules or toolboxes (if -t is given)
  277. For toolboxes it lists also all modules.
  278. """
  279. gscript.debug("list_available_extensions(url={0})".format(url))
  280. if flags['t']:
  281. grass.message(_("List of available extensions (toolboxes):"))
  282. tlist = get_available_toolboxes(url)
  283. for toolbox_code, toolbox_data in tlist.items():
  284. if flags['g']:
  285. print('toolbox_name=' + toolbox_data['name'])
  286. print('toolbox_code=' + toolbox_code)
  287. else:
  288. print('%s (%s)' % (toolbox_data['name'], toolbox_code))
  289. if flags['c'] or flags['g']:
  290. list_available_modules(url, toolbox_data['modules'])
  291. else:
  292. if toolbox_data['modules']:
  293. print(os.linesep.join(['* ' + x for x in toolbox_data['modules']]))
  294. else:
  295. grass.message(_("List of available extensions (modules):"))
  296. list_available_modules(url)
  297. def get_available_toolboxes(url):
  298. """Return toolboxes available in the repository"""
  299. tdict = dict()
  300. url = url + "toolboxes.xml"
  301. try:
  302. tree = etree_fromurl(url)
  303. for tnode in tree.findall('toolbox'):
  304. mlist = list()
  305. clist = list()
  306. tdict[tnode.get('code')] = {'name': tnode.get('name'),
  307. 'correlate': clist,
  308. 'modules': mlist}
  309. for cnode in tnode.findall('correlate'):
  310. clist.append(cnode.get('name'))
  311. for mnode in tnode.findall('task'):
  312. mlist.append(mnode.get('name'))
  313. except (HTTPError, IOError, OSError):
  314. grass.fatal(_("Unable to fetch addons metadata file"))
  315. return tdict
  316. def get_toolbox_modules(url, name):
  317. """Get modules inside a toolbox in toolbox file at given URL
  318. :param url: URL of the directory (file name will be attached)
  319. :param name: toolbox name
  320. """
  321. tlist = list()
  322. url = url + "toolboxes.xml"
  323. try:
  324. tree = etree_fromurl(url)
  325. for tnode in tree.findall('toolbox'):
  326. if name == tnode.get('code'):
  327. for mnode in tnode.findall('task'):
  328. tlist.append(mnode.get('name'))
  329. break
  330. except (HTTPError, IOError, OSError):
  331. grass.fatal(_("Unable to fetch addons metadata file"))
  332. return tlist
  333. def get_module_files(mnode):
  334. """Return list of module files
  335. :param mnode: XML node for a module
  336. """
  337. flist = []
  338. for file_node in mnode.find('binary').findall('file'):
  339. filepath = file_node.text
  340. flist.append(filepath)
  341. return flist
  342. def get_module_executables(mnode):
  343. """Return list of module executables
  344. :param mnode: XML node for a module
  345. """
  346. flist = []
  347. for filepath in get_module_files(mnode):
  348. if filepath.startswith(options['prefix'] + os.path.sep + 'bin') or \
  349. (sys.platform != 'win32' and
  350. filepath.startswith(options['prefix'] + os.path.sep + 'scripts')):
  351. filename = os.path.basename(filepath)
  352. if sys.platform == 'win32':
  353. filename = os.path.splitext(filename)[0]
  354. flist.append(filename)
  355. return flist
  356. def get_optional_params(mnode):
  357. """Return description and keywords as a tuple
  358. :param mnode: XML node for a module
  359. """
  360. try:
  361. desc = mnode.find('description').text
  362. except AttributeError:
  363. desc = ''
  364. if desc is None:
  365. desc = ''
  366. try:
  367. keyw = mnode.find('keywords').text
  368. except AttributeError:
  369. keyw = ''
  370. if keyw is None:
  371. keyw = ''
  372. return desc, keyw
  373. def list_available_modules(url, mlist=None):
  374. """List modules available in the repository
  375. Tries to use XML metadata file first. Fallbacks to HTML page with a list.
  376. :param url: URL of the directory (file name will be attached)
  377. :param mlist: list only modules in this list
  378. """
  379. file_url = url + "modules.xml"
  380. grass.debug("url=%s" % file_url, 1)
  381. try:
  382. tree = etree_fromurl(file_url)
  383. except ETREE_EXCEPTIONS:
  384. grass.warning(_("Unable to parse '%s'. Trying to scan"
  385. " SVN repository (may take some time)...") % file_url)
  386. list_available_extensions_svn(url)
  387. return
  388. except (HTTPError, URLError, IOError, OSError):
  389. list_available_extensions_svn(url)
  390. return
  391. for mnode in tree.findall('task'):
  392. name = mnode.get('name').strip()
  393. if mlist and name not in mlist:
  394. continue
  395. if flags['c'] or flags['g']:
  396. desc, keyw = get_optional_params(mnode)
  397. if flags['g']:
  398. print('name=' + name)
  399. print('description=' + desc)
  400. print('keywords=' + keyw)
  401. elif flags['c']:
  402. if mlist:
  403. print('*', end='')
  404. print(name + ' - ' + desc)
  405. else:
  406. print(name)
  407. # TODO: this is now broken/dead code, SVN is basically not used
  408. # fallback for Trac should parse Trac HTML page
  409. # this might be useful for potential SVN repos or anything
  410. # which would list the extensions/addons as list
  411. # TODO: fail when nothing is accessible
  412. def list_available_extensions_svn(url):
  413. """List available extensions from HTML given by URL
  414. Filename is generated based on the module class/family.
  415. This works well for the structure which is in grass-addons repository.
  416. ``<li><a href=...`` is parsed to find module names.
  417. This works well for HTML page generated by Subversion.
  418. :param url: a directory URL (filename will be attached)
  419. """
  420. gscript.debug("list_available_extensions_svn(url=%s)" % url, 2)
  421. grass.message(_('Fetching list of extensions from'
  422. ' GRASS-Addons SVN repository (be patient)...'))
  423. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  424. if flags['c']:
  425. grass.warning(
  426. _("Flag 'c' ignored, addons metadata file not available"))
  427. if flags['g']:
  428. grass.warning(
  429. _("Flag 'g' ignored, addons metadata file not available"))
  430. prefixes = ['d', 'db', 'g', 'i', 'm', 'ps',
  431. 'p', 'r', 'r3', 's', 't', 'v']
  432. for prefix in prefixes:
  433. modclass = expand_module_class_name(prefix)
  434. grass.verbose(_("Checking for '%s' modules...") % modclass)
  435. # construct a full URL of a file
  436. file_url = '%s/%s' % (url, modclass)
  437. grass.debug("url = %s" % file_url, debug=2)
  438. try:
  439. file_ = urlopen(url)
  440. except (HTTPError, IOError, OSError):
  441. grass.debug(_("Unable to fetch '%s'") % file_url, debug=1)
  442. continue
  443. for line in file_.readlines():
  444. # list extensions
  445. sline = pattern.search(line)
  446. if not sline:
  447. continue
  448. name = sline.group(2).rstrip('/')
  449. if name.split('.', 1)[0] == prefix:
  450. print(name)
  451. # get_wxgui_extensions(url)
  452. # TODO: this is a dead code, not clear why not used, but seems not needed
  453. def get_wxgui_extensions(url):
  454. """Return list of extensions/addons in wxGUI directory at given URL
  455. :param url: a directory URL (filename will be attached)
  456. """
  457. mlist = list()
  458. grass.debug('Fetching list of wxGUI extensions from '
  459. 'GRASS-Addons SVN repository (be patient)...')
  460. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  461. grass.verbose(_("Checking for '%s' modules...") % 'gui/wxpython')
  462. # construct a full URL of a file
  463. url = '%s/%s' % (url, 'gui/wxpython')
  464. grass.debug("url = %s" % url, debug=2)
  465. file_ = urlopen(url)
  466. if not file_:
  467. grass.warning(_("Unable to fetch '%s'") % url)
  468. return
  469. for line in file.readlines():
  470. # list extensions
  471. sline = pattern.search(line)
  472. if not sline:
  473. continue
  474. name = sline.group(2).rstrip('/')
  475. if name not in ('..', 'Makefile'):
  476. mlist.append(name)
  477. return mlist
  478. def cleanup():
  479. """Cleanup after the downloads and copilation"""
  480. if REMOVE_TMPDIR:
  481. try_rmdir(TMPDIR)
  482. else:
  483. grass.message("\n%s\n" % _("Path to the source code:"))
  484. sys.stderr.write('%s\n' % os.path.join(TMPDIR, options['extension']))
  485. def write_xml_modules(name, tree=None):
  486. """Write element tree as a modules matadata file
  487. If the *tree* is not given, an empty file is created.
  488. :param name: file name
  489. :param tree: XML element tree
  490. """
  491. file_ = open(name, 'w')
  492. file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  493. file_.write('<!DOCTYPE task SYSTEM "grass-addons.dtd">\n')
  494. file_.write('<addons version="%s">\n' % version[0])
  495. libgis_revison = grass.version()['libgis_revision']
  496. if tree is not None:
  497. for tnode in tree.findall('task'):
  498. indent = 4
  499. file_.write('%s<task name="%s">\n' %
  500. (' ' * indent, tnode.get('name')))
  501. indent += 4
  502. file_.write('%s<description>%s</description>\n' %
  503. (' ' * indent, tnode.find('description').text))
  504. file_.write('%s<keywords>%s</keywords>\n' %
  505. (' ' * indent, tnode.find('keywords').text))
  506. bnode = tnode.find('binary')
  507. if bnode is not None:
  508. file_.write('%s<binary>\n' % (' ' * indent))
  509. indent += 4
  510. for fnode in bnode.findall('file'):
  511. file_.write('%s<file>%s</file>\n' %
  512. (' ' * indent, os.path.join(options['prefix'],
  513. fnode.text)))
  514. indent -= 4
  515. file_.write('%s</binary>\n' % (' ' * indent))
  516. file_.write('%s<libgis revision="%s" />\n' %
  517. (' ' * indent, libgis_revison))
  518. indent -= 4
  519. file_.write('%s</task>\n' % (' ' * indent))
  520. file_.write('</addons>\n')
  521. file_.close()
  522. def write_xml_toolboxes(name, tree=None):
  523. """Write element tree as a toolboxes matadata file
  524. If the *tree* is not given, an empty file is created.
  525. :param name: file name
  526. :param tree: XML element tree
  527. """
  528. file_ = open(name, 'w')
  529. file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  530. file_.write('<!DOCTYPE toolbox SYSTEM "grass-addons.dtd">\n')
  531. file_.write('<addons version="%s">\n' % version[0])
  532. if tree is not None:
  533. for tnode in tree.findall('toolbox'):
  534. indent = 4
  535. file_.write('%s<toolbox name="%s" code="%s">\n' %
  536. (' ' * indent, tnode.get('name'), tnode.get('code')))
  537. indent += 4
  538. for cnode in tnode.findall('correlate'):
  539. file_.write('%s<correlate code="%s" />\n' %
  540. (' ' * indent, tnode.get('code')))
  541. for mnode in tnode.findall('task'):
  542. file_.write('%s<task name="%s" />\n' %
  543. (' ' * indent, mnode.get('name')))
  544. indent -= 4
  545. file_.write('%s</toolbox>\n' % (' ' * indent))
  546. file_.write('</addons>\n')
  547. file_.close()
  548. def install_extension(source, url, xmlurl):
  549. """Install extension (e.g. one module) or a toolbox (list of modules)"""
  550. gisbase = os.getenv('GISBASE')
  551. if not gisbase:
  552. grass.fatal(_('$GISBASE not defined'))
  553. if options['extension'] in get_installed_extensions(force=True):
  554. grass.warning(_("Extension <%s> already installed. Re-installing...") %
  555. options['extension'])
  556. if flags['t']:
  557. grass.message(_("Installing toolbox <%s>...") % options['extension'])
  558. mlist = get_toolbox_modules(xmlurl, options['extension'])
  559. else:
  560. mlist = [options['extension']]
  561. if not mlist:
  562. grass.warning(_("Nothing to install"))
  563. return
  564. ret = 0
  565. for module in mlist:
  566. if sys.platform == "win32":
  567. ret += install_extension_win(module)
  568. else:
  569. ret1, installed_modules, tmp_dir = install_extension_std_platforms(module,
  570. source=source, url=url)
  571. ret += ret1
  572. if len(mlist) > 1:
  573. print('-' * 60)
  574. if flags['d']:
  575. return
  576. if ret != 0:
  577. grass.warning(_('Installation failed, sorry.'
  578. ' Please check above error messages.'))
  579. else:
  580. # for now it is reasonable to assume that only official source
  581. # will provide the metadata file
  582. if source == 'official' and len(installed_modules) <= len(mlist):
  583. grass.message(_("Updating addons metadata file..."))
  584. blist = install_extension_xml(xmlurl, mlist)
  585. if source == 'official' and len(installed_modules) > len(mlist):
  586. grass.message(_("Updating addons metadata file..."))
  587. blist = install_private_extension_xml(tmp_dir, installed_modules)
  588. else:
  589. grass.message(_("Updating private addons metadata file..."))
  590. if len(installed_modules) > 1:
  591. blist = install_private_extension_xml(tmp_dir, installed_modules)
  592. else:
  593. blist = install_private_extension_xml(tmp_dir, mlist)
  594. # the blist was used here, but it seems that it is the same as mlist
  595. for module in mlist:
  596. update_manual_page(module)
  597. grass.message(_("Installation of <%s> successfully finished") %
  598. options['extension'])
  599. if not os.getenv('GRASS_ADDON_BASE'):
  600. grass.warning(_('This add-on module will not function until'
  601. ' you set the GRASS_ADDON_BASE environment'
  602. ' variable (see "g.manual variables")'))
  603. def get_toolboxes_metadata(url):
  604. """Return metadata for all toolboxes from given URL
  605. :param url: URL of a modules matadata file
  606. :param mlist: list of modules to get metadata for
  607. :returns: tuple where first item is dictionary with module names as keys
  608. and dictionary with dest, keyw, files keys as value, the second item
  609. is list of 'binary' files (installation files)
  610. """
  611. data = dict()
  612. try:
  613. tree = etree_fromurl(url)
  614. for tnode in tree.findall('toolbox'):
  615. clist = list()
  616. for cnode in tnode.findall('correlate'):
  617. clist.append(cnode.get('code'))
  618. mlist = list()
  619. for mnode in tnode.findall('task'):
  620. mlist.append(mnode.get('name'))
  621. code = tnode.get('code')
  622. data[code] = {
  623. 'name': tnode.get('name'),
  624. 'correlate': clist,
  625. 'modules': mlist,
  626. }
  627. except (HTTPError, IOError, OSError):
  628. grass.error(_("Unable to read addons metadata file "
  629. "from the remote server"))
  630. return data
  631. def install_toolbox_xml(url, name):
  632. """Update local toolboxes metadata file"""
  633. # read metadata from remote server (toolboxes)
  634. url = url + "toolboxes.xml"
  635. data = get_toolboxes_metadata(url)
  636. if not data:
  637. grass.warning(_("No addons metadata available"))
  638. return
  639. if name not in data:
  640. grass.warning(_("No addons metadata available for <%s>") % name)
  641. return
  642. xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
  643. # create an empty file if not exists
  644. if not os.path.exists(xml_file):
  645. write_xml_modules(xml_file)
  646. # read XML file
  647. with open(xml_file, 'r') as xml:
  648. tree = etree.fromstring(xml.read())
  649. # update tree
  650. tnode = None
  651. for node in tree.findall('toolbox'):
  652. if node.get('code') == name:
  653. tnode = node
  654. break
  655. tdata = data[name]
  656. if tnode is not None:
  657. # update existing node
  658. for cnode in tnode.findall('correlate'):
  659. tnode.remove(cnode)
  660. for mnode in tnode.findall('task'):
  661. tnode.remove(mnode)
  662. else:
  663. # create new node for task
  664. tnode = etree.Element(
  665. 'toolbox', attrib={'name': tdata['name'], 'code': name})
  666. tree.append(tnode)
  667. for cname in tdata['correlate']:
  668. cnode = etree.Element('correlate', attrib={'code': cname})
  669. tnode.append(cnode)
  670. for tname in tdata['modules']:
  671. mnode = etree.Element('task', attrib={'name': tname})
  672. tnode.append(mnode)
  673. write_xml_toolboxes(xml_file, tree)
  674. def get_addons_metadata(url, mlist):
  675. """Return metadata for list of modules from given URL
  676. :param url: URL of a modules matadata file
  677. :param mlist: list of modules to get metadata for
  678. :returns: tuple where first item is dictionary with module names as keys
  679. and dictionary with dest, keyw, files keys as value, the second item
  680. is list of 'binary' files (installation files)
  681. """
  682. data = {}
  683. bin_list = []
  684. try:
  685. tree = etree_fromurl(url)
  686. except (HTTPError, URLError, IOError, OSError) as error:
  687. grass.error(_("Unable to read addons metadata file"
  688. " from the remote server: {0}").format(error))
  689. return data, bin_list
  690. except ETREE_EXCEPTIONS as error:
  691. grass.warning(_("Unable to parse '%s': {0}").format(error) % url)
  692. return data, bin_list
  693. for mnode in tree.findall('task'):
  694. name = mnode.get('name')
  695. if name not in mlist:
  696. continue
  697. file_list = list()
  698. bnode = mnode.find('binary')
  699. windows = sys.platform == 'win32'
  700. if bnode is not None:
  701. for fnode in bnode.findall('file'):
  702. path = fnode.text.split('/')
  703. if path[0] == 'bin':
  704. bin_list.append(path[-1])
  705. if windows:
  706. path[-1] += '.exe'
  707. elif path[0] == 'scripts':
  708. bin_list.append(path[-1])
  709. if windows:
  710. path[-1] += '.py'
  711. file_list.append(os.path.sep.join(path))
  712. desc, keyw = get_optional_params(mnode)
  713. data[name] = {
  714. 'desc': desc,
  715. 'keyw': keyw,
  716. 'files': file_list,
  717. }
  718. return data, bin_list
  719. def install_extension_xml(url, mlist):
  720. """Update XML files with metadata about installed modules and toolbox
  721. Uses the remote/repository XML files for modules to obtain the metadata.
  722. :returns: list of executables (usable for ``update_manual_page()``)
  723. """
  724. if len(mlist) > 1:
  725. # read metadata from remote server (toolboxes)
  726. install_toolbox_xml(url, options['extension'])
  727. # read metadata from remote server (modules)
  728. url = url + "modules.xml"
  729. data, bin_list = get_addons_metadata(url, mlist)
  730. if not data:
  731. grass.warning(_("No addons metadata available."
  732. " Addons metadata file not updated."))
  733. return []
  734. xml_file = os.path.join(options['prefix'], 'modules.xml')
  735. # create an empty file if not exists
  736. if not os.path.exists(xml_file):
  737. write_xml_modules(xml_file)
  738. # read XML file
  739. tree = etree_fromfile(xml_file)
  740. # update tree
  741. for name in mlist:
  742. tnode = None
  743. for node in tree.findall('task'):
  744. if node.get('name') == name:
  745. tnode = node
  746. break
  747. if name not in data:
  748. grass.warning(_("No addons metadata found for <%s>") % name)
  749. continue
  750. ndata = data[name]
  751. if tnode is not None:
  752. # update existing node
  753. dnode = tnode.find('description')
  754. if dnode is not None:
  755. dnode.text = ndata['desc']
  756. knode = tnode.find('keywords')
  757. if knode is not None:
  758. knode.text = ndata['keyw']
  759. bnode = tnode.find('binary')
  760. if bnode is not None:
  761. tnode.remove(bnode)
  762. bnode = etree.Element('binary')
  763. for file_name in ndata['files']:
  764. fnode = etree.Element('file')
  765. fnode.text = file_name
  766. bnode.append(fnode)
  767. tnode.append(bnode)
  768. else:
  769. # create new node for task
  770. tnode = etree.Element('task', attrib={'name': name})
  771. dnode = etree.Element('description')
  772. dnode.text = ndata['desc']
  773. tnode.append(dnode)
  774. knode = etree.Element('keywords')
  775. knode.text = ndata['keyw']
  776. tnode.append(knode)
  777. bnode = etree.Element('binary')
  778. for file_name in ndata['files']:
  779. fnode = etree.Element('file')
  780. fnode.text = file_name
  781. bnode.append(fnode)
  782. tnode.append(bnode)
  783. tree.append(tnode)
  784. write_xml_modules(xml_file, tree)
  785. return bin_list
  786. def install_private_extension_xml(url, mlist):
  787. """Update XML files with metadata about installed modules and toolbox
  788. of an private addon
  789. """
  790. # TODO toolbox
  791. # if len(mlist) > 1:
  792. # # read metadata from remote server (toolboxes)
  793. # install_toolbox_xml(url, options['extension'])
  794. xml_file = os.path.join(options['prefix'], 'modules.xml')
  795. # create an empty file if not exists
  796. if not os.path.exists(xml_file):
  797. write_xml_modules(xml_file)
  798. # read XML file
  799. tree = etree_fromfile(xml_file)
  800. # update tree
  801. for name in mlist:
  802. try:
  803. desc = gtask.parse_interface(name).description
  804. # mname = gtask.parse_interface(name).name
  805. keywords = gtask.parse_interface(name).keywords
  806. except Exception as e:
  807. grass.warning(_("No addons metadata available."
  808. " Addons metadata file not updated."))
  809. return []
  810. tnode = None
  811. for node in tree.findall('task'):
  812. if node.get('name') == name:
  813. tnode = node
  814. break
  815. # create new node for task
  816. tnode = etree.Element('task', attrib={'name': name})
  817. dnode = etree.Element('description')
  818. dnode.text = desc
  819. tnode.append(dnode)
  820. knode = etree.Element('keywords')
  821. knode.text = (',').join(keywords)
  822. tnode.append(knode)
  823. # create binary
  824. bnode = etree.Element('binary')
  825. list_of_binary_files = []
  826. for file_name in os.listdir(url):
  827. file_type = os.path.splitext(file_name)[-1]
  828. file_n = os.path.splitext(file_name)[0]
  829. html_path = os.path.join(options['prefix'], 'docs', 'html')
  830. c_path = os.path.join(options['prefix'], 'bin')
  831. py_path = os.path.join(options['prefix'], 'scripts')
  832. # html or image file
  833. if file_type in ['.html', '.jpg', '.png'] \
  834. and file_n in os.listdir(html_path):
  835. list_of_binary_files.append(os.path.join(html_path, file_name))
  836. # c file
  837. elif file_type in ['.c'] and file_name in os.listdir(c_path):
  838. list_of_binary_files.append(os.path.join(c_path, file_n))
  839. # python file
  840. elif file_type in ['.py'] and file_name in os.listdir(py_path):
  841. list_of_binary_files.append(os.path.join(py_path, file_n))
  842. # man file
  843. man_path = os.path.join(options['prefix'], 'docs', 'man', 'man1')
  844. if name + '.1' in os.listdir(man_path):
  845. list_of_binary_files.append(os.path.join(man_path, name + '.1'))
  846. # add binaries to xml file
  847. for binary_file_name in list_of_binary_files:
  848. fnode = etree.Element('file')
  849. fnode.text = binary_file_name
  850. bnode.append(fnode)
  851. tnode.append(bnode)
  852. tree.append(tnode)
  853. write_xml_modules(xml_file, tree)
  854. return mlist
  855. def install_extension_win(name):
  856. """Install extension on MS Windows"""
  857. grass.message(_("Downloading precompiled GRASS Addons <%s>...") %
  858. options['extension'])
  859. # build base URL
  860. if build_platform == 'x86_64':
  861. platform = build_platform
  862. else:
  863. platform = 'x86'
  864. base_url = "http://wingrass.fsv.cvut.cz/" \
  865. "grass%(major)s%(minor)s/%(platform)s/addons/" \
  866. "grass-%(major)s.%(minor)s.%(patch)s" % \
  867. {'platform': platform,
  868. 'major': version[0], 'minor': version[1],
  869. 'patch': version[2]}
  870. # resolve ZIP URL
  871. source, url = resolve_source_code(url='{0}/{1}.zip'.format(base_url, name))
  872. # to hide non-error messages from subprocesses
  873. if grass.verbosity() <= 2:
  874. outdev = open(os.devnull, 'w')
  875. else:
  876. outdev = sys.stdout
  877. # download Addons ZIP file
  878. os.chdir(TMPDIR) # this is just to not leave something behind
  879. srcdir = os.path.join(TMPDIR, name)
  880. download_source_code(source=source, url=url, name=name,
  881. outdev=outdev, directory=srcdir, tmpdir=TMPDIR)
  882. # copy Addons copy tree to destination directory
  883. move_extracted_files(extract_dir=srcdir, target_dir=options['prefix'],
  884. files=os.listdir(srcdir))
  885. return 0
  886. def download_source_code_svn(url, name, outdev, directory=None):
  887. """Download source code from a Subversion reporsitory
  888. .. note:
  889. Stdout is passed to to *outdev* while stderr is will be just printed.
  890. :param url: URL of the repository
  891. (module class/family and name are attached)
  892. :param name: module name
  893. :param outdev: output divide for the standard output of the svn command
  894. :param directory: directory where the source code will be downloaded
  895. (default is the current directory with name attached)
  896. :returns: full path to the directory with the source code
  897. (useful when you not specify directory, if *directory* is specified
  898. the return value is equal to it)
  899. """
  900. if not directory:
  901. directory = os.path.join(os.getcwd, name)
  902. classchar = name.split('.', 1)[0]
  903. moduleclass = expand_module_class_name(classchar)
  904. url = url + '/' + moduleclass + '/' + name
  905. if grass.call(['svn', 'checkout',
  906. url, directory], stdout=outdev) != 0:
  907. grass.fatal(_("GRASS Addons <%s> not found") % name)
  908. return directory
  909. def move_extracted_files(extract_dir, target_dir, files):
  910. """Fix state of extracted file by moving them to different diretcory
  911. When extracting, it is not clear what will be the root directory
  912. or if there will be one at all. So this function moves the files to
  913. a different directory in the way that if there was one directory extracted,
  914. the contained files are moved.
  915. """
  916. gscript.debug("move_extracted_files({0})".format(locals()))
  917. if len(files) == 1:
  918. shutil.copytree(os.path.join(extract_dir, files[0]), target_dir)
  919. else:
  920. if not os.path.exists(target_dir):
  921. os.mkdir(target_dir)
  922. for file_name in files:
  923. actual_file = os.path.join(extract_dir, file_name)
  924. if os.path.isdir(actual_file):
  925. # shutil.copytree() replaced by copy_tree() because
  926. # shutil's copytree() fails when subdirectory exists
  927. copy_tree(actual_file,
  928. os.path.join(target_dir, file_name))
  929. else:
  930. shutil.copy(actual_file, os.path.join(target_dir, file_name))
  931. # Original copyright and license of the original version of the CRLF function
  932. # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010
  933. # Python Software Foundation; All Rights Reserved
  934. # Python Software Foundation License Version 2
  935. # http://svn.python.org/projects/python/trunk/Tools/scripts/crlf.py
  936. def fix_newlines(directory):
  937. """Replace CRLF with LF in all files in the directory
  938. Binary files are ignored. Recurses into subdirectories.
  939. """
  940. # skip binary files
  941. # see https://stackoverflow.com/a/7392391
  942. textchars = bytearray({7,8,9,10,12,13,27} | set(range(0x20, 0x100)) - {0x7f})
  943. is_binary_string = lambda bytes: bool(bytes.translate(None, textchars))
  944. for root, unused, files in os.walk(directory):
  945. for name in files:
  946. filename = os.path.join(root, name)
  947. if is_binary_string(open(filename, 'rb').read(1024)):
  948. continue # ignore binary files
  949. # read content of text file
  950. with open(filename, 'rb') as fd:
  951. data = fd.read()
  952. # we don't expect there would be CRLF file by
  953. # purpose if we want to allow CRLF files we would
  954. # have to whitelite .py etc
  955. newdata = data.replace(b'\r\n', b'\n')
  956. if newdata != data:
  957. with open(filename, 'wb') as newfile:
  958. newfile.write(newdata)
  959. def extract_zip(name, directory, tmpdir):
  960. """Extract a ZIP file into a directory"""
  961. gscript.debug("extract_zip(name={name}, directory={directory},"
  962. " tmpdir={tmpdir})".format(name=name, directory=directory,
  963. tmpdir=tmpdir), 3)
  964. try:
  965. zip_file = zipfile.ZipFile(name, mode='r')
  966. file_list = zip_file.namelist()
  967. # we suppose we can write to parent of the given dir
  968. # (supposing a tmp dir)
  969. extract_dir = os.path.join(tmpdir, 'extract_dir')
  970. os.mkdir(extract_dir)
  971. for subfile in file_list:
  972. # this should be safe in Python 2.7.4
  973. zip_file.extract(subfile, extract_dir)
  974. files = os.listdir(extract_dir)
  975. move_extracted_files(extract_dir=extract_dir,
  976. target_dir=directory, files=files)
  977. except zipfile.BadZipfile as error:
  978. gscript.fatal(_("ZIP file is unreadable: {0}").format(error))
  979. # TODO: solve the other related formats
  980. def extract_tar(name, directory, tmpdir):
  981. """Extract a TAR or a similar file into a directory"""
  982. gscript.debug("extract_tar(name={name}, directory={directory},"
  983. " tmpdir={tmpdir})".format(name=name, directory=directory,
  984. tmpdir=tmpdir), 3)
  985. try:
  986. import tarfile # we don't need it anywhere else
  987. tar = tarfile.open(name)
  988. extract_dir = os.path.join(tmpdir, 'extract_dir')
  989. os.mkdir(extract_dir)
  990. tar.extractall(path=extract_dir)
  991. files = os.listdir(extract_dir)
  992. move_extracted_files(extract_dir=extract_dir,
  993. target_dir=directory, files=files)
  994. except tarfile.TarError as error:
  995. gscript.fatal(_("Archive file is unreadable: {0}").format(error))
  996. extract_tar.supported_formats = ['tar.gz', 'gz', 'bz2', 'tar', 'gzip', 'targz']
  997. def download_source_code(source, url, name, outdev,
  998. directory=None, tmpdir=None):
  999. """Get source code to a local directory for compilation"""
  1000. gscript.verbose("Downloading source code for <{name}> from <{url}>"
  1001. " which is identified as '{source}' type of source..."
  1002. .format(source=source, url=url, name=name))
  1003. if source == 'svn':
  1004. download_source_code_svn(url, name, outdev, directory)
  1005. elif source in ['remote_zip', 'official']:
  1006. # we expect that the module.zip file is not by chance in the archive
  1007. zip_name = os.path.join(tmpdir, 'extension.zip')
  1008. try:
  1009. response = urlopen(url)
  1010. except URLError:
  1011. grass.fatal(_("Extension <%s> not found") % name)
  1012. with open(zip_name, 'wb') as out_file:
  1013. shutil.copyfileobj(response, out_file)
  1014. extract_zip(name=zip_name, directory=directory, tmpdir=tmpdir)
  1015. fix_newlines(directory)
  1016. elif source.startswith('remote_') and \
  1017. source.split('_')[1] in extract_tar.supported_formats:
  1018. # we expect that the module.tar.gz file is not by chance in the archive
  1019. archive_name = os.path.join(tmpdir,
  1020. 'extension.' + source.split('_')[1])
  1021. urlretrieve(url, archive_name)
  1022. extract_tar(name=archive_name, directory=directory, tmpdir=tmpdir)
  1023. fix_newlines(directory)
  1024. elif source == 'zip':
  1025. extract_zip(name=url, directory=directory, tmpdir=tmpdir)
  1026. fix_newlines(directory)
  1027. elif source in extract_tar.supported_formats:
  1028. extract_tar(name=url, directory=directory, tmpdir=tmpdir)
  1029. fix_newlines(directory)
  1030. elif source == 'dir':
  1031. shutil.copytree(url, directory)
  1032. fix_newlines(directory)
  1033. else:
  1034. # probably programmer error
  1035. grass.fatal(_("Unknown extension (addon) source type '{0}'."
  1036. " Please report this to the grass-user mailing list.")
  1037. .format(source))
  1038. assert os.path.isdir(directory)
  1039. def install_extension_std_platforms(name, source, url):
  1040. """Install extension on standard platforms"""
  1041. gisbase = os.getenv('GISBASE')
  1042. source_url = "https://trac.osgeo.org/grass/browser/grass-addons/grass7/"
  1043. if source == 'official':
  1044. gscript.message(_("Fetching <%s> from "
  1045. "GRASS GIS Addons repository (be patient)...") % name)
  1046. else:
  1047. gscript.message(_("Fetching <{name}> from "
  1048. "<{url}> (be patient)...").format(name=name, url=url))
  1049. # to hide non-error messages from subprocesses
  1050. if grass.verbosity() <= 2:
  1051. outdev = open(os.devnull, 'w')
  1052. else:
  1053. outdev = sys.stdout
  1054. os.chdir(TMPDIR) # this is just to not leave something behind
  1055. srcdir = os.path.join(TMPDIR, name)
  1056. download_source_code(source=source, url=url, name=name,
  1057. outdev=outdev, directory=srcdir, tmpdir=TMPDIR)
  1058. os.chdir(srcdir)
  1059. dirs = {
  1060. 'bin': os.path.join(TMPDIR, name, 'bin'),
  1061. 'docs': os.path.join(TMPDIR, name, 'docs'),
  1062. 'html': os.path.join(TMPDIR, name, 'docs', 'html'),
  1063. 'rest': os.path.join(TMPDIR, name, 'docs', 'rest'),
  1064. 'man': os.path.join(TMPDIR, name, 'docs', 'man'),
  1065. 'script': os.path.join(TMPDIR, name, 'scripts'),
  1066. # TODO: handle locales also for addons
  1067. # 'string' : os.path.join(TMPDIR, name, 'locale'),
  1068. 'string': os.path.join(TMPDIR, name),
  1069. 'etc': os.path.join(TMPDIR, name, 'etc'),
  1070. }
  1071. make_cmd = [
  1072. 'make',
  1073. 'MODULE_TOPDIR=%s' % gisbase.replace(' ', r'\ '),
  1074. 'RUN_GISRC=%s' % os.environ['GISRC'],
  1075. 'BIN=%s' % dirs['bin'],
  1076. 'HTMLDIR=%s' % dirs['html'],
  1077. 'RESTDIR=%s' % dirs['rest'],
  1078. 'MANBASEDIR=%s' % dirs['man'],
  1079. 'SCRIPTDIR=%s' % dirs['script'],
  1080. 'STRINGDIR=%s' % dirs['string'],
  1081. 'ETC=%s' % os.path.join(dirs['etc']),
  1082. 'SOURCE_URL=%s' % source_url
  1083. ]
  1084. install_cmd = [
  1085. 'make',
  1086. 'MODULE_TOPDIR=%s' % gisbase,
  1087. 'ARCH_DISTDIR=%s' % os.path.join(TMPDIR, name),
  1088. 'INST_DIR=%s' % options['prefix'],
  1089. 'install'
  1090. ]
  1091. if flags['d']:
  1092. grass.message("\n%s\n" % _("To compile run:"))
  1093. sys.stderr.write(' '.join(make_cmd) + '\n')
  1094. grass.message("\n%s\n" % _("To install run:"))
  1095. sys.stderr.write(' '.join(install_cmd) + '\n')
  1096. return 0
  1097. os.chdir(os.path.join(TMPDIR, name))
  1098. grass.message(_("Compiling..."))
  1099. if not os.path.exists(os.path.join(gisbase, 'include',
  1100. 'Make', 'Module.make')):
  1101. grass.fatal(_("Please install GRASS development package"))
  1102. if 0 != grass.call(make_cmd,
  1103. stdout=outdev):
  1104. grass.fatal(_('Compilation failed, sorry.'
  1105. ' Please check above error messages.'))
  1106. if flags['i']:
  1107. return 0
  1108. grass.message(_("Installing..."))
  1109. with open(os.path.join(TMPDIR, name, 'Makefile')) as f:
  1110. datafile = f.readlines()
  1111. makefile_part = ""
  1112. next_line = False
  1113. for line in datafile:
  1114. if 'SUBDIRS' in line or next_line:
  1115. makefile_part += line
  1116. if (line.strip()).endswith('\\'):
  1117. next_line = True
  1118. else:
  1119. next_line = False
  1120. modules = makefile_part.replace('SUBDIRS', '').replace('=', '').replace('\\', '').strip().split('\n')
  1121. c_path = os.path.join(options['prefix'], 'bin')
  1122. py_path = os.path.join(options['prefix'], 'scripts')
  1123. all_modules = os.listdir(c_path)
  1124. all_modules.extend(os.listdir(py_path))
  1125. module_list = [x.strip() for x in modules if x.strip() in all_modules]
  1126. return grass.call(install_cmd, stdout=outdev), module_list, os.path.join(TMPDIR, name)
  1127. def remove_extension(force=False):
  1128. """Remove existing extension (module or toolbox if -t is given)"""
  1129. if flags['t']:
  1130. mlist = get_toolbox_modules(options['prefix'], options['extension'])
  1131. else:
  1132. mlist = [options['extension']]
  1133. if force:
  1134. grass.verbose(_("List of removed files:"))
  1135. else:
  1136. grass.info(_("Files to be removed:"))
  1137. remove_modules(mlist, force)
  1138. if force:
  1139. grass.message(_("Updating addons metadata file..."))
  1140. remove_extension_xml(mlist)
  1141. grass.message(_("Extension <%s> successfully uninstalled.") %
  1142. options['extension'])
  1143. else:
  1144. grass.warning(_("Extension <%s> not removed. "
  1145. "Re-run '%s' with '-f' flag to force removal")
  1146. % (options['extension'], 'g.extension'))
  1147. # remove existing extension(s) (reading XML file)
  1148. def remove_modules(mlist, force=False):
  1149. """Remove extensions/modules specified in a list
  1150. Collects the file names from the file with metadata and fallbacks
  1151. to standard layout of files on prefix path on error.
  1152. """
  1153. # try to read XML metadata file first
  1154. xml_file = os.path.join(options['prefix'], 'modules.xml')
  1155. installed = get_installed_modules()
  1156. if os.path.exists(xml_file):
  1157. tree = etree_fromfile(xml_file)
  1158. else:
  1159. tree = None
  1160. for name in mlist:
  1161. if name not in installed:
  1162. # try even if module does not seem to be available,
  1163. # as the user may be trying to get rid of left over cruft
  1164. grass.warning(_("Extension <%s> not found") % name)
  1165. if tree is not None:
  1166. flist = []
  1167. for task in tree.findall('task'):
  1168. if name == task.get('name') and \
  1169. task.find('binary') is not None:
  1170. flist = get_module_files(task)
  1171. break
  1172. if flist:
  1173. removed = False
  1174. err = list()
  1175. for fpath in flist:
  1176. grass.verbose(fpath)
  1177. if force:
  1178. try:
  1179. os.remove(fpath)
  1180. removed = True
  1181. except OSError:
  1182. msg = "Unable to remove file '%s'"
  1183. err.append((_(msg) % fpath))
  1184. if force and not removed:
  1185. grass.fatal(_("Extension <%s> not found") % name)
  1186. if err:
  1187. for error_line in err:
  1188. grass.error(error_line)
  1189. else:
  1190. remove_extension_std(name, force)
  1191. else:
  1192. remove_extension_std(name, force)
  1193. # remove module libraries directories under GRASS_ADDONS/etc/{name}/*
  1194. libpath = os.path.join(options['prefix'], 'etc', name)
  1195. if os.path.isdir(libpath):
  1196. grass.verbose(libpath)
  1197. if force:
  1198. shutil.rmtree(libpath)
  1199. def remove_extension_std(name, force=False):
  1200. """Remove extension/module expecting the standard layout"""
  1201. for fpath in [os.path.join(options['prefix'], 'bin', name),
  1202. os.path.join(options['prefix'], 'scripts', name),
  1203. os.path.join(
  1204. options['prefix'], 'docs', 'html', name + '.html'),
  1205. os.path.join(
  1206. options['prefix'], 'docs', 'rest', name + '.txt'),
  1207. os.path.join(options['prefix'], 'docs', 'man', 'man1',
  1208. name + '.1')]:
  1209. if os.path.isfile(fpath):
  1210. grass.verbose(fpath)
  1211. if force:
  1212. os.remove(fpath)
  1213. # remove module libraries under GRASS_ADDONS/etc/{name}/*
  1214. libpath = os.path.join(options['prefix'], 'etc', name)
  1215. if os.path.isdir(libpath):
  1216. grass.verbose(libpath)
  1217. if force:
  1218. shutil.rmtree(libpath)
  1219. def remove_from_toolbox_xml(name):
  1220. """Update local meta-file when removing existing toolbox"""
  1221. xml_file = os.path.join(options['prefix'], 'toolboxes.xml')
  1222. if not os.path.exists(xml_file):
  1223. return
  1224. # read XML file
  1225. tree = etree_fromfile(xml_file)
  1226. for node in tree.findall('toolbox'):
  1227. if node.get('code') != name:
  1228. continue
  1229. tree.remove(node)
  1230. write_xml_toolboxes(xml_file, tree)
  1231. def remove_extension_xml(modules):
  1232. """Update local meta-file when removing existing extension"""
  1233. if len(modules) > 1:
  1234. # update also toolboxes metadata
  1235. remove_from_toolbox_xml(options['extension'])
  1236. xml_file = os.path.join(options['prefix'], 'modules.xml')
  1237. if not os.path.exists(xml_file):
  1238. return
  1239. # read XML file
  1240. tree = etree_fromfile(xml_file)
  1241. for name in modules:
  1242. for node in tree.findall('task'):
  1243. if node.get('name') != name:
  1244. continue
  1245. tree.remove(node)
  1246. write_xml_modules(xml_file, tree)
  1247. # check links in CSS
  1248. def check_style_files(fil):
  1249. """Ensures that a specified HTML documentation support file exists
  1250. If the file, e.g. a CSS file does not exist, the file is copied from
  1251. the distribution.
  1252. """
  1253. dist_file = os.path.join(os.getenv('GISBASE'), 'docs', 'html', fil)
  1254. addons_file = os.path.join(options['prefix'], 'docs', 'html', fil)
  1255. if os.path.isfile(addons_file):
  1256. return
  1257. try:
  1258. shutil.copyfile(dist_file, addons_file)
  1259. except OSError as error:
  1260. grass.fatal(_("Unable to create '%s': %s") % (addons_file, error))
  1261. def create_dir(path):
  1262. """Creates the specified directory (with all dirs in between)
  1263. NOOP for existing directory.
  1264. """
  1265. if os.path.isdir(path):
  1266. return
  1267. try:
  1268. os.makedirs(path)
  1269. except OSError as error:
  1270. grass.fatal(_("Unable to create '%s': %s") % (path, error))
  1271. grass.debug("'%s' created" % path)
  1272. def check_dirs():
  1273. """Ensure that the necessary directories in prefix path exist"""
  1274. create_dir(os.path.join(options['prefix'], 'bin'))
  1275. create_dir(os.path.join(options['prefix'], 'docs', 'html'))
  1276. create_dir(os.path.join(options['prefix'], 'docs', 'rest'))
  1277. check_style_files('grass_logo.png')
  1278. check_style_files('grassdocs.css')
  1279. create_dir(os.path.join(options['prefix'], 'etc'))
  1280. create_dir(os.path.join(options['prefix'], 'docs', 'man', 'man1'))
  1281. create_dir(os.path.join(options['prefix'], 'scripts'))
  1282. # fix file URI in manual page
  1283. def update_manual_page(module):
  1284. """Fix manual page for addons which are at different directory then rest"""
  1285. if module.split('.', 1)[0] == 'wx':
  1286. return # skip for GUI modules
  1287. grass.verbose(_("Manual page for <%s> updated") % module)
  1288. # read original html file
  1289. htmlfile = os.path.join(
  1290. options['prefix'], 'docs', 'html', module + '.html')
  1291. try:
  1292. oldfile = open(htmlfile)
  1293. shtml = oldfile.read()
  1294. except IOError as error:
  1295. gscript.fatal(_("Unable to read manual page: %s") % error)
  1296. else:
  1297. oldfile.close()
  1298. pos = []
  1299. # fix logo URL
  1300. pattern = r'''<a href="([^"]+)"><img src="grass_logo.png"'''
  1301. for match in re.finditer(pattern, shtml):
  1302. pos.append(match.start(1))
  1303. # find URIs
  1304. pattern = r'''<a href="([^"]+)">([^>]+)</a>'''
  1305. addons = get_installed_extensions(force=True)
  1306. for match in re.finditer(pattern, shtml):
  1307. if match.group(1)[:4] == 'http':
  1308. continue
  1309. if match.group(1).replace('.html', '') in addons:
  1310. continue
  1311. pos.append(match.start(1))
  1312. if not pos:
  1313. return # no match
  1314. # replace file URIs
  1315. prefix = 'file://' + '/'.join([os.getenv('GISBASE'), 'docs', 'html'])
  1316. ohtml = shtml[:pos[0]]
  1317. for i in range(1, len(pos)):
  1318. ohtml += prefix + '/' + shtml[pos[i - 1]:pos[i]]
  1319. ohtml += prefix + '/' + shtml[pos[-1]:]
  1320. # write updated html file
  1321. try:
  1322. newfile = open(htmlfile, 'w')
  1323. newfile.write(ohtml)
  1324. except IOError as error:
  1325. gscript.fatal(_("Unable for write manual page: %s") % error)
  1326. else:
  1327. newfile.close()
  1328. def resolve_install_prefix(path, to_system):
  1329. """Determine and check the path for installation"""
  1330. if to_system:
  1331. path = os.environ['GISBASE']
  1332. if path == '$GRASS_ADDON_BASE':
  1333. if not os.getenv('GRASS_ADDON_BASE'):
  1334. grass.warning(_("GRASS_ADDON_BASE is not defined, "
  1335. "installing to ~/.grass%s/addons") % version[0])
  1336. path = os.path.join(
  1337. os.environ['HOME'], '.grass%s' % version[0], 'addons')
  1338. else:
  1339. path = os.environ['GRASS_ADDON_BASE']
  1340. if os.path.exists(path) and \
  1341. not os.access(path, os.W_OK):
  1342. grass.fatal(_("You don't have permission to install extension to <{0}>."
  1343. " Try to run {1} with administrator rights"
  1344. " (su or sudo).")
  1345. .format(path, 'g.extension'))
  1346. # ensure dir sep at the end for cases where path is used as URL and pasted
  1347. # together with file names
  1348. if not path.endswith(os.path.sep):
  1349. path = path + os.path.sep
  1350. return os.path.abspath(path) # make likes absolute paths
  1351. def resolve_xmlurl_prefix(url, source=None):
  1352. """Determine and check the URL where the XML metadata files are stored
  1353. It ensures that there is a single slash at the end of URL, so we can attach
  1354. file name easily:
  1355. >>> resolve_xmlurl_prefix('https://grass.osgeo.org/addons')
  1356. 'https://grass.osgeo.org/addons/'
  1357. >>> resolve_xmlurl_prefix('https://grass.osgeo.org/addons/')
  1358. 'https://grass.osgeo.org/addons/'
  1359. """
  1360. gscript.debug("resolve_xmlurl_prefix(url={0}, source={1})".format(url, source))
  1361. if source == 'official':
  1362. # use pregenerated modules XML file
  1363. url = 'https://grass.osgeo.org/addons/grass%s/' % version[0]
  1364. # else try to get modules XMl from SVN repository (provided URL)
  1365. # the exact action depends on subsequent code (somewhere)
  1366. if not url.endswith('/'):
  1367. url = url + '/'
  1368. return url
  1369. KNOWN_HOST_SERVICES_INFO = {
  1370. 'OSGeo Trac': {
  1371. 'domain': 'trac.osgeo.org',
  1372. 'ignored_suffixes': ['format=zip'],
  1373. 'possible_starts': ['', 'https://', 'http://'],
  1374. 'url_start': 'https://',
  1375. 'url_end': '?format=zip',
  1376. },
  1377. 'GitHub': {
  1378. 'domain': 'github.com',
  1379. 'ignored_suffixes': ['.zip', '.tar.gz'],
  1380. 'possible_starts': ['', 'https://', 'http://'],
  1381. 'url_start': 'https://',
  1382. 'url_end': '/archive/master.zip',
  1383. },
  1384. 'GitLab': {
  1385. 'domain': 'gitlab.com',
  1386. 'ignored_suffixes': ['.zip', '.tar.gz', '.tar.bz2', '.tar'],
  1387. 'possible_starts': ['', 'https://', 'http://'],
  1388. 'url_start': 'https://',
  1389. 'url_end': '/repository/archive.zip',
  1390. },
  1391. 'Bitbucket': {
  1392. 'domain': 'bitbucket.org',
  1393. 'ignored_suffixes': ['.zip', '.tar.gz', '.gz', '.bz2'],
  1394. 'possible_starts': ['', 'https://', 'http://'],
  1395. 'url_start': 'https://',
  1396. 'url_end': '/get/master.zip',
  1397. },
  1398. }
  1399. # TODO: support ZIP URLs which don't end with zip
  1400. # https://gitlab.com/user/reponame/repository/archive.zip?ref=b%C3%A9po
  1401. def resolve_known_host_service(url):
  1402. """Determine source type and full URL for known hosting service
  1403. If the service is not determined from the provided URL, tuple with
  1404. is two ``None`` values is returned.
  1405. """
  1406. match = None
  1407. actual_start = None
  1408. for key, value in KNOWN_HOST_SERVICES_INFO.items():
  1409. for start in value['possible_starts']:
  1410. if url.startswith(start + value['domain']):
  1411. match = value
  1412. actual_start = start
  1413. gscript.verbose(_("Identified {0} as known hosting service")
  1414. .format(key))
  1415. for suffix in value['ignored_suffixes']:
  1416. if url.endswith(suffix):
  1417. gscript.verbose(
  1418. _("Not using {service} as known hosting service"
  1419. " because the URL ends with '{suffix}'")
  1420. .format(service=key, suffix=suffix))
  1421. return None, None
  1422. if match:
  1423. if not actual_start:
  1424. actual_start = match['url_start']
  1425. else:
  1426. actual_start = ''
  1427. url = '{prefix}{base}{suffix}'.format(prefix=actual_start,
  1428. base=url.rstrip('/'),
  1429. suffix=match['url_end'])
  1430. gscript.verbose(_("Will use the following URL for download: {0}")
  1431. .format(url))
  1432. return 'remote_zip', url
  1433. else:
  1434. return None, None
  1435. # TODO: add also option to enforce the source type
  1436. def resolve_source_code(url=None, name=None):
  1437. """Return type and URL or path of the source code
  1438. Local paths are not presented as URLs to be usable in standard functions.
  1439. Path is identified as local path if the directory of file exists which
  1440. has the unfortunate consequence that the not existing files are evaluated
  1441. as remote URLs. When path is not evaluated, Subversion is assumed for
  1442. backwards compatibility. When GitHub repository is specified, ZIP file
  1443. link is returned. The ZIP is for master branch, not the default one because
  1444. GitHub does not provide the default branch in the URL (July 2015).
  1445. :returns: tuple with type of source and full URL or path
  1446. Official repository:
  1447. >>> resolve_source_code(name='g.example') # doctest: +SKIP
  1448. ('official', 'https://trac.osgeo.org/.../general/g.example')
  1449. Subversion:
  1450. >>> resolve_source_code('https://svn.osgeo.org/grass/grass-addons/grass7')
  1451. ('svn', 'https://svn.osgeo.org/grass/grass-addons/grass7')
  1452. ZIP files online:
  1453. >>> resolve_source_code('https://trac.osgeo.org/.../r.modis?format=zip') # doctest: +SKIP
  1454. ('remote_zip', 'https://trac.osgeo.org/.../r.modis?format=zip')
  1455. Local directories and ZIP files:
  1456. >>> resolve_source_code(os.path.expanduser("~")) # doctest: +ELLIPSIS
  1457. ('dir', '...')
  1458. >>> resolve_source_code('/local/directory/downloaded.zip') # doctest: +SKIP
  1459. ('zip', '/local/directory/downloaded.zip')
  1460. OSGeo Trac:
  1461. >>> resolve_source_code('trac.osgeo.org/.../r.agent.aco') # doctest: +SKIP
  1462. ('remote_zip', 'https://trac.osgeo.org/.../r.agent.aco?format=zip')
  1463. >>> resolve_source_code('https://trac.osgeo.org/.../r.agent.aco') # doctest: +SKIP
  1464. ('remote_zip', 'https://trac.osgeo.org/.../r.agent.aco?format=zip')
  1465. GitHub:
  1466. >>> resolve_source_code('github.com/user/g.example') # doctest: +SKIP
  1467. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1468. >>> resolve_source_code('github.com/user/g.example/') # doctest: +SKIP
  1469. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1470. >>> resolve_source_code('https://github.com/user/g.example') # doctest: +SKIP
  1471. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1472. >>> resolve_source_code('https://github.com/user/g.example/') # doctest: +SKIP
  1473. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  1474. GitLab:
  1475. >>> resolve_source_code('gitlab.com/JoeUser/GrassModule') # doctest: +SKIP
  1476. ('remote_zip', 'https://gitlab.com/JoeUser/GrassModule/repository/archive.zip')
  1477. >>> resolve_source_code('https://gitlab.com/JoeUser/GrassModule') # doctest: +SKIP
  1478. ('remote_zip', 'https://gitlab.com/JoeUser/GrassModule/repository/archive.zip')
  1479. Bitbucket:
  1480. >>> resolve_source_code('bitbucket.org/joe-user/grass-module') # doctest: +SKIP
  1481. ('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
  1482. >>> resolve_source_code('https://bitbucket.org/joe-user/grass-module') # doctest: +SKIP
  1483. ('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
  1484. """
  1485. if not url and name:
  1486. module_class = get_module_class_name(name)
  1487. trac_url = 'https://trac.osgeo.org/grass/browser/grass-addons/' \
  1488. 'grass{version}/{module_class}/{module_name}?format=zip' \
  1489. .format(version=version[0],
  1490. module_class=module_class, module_name=name)
  1491. return 'official', trac_url
  1492. # Check if URL can be found
  1493. # Catch corner case if local URL is given starting with file://
  1494. url = url[6:] if url.startswith('file://') else url
  1495. if not os.path.exists(url):
  1496. url_validated = False
  1497. if url.startswith('http'):
  1498. try:
  1499. open_url = urlopen(url)
  1500. open_url.close()
  1501. url_validated = True
  1502. except:
  1503. pass
  1504. else:
  1505. try:
  1506. open_url = urlopen('http://' + url)
  1507. open_url.close()
  1508. url_validated = True
  1509. except:
  1510. pass
  1511. try:
  1512. open_url = urlopen('https://' + url)
  1513. open_url.close()
  1514. url_validated = True
  1515. except:
  1516. pass
  1517. if not url_validated:
  1518. grass.fatal(_('Cannot open URL: {}'.format(url)))
  1519. # Handle local URLs
  1520. if os.path.isdir(url):
  1521. return 'dir', os.path.abspath(url)
  1522. elif os.path.exists(url):
  1523. if url.endswith('.zip'):
  1524. return 'zip', os.path.abspath(url)
  1525. for suffix in extract_tar.supported_formats:
  1526. if url.endswith('.' + suffix):
  1527. return suffix, os.path.abspath(url)
  1528. # Handle remote URLs
  1529. else:
  1530. source, resolved_url = resolve_known_host_service(url)
  1531. if source:
  1532. return source, resolved_url
  1533. # we allow URL to end with =zip or ?zip and not only .zip
  1534. # unfortunately format=zip&version=89612 would require something else
  1535. # special option to force the source type would solve it
  1536. if url.endswith('zip'):
  1537. return 'remote_zip', url
  1538. for suffix in extract_tar.supported_formats:
  1539. if url.endswith(suffix):
  1540. return 'remote_' + suffix, url
  1541. # fallback to the classic behavior
  1542. return 'svn', url
  1543. def main():
  1544. # check dependencies
  1545. if not flags['a'] and sys.platform != "win32":
  1546. check_progs()
  1547. original_url = options['url']
  1548. # manage proxies
  1549. global PROXIES
  1550. if options['proxy']:
  1551. PROXIES = {}
  1552. for ptype, purl in (p.split('=') for p in options['proxy'].split(',')):
  1553. PROXIES[ptype] = purl
  1554. proxy = ProxyHandler(PROXIES)
  1555. opener = build_opener(proxy)
  1556. install_opener(opener)
  1557. # define path
  1558. options['prefix'] = resolve_install_prefix(path=options['prefix'],
  1559. to_system=flags['s'])
  1560. # list available extensions
  1561. if flags['l'] or flags['c'] or (flags['g'] and not flags['a']):
  1562. # using dummy module, we don't need any module URL now,
  1563. # but will work only as long as the function does not check
  1564. # if the URL is actually valid or something
  1565. source, url = resolve_source_code(name='dummy',
  1566. url=original_url)
  1567. xmlurl = resolve_xmlurl_prefix(original_url, source=source)
  1568. list_available_extensions(xmlurl)
  1569. return 0
  1570. elif flags['a']:
  1571. list_installed_extensions(toolboxes=flags['t'])
  1572. return 0
  1573. if flags['d']:
  1574. if options['operation'] != 'add':
  1575. grass.warning(_("Flag 'd' is relevant only to"
  1576. " 'operation=add'. Ignoring this flag."))
  1577. else:
  1578. global REMOVE_TMPDIR
  1579. REMOVE_TMPDIR = False
  1580. if options['operation'] == 'add':
  1581. check_dirs()
  1582. source, url = resolve_source_code(name=options['extension'],
  1583. url=original_url)
  1584. xmlurl = resolve_xmlurl_prefix(original_url, source=source)
  1585. install_extension(source=source, url=url, xmlurl=xmlurl)
  1586. else: # remove
  1587. remove_extension(force=flags['f'])
  1588. return 0
  1589. if __name__ == "__main__":
  1590. if len(sys.argv) == 2 and sys.argv[1] == '--doctest':
  1591. import doctest
  1592. sys.exit(doctest.testmod().failed)
  1593. options, flags = grass.parser()
  1594. global TMPDIR
  1595. TMPDIR = tempfile.mkdtemp()
  1596. atexit.register(cleanup)
  1597. grass_version = grass.version()
  1598. version = grass_version['version'].split('.')
  1599. build_platform = grass_version['build_platform'].split('-', 1)[0]
  1600. sys.exit(main())