g.extension.py 90 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680
  1. #!/usr/bin/env python3
  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-2021 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: - update temporary workaround of using grass7 subdir of addon-repo, see
  17. # https://github.com/OSGeo/grass-addons/issues/528
  18. # - add sudo support where needed (i.e. check first permission to write into
  19. # $GISBASE directory)
  20. # - fix toolbox support in install_private_extension_xml()
  21. #############################################################################
  22. #%module
  23. #% label: Maintains GRASS Addons extensions in local GRASS installation.
  24. #% description: Downloads and installs extensions from GRASS Addons repository or other source into the local GRASS installation or removes installed extensions.
  25. #% keyword: general
  26. #% keyword: installation
  27. #% keyword: extensions
  28. #% keyword: addons
  29. #% keyword: download
  30. #%end
  31. #%option
  32. #% key: extension
  33. #% type: string
  34. #% key_desc: name
  35. #% label: Name of extension to install or remove
  36. #% description: Name of toolbox (set of extensions) when -t flag is given
  37. #% required: yes
  38. #%end
  39. #%option
  40. #% key: operation
  41. #% type: string
  42. #% description: Operation to be performed
  43. #% required: yes
  44. #% options: add,remove
  45. #% answer: add
  46. #%end
  47. #%option
  48. #% key: url
  49. #% type: string
  50. #% key_desc: url
  51. #% label: URL or directory to get the extension from (supported only on Linux and Mac)
  52. #% 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.
  53. #%end
  54. #%option
  55. #% key: prefix
  56. #% type: string
  57. #% key_desc: path
  58. #% description: Prefix where to install extension (ignored when flag -s is given)
  59. #% answer: $GRASS_ADDON_BASE
  60. #% required: no
  61. #%end
  62. #%option
  63. #% key: proxy
  64. #% type: string
  65. #% key_desc: proxy
  66. #% description: Set the proxy with: "http=<value>,ftp=<value>"
  67. #% required: no
  68. #% multiple: yes
  69. #%end
  70. #%option
  71. #% key: branch
  72. #% type: string
  73. #% key_desc: branch
  74. #% description: Specific branch to fetch addon from (only used when fetching from git)
  75. #% required: no
  76. #% multiple: no
  77. #%end
  78. #%flag
  79. #% key: l
  80. #% description: List available extensions in the official GRASS GIS Addons repository
  81. #% guisection: Print
  82. #% suppress_required: yes
  83. #%end
  84. #%flag
  85. #% key: c
  86. #% description: List available extensions in the official GRASS GIS Addons repository including module description
  87. #% guisection: Print
  88. #% suppress_required: yes
  89. #%end
  90. #%flag
  91. #% key: g
  92. #% description: List available extensions in the official GRASS GIS Addons repository (shell script style)
  93. #% guisection: Print
  94. #% suppress_required: yes
  95. #%end
  96. #%flag
  97. #% key: a
  98. #% description: List locally installed extensions
  99. #% guisection: Print
  100. #% suppress_required: yes
  101. #%end
  102. #%flag
  103. #% key: s
  104. #% description: Install system-wide (may need system administrator rights)
  105. #% guisection: Install
  106. #%end
  107. #%flag
  108. #% key: d
  109. #% description: Download source code and exit
  110. #% guisection: Install
  111. #%end
  112. #%flag
  113. #% key: i
  114. #% description: Do not install new extension, just compile it
  115. #% guisection: Install
  116. #%end
  117. #%flag
  118. #% key: f
  119. #% description: Force removal when uninstalling extension (operation=remove)
  120. #% guisection: Remove
  121. #%end
  122. #%flag
  123. #% key: t
  124. #% description: Operate on toolboxes instead of single modules (experimental)
  125. #% suppress_required: yes
  126. #%end
  127. #%flag
  128. #% key: o
  129. #% description: url refers to a fork of the official extension repository
  130. #%end
  131. #%flag
  132. #% key: j
  133. #% description: Generates JSON file containing the download URLs of the official Addons
  134. #% guisection: Install
  135. #% suppress_required: yes
  136. #%end
  137. #%rules
  138. #% required: extension, -l, -c, -g, -a, -j
  139. #% exclusive: extension, -l, -c, -g
  140. #% exclusive: extension, -l, -c, -a
  141. #% requires: -o, url
  142. #% requires: branch, url
  143. #%end
  144. # TODO: solve addon-extension(-module) confusion
  145. from __future__ import print_function
  146. import fileinput
  147. import http
  148. import os
  149. import codecs
  150. import sys
  151. import re
  152. import atexit
  153. import shutil
  154. import zipfile
  155. import tempfile
  156. import json
  157. import xml.etree.ElementTree as etree
  158. from distutils.dir_util import copy_tree
  159. from six.moves.urllib import request as urlrequest
  160. from six.moves.urllib.error import HTTPError, URLError
  161. from six.moves.urllib.parse import urlparse
  162. # Get the XML parsing exceptions to catch. The behavior changed with Python 2.7
  163. # and ElementTree 1.3.
  164. from xml.parsers import expat # TODO: works for any Python?
  165. if hasattr(etree, "ParseError"):
  166. ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
  167. else:
  168. ETREE_EXCEPTIONS = expat.ExpatError
  169. import grass.script as gscript
  170. from grass.script.utils import try_rmdir
  171. from grass.script import core as grass
  172. from grass.script import task as gtask
  173. # temp dir
  174. REMOVE_TMPDIR = True
  175. PROXIES = {}
  176. HEADERS = {
  177. "User-Agent": "Mozilla/5.0",
  178. }
  179. HTTP_STATUS_CODES = list(http.HTTPStatus)
  180. GIT_URL = "https://github.com/OSGeo/grass-addons"
  181. def replace_shebang_win(python_file):
  182. """
  183. Replaces "python" with "python3" in python files
  184. using UTF8 encoding on MS Windows
  185. """
  186. cur_dir = os.path.dirname(python_file)
  187. tmp_name = os.path.join(cur_dir, gscript.tempname(12))
  188. with codecs.open(python_file, "r", encoding="utf8") as in_file, codecs.open(
  189. tmp_name, "w", encoding="utf8"
  190. ) as out_file:
  191. for line in in_file:
  192. new_line = line.replace(
  193. "#!/usr/bin/env python\n", "#!/usr/bin/env python3\n"
  194. )
  195. out_file.write(new_line)
  196. os.remove(python_file) # remove original
  197. os.rename(tmp_name, python_file) # rename temp to original name
  198. def urlretrieve(url, filename, *args, **kwargs):
  199. """Same function as 'urlretrieve', but with the ability to
  200. define headers.
  201. """
  202. request = urlrequest.Request(url, headers=HEADERS)
  203. response = urlrequest.urlopen(request, *args, **kwargs)
  204. with open(filename, "wb") as f:
  205. f.write(response.read())
  206. def urlopen(url, *args, **kwargs):
  207. """Wrapper around urlopen. Same function as 'urlopen', but with the
  208. ability to define headers.
  209. """
  210. request = urlrequest.Request(url, headers=HEADERS)
  211. return urlrequest.urlopen(request, *args, **kwargs)
  212. def get_version_branch(major_version):
  213. """Check if version branch for the current GRASS version exists,
  214. if not, take branch for the previous version
  215. For the official repo we assume that at least one version branch is present"""
  216. version_branch = "grass{}".format(major_version)
  217. try:
  218. urlrequest.urlopen(
  219. "{GIT_URL}/tree/{version_branch}/src".format(
  220. GIT_URL=GIT_URL, version_branch=version_branch
  221. )
  222. )
  223. except URLError:
  224. version_branch = "grass{}".format(int(major_version) - 1)
  225. return version_branch
  226. def get_github_branches(
  227. github_api_url="https://api.github.com/repos/OSGeo/grass-addons/branches",
  228. version_only=True,
  229. ):
  230. """Get ordered list of branch names in repo using github API
  231. For the official repo we assume that at least one version branch is present
  232. Due to strict rate limits in the github API (60 calls per hour) this function
  233. is currently not used."""
  234. req = urlrequest.urlopen(github_api_url)
  235. content = json.loads(req.read())
  236. branches = [repo_branch["name"] for repo_branch in content]
  237. if version_only:
  238. branches = [
  239. version_branch
  240. for version_branch in branches
  241. if version_branch.startswith("grass")
  242. ]
  243. branches.sort()
  244. return branches
  245. def get_default_branch(full_url):
  246. """Get default branch for repository in known hosting services
  247. (currently only implemented for github, gitlab and bitbucket API)
  248. In all other cases "main" is used as default"""
  249. # Parse URL
  250. url_parts = urlparse(full_url)
  251. # Get organization and repository component
  252. try:
  253. organization, repository = url_parts.path.split("/")[1:3]
  254. except URLError:
  255. gscript.fatal(
  256. _(
  257. "Cannot retrieve organization and repository from URL: <{}>.".format(
  258. full_url
  259. )
  260. )
  261. )
  262. # Construct API call and retrieve default branch
  263. api_calls = {
  264. "github.com": "https://api.github.com/repos/{organization}/{repository}".format(
  265. organization=organization, repository=repository
  266. ),
  267. "gitlab.com": "https://gitlab.com/api/v4/projects/{organization}%2F{repository}".format(
  268. organization=organization, repository=repository
  269. ),
  270. "bitbucket.org": "https://api.bitbucket.org/2.0/repositories/{organization}/{repository}/branching-model?".format(
  271. organization=organization, repository=repository
  272. ),
  273. }
  274. # Try to get default branch via API. The API call is known to fail a) if the full_url
  275. # does not belong to an implemented hosting service or b) if the rate limit of the
  276. # API is exceeded
  277. try:
  278. req = urlrequest.urlopen(api_calls.get(url_parts.netloc))
  279. content = json.loads(req.read())
  280. # For github and gitlab
  281. default_branch = content.get("default_branch")
  282. # For bitbucket
  283. if not default_branch:
  284. default_branch = content.get("development").get("name")
  285. except URLError:
  286. default_branch = "main"
  287. return default_branch
  288. def download_addons_paths_file(url, response_format, *args, **kwargs):
  289. """Generates JSON file containing the download URLs of the official
  290. Addons
  291. :param str url: url address
  292. :param str response_format: content type
  293. :return response: urllib.request.urlopen response object or None
  294. """
  295. try:
  296. response = urlopen(url, *args, **kwargs)
  297. if not response.code == 200:
  298. index = HTTP_STATUS_CODES.index(response.code)
  299. desc = HTTP_STATUS_CODES[index].description
  300. gscript.fatal(
  301. _(
  302. "Download file from <{url}>, "
  303. "return status code {code}, "
  304. "{desc}".format(
  305. url=url,
  306. code=response.code,
  307. desc=desc,
  308. ),
  309. ),
  310. )
  311. if response_format not in response.getheader("Content-Type"):
  312. gscript.fatal(
  313. _(
  314. "Wrong downloaded file format. "
  315. "Check url <{url}>. Allowed file format is "
  316. "{response_format}.".format(
  317. url=url,
  318. response_format=response_format,
  319. ),
  320. ),
  321. )
  322. return response
  323. except HTTPError as err:
  324. if err.code == 403 and err.msg == "rate limit exceeded":
  325. gscript.warning(
  326. _(
  327. "The download of the json file with add-ons paths "
  328. "from the github server wasn't successful, "
  329. "{}. The previous downloaded json file "
  330. " will be used if exists.".format(err.msg)
  331. ),
  332. )
  333. else:
  334. return download_addons_paths_file(
  335. url=url.replace("main", "master"),
  336. response_format=response_format,
  337. )
  338. except URLError:
  339. gscript.fatal(
  340. _(
  341. "Download file from <{url}>, "
  342. "failed. Check internet connection.".format(
  343. url=url,
  344. ),
  345. ),
  346. )
  347. def etree_fromfile(filename):
  348. """Create XML element tree from a given file name"""
  349. with open(filename, "r") as file_:
  350. return etree.fromstring(file_.read())
  351. def etree_fromurl(url):
  352. """Create XML element tree from a given URL"""
  353. file_ = urlopen(url)
  354. return etree.fromstring(file_.read())
  355. def check_progs():
  356. """Check if the necessary programs are available"""
  357. # git to be tested once supported instead of `svn`
  358. for prog in ("make", "gcc", "svn"):
  359. if not grass.find_program(prog, "--help"):
  360. grass.fatal(_("'%s' required. Please install '%s' first.") % (prog, prog))
  361. # expand prefix to class name
  362. def expand_module_class_name(class_letters):
  363. """Convert module class (family) letter or letters to class (family) name
  364. The letter or letters are used in module names, e.g. r.slope.aspect.
  365. The names are used in directories in Addons but also in the source code.
  366. >>> expand_module_class_name('r')
  367. 'raster'
  368. >>> expand_module_class_name('v')
  369. 'vector'
  370. """
  371. name = {
  372. "d": "display",
  373. "db": "db",
  374. "g": "general",
  375. "i": "imagery",
  376. "m": "misc",
  377. "ps": "postscript",
  378. "p": "paint",
  379. "r": "raster",
  380. "r3": "raster3d",
  381. "s": "sites",
  382. "t": "temporal",
  383. "v": "vector",
  384. "wx": "gui/wxpython",
  385. }
  386. return name.get(class_letters, class_letters)
  387. def get_module_class_name(module_name):
  388. """Return class (family) name for a module
  389. The names are used in directories in Addons but also in the source code.
  390. >>> get_module_class_name('r.slope.aspect')
  391. 'raster'
  392. >>> get_module_class_name('v.to.rast')
  393. 'vector'
  394. """
  395. classchar = module_name.split(".", 1)[0]
  396. return expand_module_class_name(classchar)
  397. def get_installed_extensions(force=False):
  398. """Get list of installed extensions or toolboxes (if -t is set)"""
  399. if flags["t"]:
  400. return get_installed_toolboxes(force)
  401. # TODO: extension != module
  402. return get_installed_modules(force)
  403. def list_installed_extensions(toolboxes=False):
  404. """List installed extensions"""
  405. elist = get_installed_extensions()
  406. if elist:
  407. if toolboxes:
  408. grass.message(_("List of installed extensions (toolboxes):"))
  409. else:
  410. grass.message(_("List of installed extensions (modules):"))
  411. sys.stdout.write("\n".join(elist))
  412. sys.stdout.write("\n")
  413. else:
  414. if toolboxes:
  415. grass.info(_("No extension (toolbox) installed"))
  416. else:
  417. grass.info(_("No extension (module) installed"))
  418. def get_installed_toolboxes(force=False):
  419. """Get list of installed toolboxes
  420. Writes toolboxes file if it does not exist.
  421. Creates a new toolboxes file if it is not possible
  422. to read the current one.
  423. """
  424. xml_file = os.path.join(options["prefix"], "toolboxes.xml")
  425. if not os.path.exists(xml_file):
  426. write_xml_toolboxes(xml_file)
  427. # read XML file
  428. try:
  429. tree = etree_fromfile(xml_file)
  430. except ETREE_EXCEPTIONS + (OSError, IOError):
  431. os.remove(xml_file)
  432. write_xml_toolboxes(xml_file)
  433. return []
  434. ret = list()
  435. for tnode in tree.findall("toolbox"):
  436. ret.append(tnode.get("code"))
  437. return ret
  438. def get_installed_modules(force=False):
  439. """Get list of installed modules.
  440. Writes modules file if it does not exist and *force* is set to ``True``.
  441. Creates a new modules file if it is not possible
  442. to read the current one.
  443. """
  444. xml_file = os.path.join(options["prefix"], "modules.xml")
  445. if not os.path.exists(xml_file):
  446. if force:
  447. write_xml_modules(xml_file)
  448. else:
  449. grass.debug("No addons metadata file available", 1)
  450. return []
  451. # read XML file
  452. try:
  453. tree = etree_fromfile(xml_file)
  454. except ETREE_EXCEPTIONS + (OSError, IOError):
  455. os.remove(xml_file)
  456. write_xml_modules(xml_file)
  457. return []
  458. ret = list()
  459. for tnode in tree.findall("task"):
  460. if flags["g"]:
  461. desc, keyw = get_optional_params(tnode)
  462. ret.append("name={0}".format(tnode.get("name").strip()))
  463. ret.append("description={0}".format(desc))
  464. ret.append("keywords={0}".format(keyw))
  465. ret.append(
  466. "executables={0}".format(",".join(get_module_executables(tnode)))
  467. )
  468. else:
  469. ret.append(tnode.get("name").strip())
  470. return ret
  471. # list extensions (read XML file from grass.osgeo.org/addons)
  472. def list_available_extensions(url):
  473. """List available extensions/modules or toolboxes (if -t is given)
  474. For toolboxes it lists also all modules.
  475. """
  476. gscript.debug("list_available_extensions(url={0})".format(url))
  477. if flags["t"]:
  478. grass.message(_("List of available extensions (toolboxes):"))
  479. tlist = get_available_toolboxes(url)
  480. tkeys = sorted(tlist.keys())
  481. for toolbox_code in tkeys:
  482. toolbox_data = tlist[toolbox_code]
  483. if flags["g"]:
  484. print("toolbox_name=" + toolbox_data["name"])
  485. print("toolbox_code=" + toolbox_code)
  486. else:
  487. print("%s (%s)" % (toolbox_data["name"], toolbox_code))
  488. if flags["c"] or flags["g"]:
  489. list_available_modules(url, toolbox_data["modules"])
  490. else:
  491. if toolbox_data["modules"]:
  492. print(os.linesep.join(["* " + x for x in toolbox_data["modules"]]))
  493. else:
  494. grass.message(_("List of available extensions (modules):"))
  495. # TODO: extensions with several modules + lib
  496. list_available_modules(url)
  497. def get_available_toolboxes(url):
  498. """Return toolboxes available in the repository"""
  499. tdict = dict()
  500. url = url + "toolboxes.xml"
  501. try:
  502. tree = etree_fromurl(url)
  503. for tnode in tree.findall("toolbox"):
  504. mlist = list()
  505. clist = list()
  506. tdict[tnode.get("code")] = {
  507. "name": tnode.get("name"),
  508. "correlate": clist,
  509. "modules": mlist,
  510. }
  511. for cnode in tnode.findall("correlate"):
  512. clist.append(cnode.get("name"))
  513. for mnode in tnode.findall("task"):
  514. mlist.append(mnode.get("name"))
  515. except (HTTPError, IOError, OSError):
  516. grass.fatal(_("Unable to fetch addons metadata file"))
  517. return tdict
  518. def get_toolbox_extensions(url, name):
  519. """Get extensions inside a toolbox in toolbox file at given URL
  520. :param url: URL of the directory (file name will be attached)
  521. :param name: toolbox name
  522. """
  523. # dictionary of extensions
  524. edict = dict()
  525. url = url + "toolboxes.xml"
  526. try:
  527. tree = etree_fromurl(url)
  528. for tnode in tree.findall("toolbox"):
  529. if name == tnode.get("code"):
  530. for enode in tnode.findall("task"):
  531. # extension name
  532. ename = enode.get("name")
  533. edict[ename] = dict()
  534. # list of modules installed by this extension
  535. edict[ename]["mlist"] = list()
  536. # list of files installed by this extension
  537. edict[ename]["flist"] = list()
  538. break
  539. except (HTTPError, IOError, OSError):
  540. grass.fatal(_("Unable to fetch addons metadata file"))
  541. return edict
  542. def get_module_files(mnode):
  543. """Return list of module files
  544. :param mnode: XML node for a module
  545. """
  546. flist = []
  547. if mnode.find("binary") is None:
  548. return flist
  549. for file_node in mnode.find("binary").findall("file"):
  550. filepath = file_node.text
  551. flist.append(filepath)
  552. return flist
  553. def get_module_executables(mnode):
  554. """Return list of module executables
  555. :param mnode: XML node for a module
  556. """
  557. flist = []
  558. for filepath in get_module_files(mnode):
  559. if filepath.startswith(options["prefix"] + os.path.sep + "bin") or (
  560. sys.platform != "win32"
  561. and filepath.startswith(options["prefix"] + os.path.sep + "scripts")
  562. ):
  563. filename = os.path.basename(filepath)
  564. if sys.platform == "win32":
  565. filename = os.path.splitext(filename)[0]
  566. flist.append(filename)
  567. return flist
  568. def get_optional_params(mnode):
  569. """Return description and keywords of a module as a tuple
  570. :param mnode: XML node for a module
  571. """
  572. try:
  573. desc = mnode.find("description").text
  574. except AttributeError:
  575. desc = ""
  576. if desc is None:
  577. desc = ""
  578. try:
  579. keyw = mnode.find("keywords").text
  580. except AttributeError:
  581. keyw = ""
  582. if keyw is None:
  583. keyw = ""
  584. return desc, keyw
  585. def list_available_modules(url, mlist=None):
  586. """List modules available in the repository
  587. Tries to use XML metadata file first. Fallbacks to HTML page with a list.
  588. :param url: URL of the directory (file name will be attached)
  589. :param mlist: list only modules in this list
  590. """
  591. file_url = url + "modules.xml"
  592. grass.debug("url=%s" % file_url, 1)
  593. try:
  594. tree = etree_fromurl(file_url)
  595. except ETREE_EXCEPTIONS:
  596. grass.warning(
  597. _(
  598. "Unable to parse '%s'. Trying to scan"
  599. " SVN repository (may take some time)..."
  600. )
  601. % file_url
  602. )
  603. list_available_extensions_svn(url)
  604. return
  605. except (HTTPError, URLError, IOError, OSError):
  606. list_available_extensions_svn(url)
  607. return
  608. for mnode in tree.findall("task"):
  609. name = mnode.get("name").strip()
  610. if mlist and name not in mlist:
  611. continue
  612. if flags["c"] or flags["g"]:
  613. desc, keyw = get_optional_params(mnode)
  614. if flags["g"]:
  615. print("name=" + name)
  616. print("description=" + desc)
  617. print("keywords=" + keyw)
  618. elif flags["c"]:
  619. if mlist:
  620. print("*", end="")
  621. print(name + " - " + desc)
  622. else:
  623. print(name)
  624. # TODO: this is now broken/dead code, SVN is basically not used
  625. # fallback for Trac should parse Trac HTML page
  626. # this might be useful for potential SVN repos or anything
  627. # which would list the extensions/addons as list
  628. # TODO: fail when nothing is accessible
  629. def list_available_extensions_svn(url):
  630. """List available extensions from HTML given by URL
  631. Filename is generated based on the module class/family.
  632. This works well for the structure which is in grass-addons repository.
  633. ``<li><a href=...`` is parsed to find module names.
  634. This works well for HTML page generated by Subversion.
  635. :param url: a directory URL (filename will be attached)
  636. """
  637. gscript.debug("list_available_extensions_svn(url=%s)" % url, 2)
  638. grass.message(
  639. _(
  640. "Fetching list of extensions from"
  641. " GRASS-Addons SVN repository (be patient)..."
  642. )
  643. )
  644. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  645. if flags["c"]:
  646. grass.warning(_("Flag 'c' ignored, addons metadata file not available"))
  647. if flags["g"]:
  648. grass.warning(_("Flag 'g' ignored, addons metadata file not available"))
  649. prefixes = ["d", "db", "g", "i", "m", "ps", "p", "r", "r3", "s", "t", "v"]
  650. for prefix in prefixes:
  651. modclass = expand_module_class_name(prefix)
  652. grass.verbose(_("Checking for '%s' modules...") % modclass)
  653. # construct a full URL of a file
  654. file_url = "%s/%s" % (url, modclass)
  655. grass.debug("url = %s" % file_url, debug=2)
  656. try:
  657. file_ = urlopen(url)
  658. except (HTTPError, IOError, OSError):
  659. grass.debug(_("Unable to fetch '%s'") % file_url, debug=1)
  660. continue
  661. for line in file_.readlines():
  662. # list extensions
  663. sline = pattern.search(line)
  664. if not sline:
  665. continue
  666. name = sline.group(2).rstrip("/")
  667. if name.split(".", 1)[0] == prefix:
  668. print(name)
  669. # get_wxgui_extensions(url)
  670. # TODO: this is a dead code, not clear why not used, but seems not needed
  671. def get_wxgui_extensions(url):
  672. """Return list of extensions/addons in wxGUI directory at given URL
  673. :param url: a directory URL (filename will be attached)
  674. """
  675. mlist = list()
  676. grass.debug(
  677. "Fetching list of wxGUI extensions from "
  678. "GRASS-Addons SVN repository (be patient)..."
  679. )
  680. pattern = re.compile(r'(<li><a href=".+">)(.+)(</a></li>)', re.IGNORECASE)
  681. grass.verbose(_("Checking for '%s' modules...") % "gui/wxpython")
  682. # construct a full URL of a file
  683. url = "%s/%s" % (url, "gui/wxpython")
  684. grass.debug("url = %s" % url, debug=2)
  685. file_ = urlopen(url)
  686. if not file_:
  687. grass.warning(_("Unable to fetch '%s'") % url)
  688. return
  689. for line in file_.readlines():
  690. # list extensions
  691. sline = pattern.search(line)
  692. if not sline:
  693. continue
  694. name = sline.group(2).rstrip("/")
  695. if name not in ("..", "Makefile"):
  696. mlist.append(name)
  697. return mlist
  698. def cleanup():
  699. """Cleanup after the downloads and copilation"""
  700. if REMOVE_TMPDIR:
  701. try_rmdir(TMPDIR)
  702. else:
  703. grass.message("\n%s\n" % _("Path to the source code:"))
  704. sys.stderr.write("%s\n" % os.path.join(TMPDIR, options["extension"]))
  705. def write_xml_modules(name, tree=None):
  706. """Write element tree as a modules matadata file
  707. If the *tree* is not given, an empty file is created.
  708. :param name: file name
  709. :param tree: XML element tree
  710. """
  711. file_ = open(name, "w")
  712. file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  713. file_.write('<!DOCTYPE task SYSTEM "grass-addons.dtd">\n')
  714. file_.write('<addons version="%s">\n' % version[0])
  715. libgis_revison = grass.version()["libgis_revision"]
  716. if tree is not None:
  717. for tnode in tree.findall("task"):
  718. indent = 4
  719. file_.write('%s<task name="%s">\n' % (" " * indent, tnode.get("name")))
  720. indent += 4
  721. file_.write(
  722. "%s<description>%s</description>\n"
  723. % (" " * indent, tnode.find("description").text)
  724. )
  725. file_.write(
  726. "%s<keywords>%s</keywords>\n"
  727. % (" " * indent, tnode.find("keywords").text)
  728. )
  729. bnode = tnode.find("binary")
  730. if bnode is not None:
  731. file_.write("%s<binary>\n" % (" " * indent))
  732. indent += 4
  733. for fnode in bnode.findall("file"):
  734. file_.write(
  735. "%s<file>%s</file>\n"
  736. % (" " * indent, os.path.join(options["prefix"], fnode.text))
  737. )
  738. indent -= 4
  739. file_.write("%s</binary>\n" % (" " * indent))
  740. file_.write('%s<libgis revision="%s" />\n' % (" " * indent, libgis_revison))
  741. indent -= 4
  742. file_.write("%s</task>\n" % (" " * indent))
  743. file_.write("</addons>\n")
  744. file_.close()
  745. def write_xml_extensions(name, tree=None):
  746. """Write element tree as a modules matadata file
  747. If the *tree* is not given, an empty file is created.
  748. :param name: file name
  749. :param tree: XML element tree
  750. """
  751. file_ = open(name, "w")
  752. file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  753. file_.write('<!DOCTYPE task SYSTEM "grass-addons.dtd">\n')
  754. file_.write('<addons version="%s">\n' % version[0])
  755. libgis_revison = grass.version()["libgis_revision"]
  756. if tree is not None:
  757. for tnode in tree.findall("task"):
  758. indent = 4
  759. # extension name
  760. file_.write('%s<task name="%s">\n' % (" " * indent, tnode.get("name")))
  761. indent += 4
  762. """
  763. file_.write('%s<description>%s</description>\n' %
  764. (' ' * indent, tnode.find('description').text))
  765. file_.write('%s<keywords>%s</keywords>\n' %
  766. (' ' * indent, tnode.find('keywords').text))
  767. """
  768. # extension files
  769. bnode = tnode.find("binary")
  770. if bnode is not None:
  771. file_.write("%s<binary>\n" % (" " * indent))
  772. indent += 4
  773. for fnode in bnode.findall("file"):
  774. file_.write(
  775. "%s<file>%s</file>\n"
  776. % (" " * indent, os.path.join(options["prefix"], fnode.text))
  777. )
  778. indent -= 4
  779. file_.write("%s</binary>\n" % (" " * indent))
  780. # extension modules
  781. mnode = tnode.find("modules")
  782. if mnode is not None:
  783. file_.write("%s<modules>\n" % (" " * indent))
  784. indent += 4
  785. for fnode in mnode.findall("module"):
  786. file_.write("%s<module>%s</module>\n" % (" " * indent, fnode.text))
  787. indent -= 4
  788. file_.write("%s</modules>\n" % (" " * indent))
  789. file_.write('%s<libgis revision="%s" />\n' % (" " * indent, libgis_revison))
  790. indent -= 4
  791. file_.write("%s</task>\n" % (" " * indent))
  792. file_.write("</addons>\n")
  793. file_.close()
  794. def write_xml_toolboxes(name, tree=None):
  795. """Write element tree as a toolboxes matadata file
  796. If the *tree* is not given, an empty file is created.
  797. :param name: file name
  798. :param tree: XML element tree
  799. """
  800. file_ = open(name, "w")
  801. file_.write('<?xml version="1.0" encoding="UTF-8"?>\n')
  802. file_.write('<!DOCTYPE toolbox SYSTEM "grass-addons.dtd">\n')
  803. file_.write('<addons version="%s">\n' % version[0])
  804. if tree is not None:
  805. for tnode in tree.findall("toolbox"):
  806. indent = 4
  807. file_.write(
  808. '%s<toolbox name="%s" code="%s">\n'
  809. % (" " * indent, tnode.get("name"), tnode.get("code"))
  810. )
  811. indent += 4
  812. for cnode in tnode.findall("correlate"):
  813. file_.write(
  814. '%s<correlate code="%s" />\n' % (" " * indent, tnode.get("code"))
  815. )
  816. for mnode in tnode.findall("task"):
  817. file_.write(
  818. '%s<task name="%s" />\n' % (" " * indent, mnode.get("name"))
  819. )
  820. indent -= 4
  821. file_.write("%s</toolbox>\n" % (" " * indent))
  822. file_.write("</addons>\n")
  823. file_.close()
  824. def install_extension(source, url, xmlurl, branch):
  825. """Install extension (e.g. one module) or a toolbox (list of modules)"""
  826. gisbase = os.getenv("GISBASE")
  827. if not gisbase:
  828. grass.fatal(_("$GISBASE not defined"))
  829. if options["extension"] in get_installed_extensions(force=True):
  830. grass.warning(
  831. _("Extension <%s> already installed. Re-installing...")
  832. % options["extension"]
  833. )
  834. # create a dictionary of extensions
  835. # for each extension
  836. # - a list of modules installed by this extension
  837. # - a list of files installed by this extension
  838. edict = None
  839. if flags["t"]:
  840. grass.message(_("Installing toolbox <%s>...") % options["extension"])
  841. edict = get_toolbox_extensions(xmlurl, options["extension"])
  842. else:
  843. edict = dict()
  844. edict[options["extension"]] = dict()
  845. # list of modules installed by this extension
  846. edict[options["extension"]]["mlist"] = list()
  847. # list of files installed by this extension
  848. edict[options["extension"]]["flist"] = list()
  849. if not edict:
  850. grass.warning(_("Nothing to install"))
  851. return
  852. ret = 0
  853. tmp_dir = None
  854. new_modules = list()
  855. for extension in edict:
  856. ret1 = 0
  857. new_modules_ext = None
  858. if sys.platform == "win32":
  859. ret1, new_modules_ext, new_files_ext = install_extension_win(extension)
  860. else:
  861. (
  862. ret1,
  863. new_modules_ext,
  864. new_files_ext,
  865. tmp_dir,
  866. ) = install_extension_std_platforms(
  867. extension, source=source, url=url, branch=branch
  868. )
  869. if not flags["d"] and not flags["i"]:
  870. edict[extension]["mlist"].extend(new_modules_ext)
  871. edict[extension]["flist"].extend(new_files_ext)
  872. new_modules.extend(new_modules_ext)
  873. ret += ret1
  874. if len(edict) > 1:
  875. print("-" * 60)
  876. if flags["d"] or flags["i"]:
  877. return
  878. if ret != 0:
  879. grass.warning(
  880. _("Installation failed, sorry." " Please check above error messages.")
  881. )
  882. else:
  883. # update extensions metadata file
  884. grass.message(_("Updating extensions metadata file..."))
  885. install_extension_xml(edict)
  886. # update modules metadata file
  887. grass.message(_("Updating extension modules metadata file..."))
  888. install_module_xml(new_modules)
  889. for module in new_modules:
  890. update_manual_page(module)
  891. grass.message(
  892. _("Installation of <%s> successfully finished") % options["extension"]
  893. )
  894. if not os.getenv("GRASS_ADDON_BASE"):
  895. grass.warning(
  896. _(
  897. "This add-on module will not function until"
  898. " you set the GRASS_ADDON_BASE environment"
  899. ' variable (see "g.manual variables")'
  900. )
  901. )
  902. def get_toolboxes_metadata(url):
  903. """Return metadata for all toolboxes from given URL
  904. :param url: URL of a modules matadata file
  905. :param mlist: list of modules to get metadata for
  906. :returns: tuple where first item is dictionary with module names as keys
  907. and dictionary with dest, keyw, files keys as value, the second item
  908. is list of 'binary' files (installation files)
  909. """
  910. data = dict()
  911. try:
  912. tree = etree_fromurl(url)
  913. for tnode in tree.findall("toolbox"):
  914. clist = list()
  915. for cnode in tnode.findall("correlate"):
  916. clist.append(cnode.get("code"))
  917. mlist = list()
  918. for mnode in tnode.findall("task"):
  919. mlist.append(mnode.get("name"))
  920. code = tnode.get("code")
  921. data[code] = {
  922. "name": tnode.get("name"),
  923. "correlate": clist,
  924. "modules": mlist,
  925. }
  926. except (HTTPError, IOError, OSError):
  927. grass.error(_("Unable to read addons metadata file " "from the remote server"))
  928. return data
  929. def install_toolbox_xml(url, name):
  930. """Update local toolboxes metadata file"""
  931. # read metadata from remote server (toolboxes)
  932. url = url + "toolboxes.xml"
  933. data = get_toolboxes_metadata(url)
  934. if not data:
  935. grass.warning(_("No addons metadata available"))
  936. return
  937. if name not in data:
  938. grass.warning(_("No addons metadata available for <%s>") % name)
  939. return
  940. xml_file = os.path.join(options["prefix"], "toolboxes.xml")
  941. # create an empty file if not exists
  942. if not os.path.exists(xml_file):
  943. write_xml_modules(xml_file)
  944. # read XML file
  945. with open(xml_file, "r") as xml:
  946. tree = etree.fromstring(xml.read())
  947. # update tree
  948. tnode = None
  949. for node in tree.findall("toolbox"):
  950. if node.get("code") == name:
  951. tnode = node
  952. break
  953. tdata = data[name]
  954. if tnode is not None:
  955. # update existing node
  956. for cnode in tnode.findall("correlate"):
  957. tnode.remove(cnode)
  958. for mnode in tnode.findall("task"):
  959. tnode.remove(mnode)
  960. else:
  961. # create new node for task
  962. tnode = etree.Element("toolbox", attrib={"name": tdata["name"], "code": name})
  963. tree.append(tnode)
  964. for cname in tdata["correlate"]:
  965. cnode = etree.Element("correlate", attrib={"code": cname})
  966. tnode.append(cnode)
  967. for tname in tdata["modules"]:
  968. mnode = etree.Element("task", attrib={"name": tname})
  969. tnode.append(mnode)
  970. write_xml_toolboxes(xml_file, tree)
  971. def get_addons_metadata(url, mlist):
  972. """Return metadata for list of modules from given URL
  973. :param url: URL of a modules matadata file
  974. :param mlist: list of modules to get metadata for
  975. :returns: tuple where first item is dictionary with module names as keys
  976. and dictionary with dest, keyw, files keys as value, the second item
  977. is list of 'binary' files (installation files)
  978. """
  979. # TODO: extensions with multiple modules
  980. data = {}
  981. bin_list = []
  982. try:
  983. tree = etree_fromurl(url)
  984. except (HTTPError, URLError, IOError, OSError) as error:
  985. grass.error(
  986. _(
  987. "Unable to read addons metadata file" " from the remote server: {0}"
  988. ).format(error)
  989. )
  990. return data, bin_list
  991. except ETREE_EXCEPTIONS as error:
  992. grass.warning(_("Unable to parse '%s': {0}").format(error) % url)
  993. return data, bin_list
  994. for mnode in tree.findall("task"):
  995. name = mnode.get("name")
  996. if name not in mlist:
  997. continue
  998. file_list = list()
  999. bnode = mnode.find("binary")
  1000. windows = sys.platform == "win32"
  1001. if bnode is not None:
  1002. for fnode in bnode.findall("file"):
  1003. path = fnode.text.split("/")
  1004. if path[0] == "bin":
  1005. bin_list.append(path[-1])
  1006. if windows:
  1007. path[-1] += ".exe"
  1008. elif path[0] == "scripts":
  1009. bin_list.append(path[-1])
  1010. if windows:
  1011. path[-1] += ".py"
  1012. file_list.append(os.path.sep.join(path))
  1013. desc, keyw = get_optional_params(mnode)
  1014. data[name] = {
  1015. "desc": desc,
  1016. "keyw": keyw,
  1017. "files": file_list,
  1018. }
  1019. return data, bin_list
  1020. def install_extension_xml(edict):
  1021. """Update XML files with metadata about installed modules and toolbox
  1022. of an private addon
  1023. """
  1024. # TODO toolbox
  1025. # if len(mlist) > 1:
  1026. # # read metadata from remote server (toolboxes)
  1027. # install_toolbox_xml(url, options['extension'])
  1028. xml_file = os.path.join(options["prefix"], "extensions.xml")
  1029. # create an empty file if not exists
  1030. if not os.path.exists(xml_file):
  1031. write_xml_extensions(xml_file)
  1032. # read XML file
  1033. tree = etree_fromfile(xml_file)
  1034. # update tree
  1035. for name in edict:
  1036. # so far extensions do not have description or keywords
  1037. # only modules have
  1038. """
  1039. try:
  1040. desc = gtask.parse_interface(name).description
  1041. # mname = gtask.parse_interface(name).name
  1042. keywords = gtask.parse_interface(name).keywords
  1043. except Exception as e:
  1044. grass.warning(_("No addons metadata available."
  1045. " Addons metadata file not updated."))
  1046. return []
  1047. """
  1048. tnode = None
  1049. for node in tree.findall("task"):
  1050. if node.get("name") == name:
  1051. tnode = node
  1052. break
  1053. if tnode is None:
  1054. # create new node for task
  1055. tnode = etree.Element("task", attrib={"name": name})
  1056. """
  1057. dnode = etree.Element('description')
  1058. dnode.text = desc
  1059. tnode.append(dnode)
  1060. knode = etree.Element('keywords')
  1061. knode.text = (',').join(keywords)
  1062. tnode.append(knode)
  1063. """
  1064. # create binary
  1065. bnode = etree.Element("binary")
  1066. # list of all installed files for this extension
  1067. for file_name in edict[name]["flist"]:
  1068. fnode = etree.Element("file")
  1069. fnode.text = file_name
  1070. bnode.append(fnode)
  1071. tnode.append(bnode)
  1072. # create modules
  1073. msnode = etree.Element("modules")
  1074. # list of all installed modules for this extension
  1075. for module_name in edict[name]["mlist"]:
  1076. mnode = etree.Element("module")
  1077. mnode.text = module_name
  1078. msnode.append(mnode)
  1079. tnode.append(msnode)
  1080. tree.append(tnode)
  1081. else:
  1082. grass.verbose(
  1083. "Extension already listed in metadata file; metadata not updated!"
  1084. )
  1085. write_xml_extensions(xml_file, tree)
  1086. return None
  1087. def get_multi_addon_addons_which_install_only_html_man_page():
  1088. """Get multi-addon addons which install only manual html page
  1089. :return list addons: list of multi-addon addons which install
  1090. only manual html page
  1091. """
  1092. addons = []
  1093. all_addon_dirs = []
  1094. addon_dirs_with_source_module = [] # *.py, *.c file
  1095. addon_pattern = re.compile(r".*{}".format(options["extension"]))
  1096. addon_src_file_pattern = re.compile(r".*.py$|.*.c$")
  1097. addons_paths_file = os.path.join(
  1098. options["prefix"],
  1099. get_addons_paths.json_file,
  1100. )
  1101. if not os.path.exists(addons_paths_file):
  1102. get_addons_paths(gg_addons_base_dir=options["prefix"])
  1103. with open(addons_paths_file) as f:
  1104. addons_paths = json.loads(f.read())
  1105. for addon in addons_paths["tree"]:
  1106. if re.match(addon_pattern, addon["path"]) and addon["type"] == "blob":
  1107. if re.match(addon_src_file_pattern, addon["path"]):
  1108. # Add addon dirs which contains source module *.py, *.c file
  1109. addon_dirs_with_source_module.append(
  1110. os.path.dirname(addon["path"]),
  1111. )
  1112. elif re.match(addon_pattern, addon["path"]) and addon["type"] == "tree":
  1113. # Add all addon dirs
  1114. all_addon_dirs.append(addon["path"])
  1115. for addon in set(all_addon_dirs) ^ set(addon_dirs_with_source_module):
  1116. addons.append(os.path.basename(addon))
  1117. return addons
  1118. def filter_multi_addon_addons(mlist):
  1119. """Filter out list of multi-addon addons which contains
  1120. and installs only *.html manual page, without source/binary
  1121. excutable module and doesn't need to check metadata.
  1122. e.g. the i.sentinel multi-addon consists of several full i.sentinel.*
  1123. addons along with a i.sentinel.html overview file.
  1124. :param list mlist: list of multi-addons (groups of addons
  1125. with respective addon overview HTML pages)
  1126. :return list mlist: list of individual multi-addons without respective
  1127. addon overview HTML pages
  1128. """
  1129. # Filters out add-ons that only contain the *.html man page,
  1130. # e.g. multi-addon i.sentinel (root directory) contains only
  1131. # the *.html manual page for installation, it does not need
  1132. # to check if metadata is available if there is no executable module.
  1133. for addon in get_multi_addon_addons_which_install_only_html_man_page():
  1134. if addon in mlist:
  1135. mlist.pop(mlist.index(addon))
  1136. return mlist
  1137. def install_module_xml(mlist):
  1138. """Update XML files with metadata about installed modules and toolbox
  1139. of an private addon
  1140. """
  1141. xml_file = os.path.join(options["prefix"], "modules.xml")
  1142. # create an empty file if not exists
  1143. if not os.path.exists(xml_file):
  1144. write_xml_modules(xml_file)
  1145. # read XML file
  1146. tree = etree_fromfile(xml_file)
  1147. # Filter multi-addon addons
  1148. if len(mlist) > 1:
  1149. mlist = filter_multi_addon_addons(
  1150. mlist.copy()
  1151. ) # mlist.copy() keep the original list of add-ons
  1152. # update tree
  1153. for name in mlist:
  1154. try:
  1155. desc = gtask.parse_interface(name).description
  1156. # mname = gtask.parse_interface(name).name
  1157. keywords = gtask.parse_interface(name).keywords
  1158. except Exception as error:
  1159. grass.warning(
  1160. _("No metadata available for module '{name}': {error}").format(
  1161. name=name, error=error
  1162. )
  1163. )
  1164. continue
  1165. tnode = None
  1166. for node in tree.findall("task"):
  1167. if node.get("name") == name:
  1168. tnode = node
  1169. break
  1170. if tnode is None:
  1171. # create new node for task
  1172. tnode = etree.Element("task", attrib={"name": name})
  1173. dnode = etree.Element("description")
  1174. dnode.text = desc
  1175. tnode.append(dnode)
  1176. knode = etree.Element("keywords")
  1177. knode.text = (",").join(keywords)
  1178. tnode.append(knode)
  1179. # binary files installed with an extension are now
  1180. # listed in extensions.xml
  1181. """
  1182. # create binary
  1183. bnode = etree.Element('binary')
  1184. list_of_binary_files = []
  1185. for file_name in os.listdir(url):
  1186. file_type = os.path.splitext(file_name)[-1]
  1187. file_n = os.path.splitext(file_name)[0]
  1188. html_path = os.path.join(options['prefix'], 'docs', 'html')
  1189. c_path = os.path.join(options['prefix'], 'bin')
  1190. py_path = os.path.join(options['prefix'], 'scripts')
  1191. # html or image file
  1192. if file_type in ['.html', '.jpg', '.png'] \
  1193. and file_n in os.listdir(html_path):
  1194. list_of_binary_files.append(os.path.join(html_path, file_name))
  1195. # c file
  1196. elif file_type in ['.c'] and file_name in os.listdir(c_path):
  1197. list_of_binary_files.append(os.path.join(c_path, file_n))
  1198. # python file
  1199. elif file_type in ['.py'] and file_name in os.listdir(py_path):
  1200. list_of_binary_files.append(os.path.join(py_path, file_n))
  1201. # man file
  1202. man_path = os.path.join(options['prefix'], 'docs', 'man', 'man1')
  1203. if name + '.1' in os.listdir(man_path):
  1204. list_of_binary_files.append(os.path.join(man_path, name + '.1'))
  1205. # add binaries to xml file
  1206. for binary_file_name in list_of_binary_files:
  1207. fnode = etree.Element('file')
  1208. fnode.text = binary_file_name
  1209. bnode.append(fnode)
  1210. tnode.append(bnode)
  1211. """
  1212. tree.append(tnode)
  1213. else:
  1214. grass.verbose(
  1215. "Extension module already listed in metadata file; metadata not updated!"
  1216. )
  1217. write_xml_modules(xml_file, tree)
  1218. return mlist
  1219. def install_extension_win(name):
  1220. """Install extension on MS Windows"""
  1221. grass.message(
  1222. _("Downloading precompiled GRASS Addons <%s>...") % options["extension"]
  1223. )
  1224. # build base URL
  1225. if build_platform == "x86_64":
  1226. platform = build_platform
  1227. else:
  1228. platform = "x86"
  1229. base_url = (
  1230. "http://wingrass.fsv.cvut.cz/"
  1231. "grass%(major)s%(minor)s/%(platform)s/addons/"
  1232. "grass-%(major)s.%(minor)s.%(patch)s"
  1233. % {
  1234. "platform": platform,
  1235. "major": version[0],
  1236. "minor": version[1],
  1237. "patch": version[2],
  1238. }
  1239. )
  1240. # resolve ZIP URL
  1241. source, url = resolve_source_code(url="{0}/{1}.zip".format(base_url, name))
  1242. # to hide non-error messages from subprocesses
  1243. if grass.verbosity() <= 2:
  1244. outdev = open(os.devnull, "w")
  1245. else:
  1246. outdev = sys.stdout
  1247. # download Addons ZIP file
  1248. os.chdir(TMPDIR) # this is just to not leave something behind
  1249. srcdir = os.path.join(TMPDIR, name)
  1250. download_source_code(
  1251. source=source,
  1252. url=url,
  1253. name=name,
  1254. outdev=outdev,
  1255. directory=srcdir,
  1256. tmpdir=TMPDIR,
  1257. )
  1258. # collect module names and file names
  1259. module_list = list()
  1260. for r, d, f in os.walk(srcdir):
  1261. for file in f:
  1262. # Filter GRASS module name patterns
  1263. if re.search(r"^[d,db,g,i,m,p,ps,r,r3,s,t,v,wx]\..*[\.py,\.exe]$", file):
  1264. modulename = os.path.splitext(file)[0]
  1265. module_list.append(modulename)
  1266. # remove duplicates in case there are .exe wrappers for python scripts
  1267. module_list = set(module_list)
  1268. # change shebang from python to python3
  1269. pyfiles = []
  1270. for r, d, f in os.walk(srcdir):
  1271. for file in f:
  1272. if file.endswith(".py"):
  1273. pyfiles.append(os.path.join(r, file))
  1274. for filename in pyfiles:
  1275. replace_shebang_win(filename)
  1276. # collect old files
  1277. old_file_list = list()
  1278. for r, d, f in os.walk(options["prefix"]):
  1279. for filename in f:
  1280. fullname = os.path.join(r, filename)
  1281. old_file_list.append(fullname)
  1282. # copy Addons copy tree to destination directory
  1283. move_extracted_files(
  1284. extract_dir=srcdir, target_dir=options["prefix"], files=os.listdir(srcdir)
  1285. )
  1286. # collect new files
  1287. file_list = list()
  1288. for r, d, f in os.walk(options["prefix"]):
  1289. for filename in f:
  1290. fullname = os.path.join(r, filename)
  1291. if fullname not in old_file_list:
  1292. file_list.append(fullname)
  1293. return 0, module_list, file_list
  1294. def download_source_code_svn(url, name, outdev, directory=None):
  1295. """Download source code from a Subversion repository
  1296. .. note:
  1297. Stdout is passed to to *outdev* while stderr is will be just printed.
  1298. :param url: URL of the repository
  1299. (module class/family and name are attached)
  1300. :param name: module name
  1301. :param outdev: output divide for the standard output of the svn command
  1302. :param directory: directory where the source code will be downloaded
  1303. (default is the current directory with name attached)
  1304. :returns: full path to the directory with the source code
  1305. (useful when you not specify directory, if *directory* is specified
  1306. the return value is equal to it)
  1307. """
  1308. if not directory:
  1309. directory = os.path.join(os.getcwd, name)
  1310. classchar = name.split(".", 1)[0]
  1311. moduleclass = expand_module_class_name(classchar)
  1312. url = url + "/" + moduleclass + "/" + name
  1313. if grass.call(["svn", "checkout", url, directory], stdout=outdev) != 0:
  1314. grass.fatal(_("GRASS Addons <%s> not found") % name)
  1315. return directory
  1316. def download_source_code_official_github(url, name, outdev, directory=None):
  1317. """Download source code from a official GitHub repository
  1318. .. note:
  1319. Stdout is passed to to *outdev* while stderr is will be just printed.
  1320. :param url: URL of the repository
  1321. (module class/family and name are attached)
  1322. :param name: module name
  1323. :param outdev: output divide for the standard output of the svn command
  1324. :param directory: directory where the source code will be downloaded
  1325. (default is the current directory with name attached)
  1326. :returns: full path to the directory with the source code
  1327. (useful when you not specify directory, if *directory* is specified
  1328. the return value is equal to it)
  1329. """
  1330. if not directory:
  1331. directory = os.path.join(os.getcwd, name)
  1332. if grass.call(["svn", "export", url, directory], stdout=outdev) != 0:
  1333. grass.fatal(_("GRASS Addons <%s> not found") % name)
  1334. return directory
  1335. def move_extracted_files(extract_dir, target_dir, files):
  1336. """Fix state of extracted files by moving them to different directory
  1337. When extracting, it is not clear what will be the root directory
  1338. or if there will be one at all. So this function moves the files to
  1339. a different directory in the way that if there was one directory extracted,
  1340. the contained files are moved.
  1341. """
  1342. gscript.debug("move_extracted_files({0})".format(locals()))
  1343. if len(files) == 1:
  1344. shutil.copytree(os.path.join(extract_dir, files[0]), target_dir)
  1345. else:
  1346. if not os.path.exists(target_dir):
  1347. os.mkdir(target_dir)
  1348. for file_name in files:
  1349. actual_file = os.path.join(extract_dir, file_name)
  1350. if os.path.isdir(actual_file):
  1351. # shutil.copytree() replaced by copy_tree() because
  1352. # shutil's copytree() fails when subdirectory exists
  1353. copy_tree(actual_file, os.path.join(target_dir, file_name))
  1354. else:
  1355. shutil.copy(actual_file, os.path.join(target_dir, file_name))
  1356. # Original copyright and license of the original version of the CRLF function
  1357. # Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010
  1358. # Python Software Foundation; All Rights Reserved
  1359. # Python Software Foundation License Version 2
  1360. # http://svn.python.org/projects/python/trunk/Tools/scripts/crlf.py
  1361. def fix_newlines(directory):
  1362. """Replace CRLF with LF in all files in the directory
  1363. Binary files are ignored. Recurses into subdirectories.
  1364. """
  1365. # skip binary files
  1366. # see https://stackoverflow.com/a/7392391
  1367. textchars = bytearray({7, 8, 9, 10, 12, 13, 27} | set(range(0x20, 0x100)) - {0x7F})
  1368. def is_binary_string(bytes):
  1369. return bool(bytes.translate(None, textchars))
  1370. for root, unused, files in os.walk(directory):
  1371. for name in files:
  1372. filename = os.path.join(root, name)
  1373. if is_binary_string(open(filename, "rb").read(1024)):
  1374. continue # ignore binary files
  1375. # read content of text file
  1376. with open(filename, "rb") as fd:
  1377. data = fd.read()
  1378. # we don't expect there would be CRLF file by
  1379. # purpose if we want to allow CRLF files we would
  1380. # have to whitelite .py etc
  1381. newdata = data.replace(b"\r\n", b"\n")
  1382. if newdata != data:
  1383. with open(filename, "wb") as newfile:
  1384. newfile.write(newdata)
  1385. def extract_zip(name, directory, tmpdir):
  1386. """Extract a ZIP file into a directory"""
  1387. gscript.debug(
  1388. "extract_zip(name={name}, directory={directory},"
  1389. " tmpdir={tmpdir})".format(name=name, directory=directory, tmpdir=tmpdir),
  1390. 3,
  1391. )
  1392. try:
  1393. zip_file = zipfile.ZipFile(name, mode="r")
  1394. file_list = zip_file.namelist()
  1395. # we suppose we can write to parent of the given dir
  1396. # (supposing a tmp dir)
  1397. extract_dir = os.path.join(tmpdir, "extract_dir")
  1398. os.mkdir(extract_dir)
  1399. for subfile in file_list:
  1400. if "__pycache__" in subfile:
  1401. continue
  1402. zip_file.extract(subfile, extract_dir)
  1403. files = os.listdir(extract_dir)
  1404. move_extracted_files(extract_dir=extract_dir, target_dir=directory, files=files)
  1405. except zipfile.BadZipfile as error:
  1406. gscript.fatal(_("ZIP file is unreadable: {0}").format(error))
  1407. # TODO: solve the other related formats
  1408. def extract_tar(name, directory, tmpdir):
  1409. """Extract a TAR or a similar file into a directory"""
  1410. gscript.debug(
  1411. "extract_tar(name={name}, directory={directory},"
  1412. " tmpdir={tmpdir})".format(name=name, directory=directory, tmpdir=tmpdir),
  1413. 3,
  1414. )
  1415. try:
  1416. import tarfile # we don't need it anywhere else
  1417. tar = tarfile.open(name)
  1418. extract_dir = os.path.join(tmpdir, "extract_dir")
  1419. os.mkdir(extract_dir)
  1420. tar.extractall(path=extract_dir)
  1421. files = os.listdir(extract_dir)
  1422. move_extracted_files(extract_dir=extract_dir, target_dir=directory, files=files)
  1423. except tarfile.TarError as error:
  1424. gscript.fatal(_("Archive file is unreadable: {0}").format(error))
  1425. extract_tar.supported_formats = ["tar.gz", "gz", "bz2", "tar", "gzip", "targz"]
  1426. def download_source_code(
  1427. source, url, name, outdev, directory=None, tmpdir=None, branch=None
  1428. ):
  1429. """Get source code to a local directory for compilation"""
  1430. gscript.verbose(_("Type of source identified as '{source}'.").format(source=source))
  1431. if source == "official":
  1432. gscript.message(
  1433. _("Fetching <%s> from " "GRASS GIS Addons repository (be patient)...")
  1434. % name
  1435. )
  1436. download_source_code_official_github(url, name, outdev, directory)
  1437. elif source == "official_fork":
  1438. gscript.message(
  1439. _("Fetching <{name}> from " "<{url}> (be patient)...").format(
  1440. name=name, url=url
  1441. )
  1442. )
  1443. download_source_code_official_github(url, name, outdev, directory)
  1444. elif source == "svn":
  1445. gscript.message(
  1446. _("Fetching <{name}> from " "<{url}> (be patient)...").format(
  1447. name=name, url=url
  1448. )
  1449. )
  1450. download_source_code_svn(url, name, outdev, directory)
  1451. elif source in ["remote_zip"]: # , 'official'
  1452. gscript.message(
  1453. _("Fetching <{name}> from " "<{url}> (be patient)...").format(
  1454. name=name, url=url
  1455. )
  1456. )
  1457. # we expect that the module.zip file is not by chance in the archive
  1458. zip_name = os.path.join(tmpdir, "extension.zip")
  1459. try:
  1460. response = urlopen(url)
  1461. except URLError:
  1462. # Try download add-on from 'master' branch if default "main" fails
  1463. if not branch:
  1464. try:
  1465. url = url.replace("main", "master")
  1466. gscript.message(
  1467. _(
  1468. "Expected default branch not found. "
  1469. "Trying again from <{url}>..."
  1470. ).format(url=url)
  1471. )
  1472. response = urlopen(url)
  1473. except URLError:
  1474. grass.fatal(
  1475. _(
  1476. "Extension <{name}> not found. Please check "
  1477. "'url' and 'branch' options".format(name=name)
  1478. )
  1479. )
  1480. else:
  1481. grass.fatal(_("Extension <%s> not found") % name)
  1482. with open(zip_name, "wb") as out_file:
  1483. shutil.copyfileobj(response, out_file)
  1484. extract_zip(name=zip_name, directory=directory, tmpdir=tmpdir)
  1485. fix_newlines(directory)
  1486. elif (
  1487. source.startswith("remote_")
  1488. and source.split("_")[1] in extract_tar.supported_formats
  1489. ):
  1490. # we expect that the module.tar.gz file is not by chance in the archive
  1491. archive_name = os.path.join(tmpdir, "extension." + source.split("_")[1])
  1492. urlretrieve(url, archive_name)
  1493. extract_tar(name=archive_name, directory=directory, tmpdir=tmpdir)
  1494. fix_newlines(directory)
  1495. elif source == "zip":
  1496. extract_zip(name=url, directory=directory, tmpdir=tmpdir)
  1497. fix_newlines(directory)
  1498. elif source in extract_tar.supported_formats:
  1499. extract_tar(name=url, directory=directory, tmpdir=tmpdir)
  1500. fix_newlines(directory)
  1501. elif source == "dir":
  1502. shutil.copytree(url, directory)
  1503. fix_newlines(directory)
  1504. else:
  1505. # probably programmer error
  1506. grass.fatal(
  1507. _(
  1508. "Unknown extension (addon) source type '{0}'."
  1509. " Please report this to the grass-user mailing list."
  1510. ).format(source)
  1511. )
  1512. assert os.path.isdir(directory)
  1513. def install_extension_std_platforms(name, source, url, branch):
  1514. """Install extension on standard platforms"""
  1515. gisbase = os.getenv("GISBASE")
  1516. # to hide non-error messages from subprocesses
  1517. if grass.verbosity() <= 2:
  1518. outdev = open(os.devnull, "w")
  1519. else:
  1520. outdev = sys.stdout
  1521. os.chdir(TMPDIR) # this is just to not leave something behind
  1522. srcdir = os.path.join(TMPDIR, name)
  1523. download_source_code(
  1524. source=source,
  1525. url=url,
  1526. name=name,
  1527. outdev=outdev,
  1528. directory=srcdir,
  1529. tmpdir=TMPDIR,
  1530. branch=branch,
  1531. )
  1532. os.chdir(srcdir)
  1533. pgm_not_found_message = _(
  1534. "Module name not found." " Check module Makefile syntax (PGM variable)."
  1535. )
  1536. # collect module names
  1537. module_list = list()
  1538. for r, d, f in os.walk(srcdir):
  1539. for filename in f:
  1540. if filename == "Makefile":
  1541. # get the module name: PGM = <module name>
  1542. with open(os.path.join(r, "Makefile")) as fp:
  1543. for line in fp.readlines():
  1544. if re.match(r"PGM.*.=|PGM=", line):
  1545. try:
  1546. modulename = line.split("=")[1].strip()
  1547. if modulename:
  1548. if modulename not in module_list:
  1549. module_list.append(modulename)
  1550. else:
  1551. grass.fatal(pgm_not_found_message)
  1552. except IndexError:
  1553. grass.fatal(pgm_not_found_message)
  1554. # change shebang from python to python3
  1555. pyfiles = []
  1556. # r=root, d=directories, f = files
  1557. for r, d, f in os.walk(srcdir):
  1558. for file in f:
  1559. if file.endswith(".py"):
  1560. pyfiles.append(os.path.join(r, file))
  1561. for filename in pyfiles:
  1562. with fileinput.FileInput(filename, inplace=True) as file:
  1563. for line in file:
  1564. print(
  1565. line.replace("#!/usr/bin/env python\n", "#!/usr/bin/env python3\n"),
  1566. end="",
  1567. )
  1568. dirs = {
  1569. "bin": os.path.join(TMPDIR, name, "bin"),
  1570. "docs": os.path.join(TMPDIR, name, "docs"),
  1571. "html": os.path.join(TMPDIR, name, "docs", "html"),
  1572. "rest": os.path.join(TMPDIR, name, "docs", "rest"),
  1573. "man": os.path.join(TMPDIR, name, "docs", "man"),
  1574. "script": os.path.join(TMPDIR, name, "scripts"),
  1575. # TODO: handle locales also for addons
  1576. # 'string' : os.path.join(TMPDIR, name, 'locale'),
  1577. "string": os.path.join(TMPDIR, name),
  1578. "etc": os.path.join(TMPDIR, name, "etc"),
  1579. }
  1580. make_cmd = [
  1581. "make",
  1582. "MODULE_TOPDIR=%s" % gisbase.replace(" ", r"\ "),
  1583. "RUN_GISRC=%s" % os.environ["GISRC"],
  1584. "BIN=%s" % dirs["bin"],
  1585. "HTMLDIR=%s" % dirs["html"],
  1586. "RESTDIR=%s" % dirs["rest"],
  1587. "MANBASEDIR=%s" % dirs["man"],
  1588. "SCRIPTDIR=%s" % dirs["script"],
  1589. "STRINGDIR=%s" % dirs["string"],
  1590. "ETC=%s" % os.path.join(dirs["etc"]),
  1591. "SOURCE_URL=%s" % url,
  1592. ]
  1593. install_cmd = [
  1594. "make",
  1595. "MODULE_TOPDIR=%s" % gisbase,
  1596. "ARCH_DISTDIR=%s" % os.path.join(TMPDIR, name),
  1597. "INST_DIR=%s" % options["prefix"],
  1598. "install",
  1599. ]
  1600. if flags["d"]:
  1601. grass.message("\n%s\n" % _("To compile run:"))
  1602. sys.stderr.write(" ".join(make_cmd) + "\n")
  1603. grass.message("\n%s\n" % _("To install run:"))
  1604. sys.stderr.write(" ".join(install_cmd) + "\n")
  1605. return 0, None, None, None
  1606. os.chdir(os.path.join(TMPDIR, name))
  1607. grass.message(_("Compiling..."))
  1608. if not os.path.exists(os.path.join(gisbase, "include", "Make", "Module.make")):
  1609. grass.fatal(_("Please install GRASS development package"))
  1610. if 0 != grass.call(make_cmd, stdout=outdev):
  1611. grass.fatal(
  1612. _("Compilation failed, sorry." " Please check above error messages.")
  1613. )
  1614. if flags["i"]:
  1615. return 0, None, None, None
  1616. # collect old files
  1617. old_file_list = list()
  1618. for r, d, f in os.walk(options["prefix"]):
  1619. for filename in f:
  1620. fullname = os.path.join(r, filename)
  1621. old_file_list.append(fullname)
  1622. grass.message(_("Installing..."))
  1623. ret = grass.call(install_cmd, stdout=outdev)
  1624. # collect new files
  1625. file_list = list()
  1626. for r, d, f in os.walk(options["prefix"]):
  1627. for filename in f:
  1628. fullname = os.path.join(r, filename)
  1629. if fullname not in old_file_list:
  1630. file_list.append(fullname)
  1631. return ret, module_list, file_list, os.path.join(TMPDIR, name)
  1632. def remove_extension(force=False):
  1633. """Remove existing extension
  1634. extension or toolbox with extensions if -t is given)"""
  1635. if flags["t"]:
  1636. edict = get_toolbox_extensions(options["prefix"], options["extension"])
  1637. else:
  1638. edict = dict()
  1639. edict[options["extension"]] = dict()
  1640. # list of modules installed by this extension
  1641. edict[options["extension"]]["mlist"] = list()
  1642. # list of files installed by this extension
  1643. edict[options["extension"]]["flist"] = list()
  1644. # collect modules and files installed by these extensions
  1645. mlist = list()
  1646. xml_file = os.path.join(options["prefix"], "extensions.xml")
  1647. if os.path.exists(xml_file):
  1648. # read XML file
  1649. tree = None
  1650. try:
  1651. tree = etree_fromfile(xml_file)
  1652. except ETREE_EXCEPTIONS + (OSError, IOError):
  1653. os.remove(xml_file)
  1654. write_xml_extensions(xml_file)
  1655. if tree is not None:
  1656. for tnode in tree.findall("task"):
  1657. ename = tnode.get("name").strip()
  1658. if ename in edict:
  1659. # modules installed by this extension
  1660. mnode = tnode.find("modules")
  1661. if mnode:
  1662. for fnode in mnode.findall("module"):
  1663. mname = fnode.text.strip()
  1664. edict[ename]["mlist"].append(mname)
  1665. mlist.append(mname)
  1666. # files installed by this extension
  1667. bnode = tnode.find("binary")
  1668. if bnode:
  1669. for fnode in bnode.findall("file"):
  1670. bname = fnode.text.strip()
  1671. edict[ename]["flist"].append(bname)
  1672. else:
  1673. if force:
  1674. write_xml_extensions(xml_file)
  1675. xml_file = os.path.join(options["prefix"], "modules.xml")
  1676. if not os.path.exists(xml_file):
  1677. if force:
  1678. write_xml_modules(xml_file)
  1679. else:
  1680. grass.debug("No addons metadata file available", 1)
  1681. # read XML file
  1682. tree = None
  1683. try:
  1684. tree = etree_fromfile(xml_file)
  1685. except ETREE_EXCEPTIONS + (OSError, IOError):
  1686. os.remove(xml_file)
  1687. write_xml_modules(xml_file)
  1688. return []
  1689. if tree is not None:
  1690. for tnode in tree.findall("task"):
  1691. ename = tnode.get("name").strip()
  1692. if ename in edict:
  1693. # assume extension name == module name
  1694. edict[ename]["mlist"].append(ename)
  1695. mlist.append(ename)
  1696. # files installed by this extension
  1697. bnode = tnode.find("binary")
  1698. if bnode:
  1699. for fnode in bnode.findall("file"):
  1700. bname = fnode.text.strip()
  1701. edict[ename]["flist"].append(bname)
  1702. if force:
  1703. grass.verbose(_("List of removed files:"))
  1704. else:
  1705. grass.info(_("Files to be removed:"))
  1706. eremoved = remove_extension_files(edict, force)
  1707. if force:
  1708. if len(eremoved) > 0:
  1709. grass.message(_("Updating addons metadata file..."))
  1710. remove_extension_xml(mlist, edict)
  1711. for ename in edict:
  1712. if ename in eremoved:
  1713. grass.message(_("Extension <%s> successfully uninstalled.") % ename)
  1714. else:
  1715. if flags["t"]:
  1716. grass.warning(
  1717. _(
  1718. "Toolbox <%s> not removed. "
  1719. "Re-run '%s' with '-f' flag to force removal"
  1720. )
  1721. % (options["extension"], "g.extension")
  1722. )
  1723. else:
  1724. grass.warning(
  1725. _(
  1726. "Extension <%s> not removed. "
  1727. "Re-run '%s' with '-f' flag to force removal"
  1728. )
  1729. % (options["extension"], "g.extension")
  1730. )
  1731. # remove existing extension(s) (reading XML file)
  1732. def remove_extension_files(edict, force=False):
  1733. """Remove extensions specified in a dictionary
  1734. Uses the file names from the file list of the dictionary
  1735. Fallbacks to standard layout of files on prefix path on error.
  1736. """
  1737. # try to read XML metadata file first
  1738. xml_file = os.path.join(options["prefix"], "extensions.xml")
  1739. einstalled = list()
  1740. eremoved = list()
  1741. if os.path.exists(xml_file):
  1742. tree = etree_fromfile(xml_file)
  1743. if tree is not None:
  1744. for task in tree.findall("task"):
  1745. ename = task.get("name").strip()
  1746. einstalled.append(ename)
  1747. else:
  1748. tree = None
  1749. for name in edict:
  1750. removed = True
  1751. if len(edict[name]["flist"]) > 0:
  1752. err = list()
  1753. for fpath in edict[name]["flist"]:
  1754. grass.verbose(fpath)
  1755. if force:
  1756. try:
  1757. os.remove(fpath)
  1758. except OSError:
  1759. msg = "Unable to remove file '%s'"
  1760. err.append((_(msg) % fpath))
  1761. removed = False
  1762. if len(err) > 0:
  1763. for error_line in err:
  1764. grass.error(error_line)
  1765. else:
  1766. if name not in einstalled:
  1767. # try even if module does not seem to be available,
  1768. # as the user may be trying to get rid of left over cruft
  1769. grass.warning(_("Extension <%s> not found") % name)
  1770. remove_extension_std(name, force)
  1771. removed = False
  1772. if removed is True:
  1773. eremoved.append(name)
  1774. return eremoved
  1775. def remove_extension_std(name, force=False):
  1776. """Remove extension/module expecting the standard layout
  1777. Any images for manuals or files installed in etc will not be
  1778. removed
  1779. """
  1780. for fpath in [
  1781. os.path.join(options["prefix"], "bin", name),
  1782. os.path.join(options["prefix"], "scripts", name),
  1783. os.path.join(options["prefix"], "docs", "html", name + ".html"),
  1784. os.path.join(options["prefix"], "docs", "rest", name + ".txt"),
  1785. os.path.join(options["prefix"], "docs", "man", "man1", name + ".1"),
  1786. ]:
  1787. if os.path.isfile(fpath):
  1788. grass.verbose(fpath)
  1789. if force:
  1790. os.remove(fpath)
  1791. # remove module libraries under GRASS_ADDONS/etc/{name}/*
  1792. libpath = os.path.join(options["prefix"], "etc", name)
  1793. if os.path.isdir(libpath):
  1794. grass.verbose(libpath)
  1795. if force:
  1796. shutil.rmtree(libpath)
  1797. def remove_from_toolbox_xml(name):
  1798. """Update local meta-file when removing existing toolbox"""
  1799. xml_file = os.path.join(options["prefix"], "toolboxes.xml")
  1800. if not os.path.exists(xml_file):
  1801. return
  1802. # read XML file
  1803. tree = etree_fromfile(xml_file)
  1804. for node in tree.findall("toolbox"):
  1805. if node.get("code") != name:
  1806. continue
  1807. tree.remove(node)
  1808. write_xml_toolboxes(xml_file, tree)
  1809. def remove_extension_xml(mlist, edict):
  1810. """Update local meta-file when removing existing extension"""
  1811. if len(edict) > 1:
  1812. # update also toolboxes metadata
  1813. remove_from_toolbox_xml(options["extension"])
  1814. # modules
  1815. xml_file = os.path.join(options["prefix"], "modules.xml")
  1816. if os.path.exists(xml_file):
  1817. # read XML file
  1818. tree = etree_fromfile(xml_file)
  1819. for name in mlist:
  1820. for node in tree.findall("task"):
  1821. if node.get("name") != name:
  1822. continue
  1823. tree.remove(node)
  1824. write_xml_modules(xml_file, tree)
  1825. # extensions
  1826. xml_file = os.path.join(options["prefix"], "extensions.xml")
  1827. if os.path.exists(xml_file):
  1828. # read XML file
  1829. tree = etree_fromfile(xml_file)
  1830. for name in edict:
  1831. for node in tree.findall("task"):
  1832. if node.get("name") != name:
  1833. continue
  1834. tree.remove(node)
  1835. write_xml_extensions(xml_file, tree)
  1836. # check links in CSS
  1837. def check_style_file(name):
  1838. """Ensures that a specified HTML documentation support file exists
  1839. If the file, e.g. a CSS file does not exist, the file is copied from
  1840. the distribution.
  1841. If the files are missing, a warning is issued.
  1842. """
  1843. dist_file = os.path.join(os.getenv("GISBASE"), "docs", "html", name)
  1844. addons_file = os.path.join(options["prefix"], "docs", "html", name)
  1845. if os.path.isfile(addons_file):
  1846. return
  1847. try:
  1848. shutil.copyfile(dist_file, addons_file)
  1849. except OSError as error:
  1850. grass.warning(
  1851. _(
  1852. "Unable to create '{filename}': {error}."
  1853. " Is the GRASS GIS documentation package installed?"
  1854. " Installation continues,"
  1855. " but documentation may not look right."
  1856. ).format(filename=addons_file, error=error)
  1857. )
  1858. def create_dir(path):
  1859. """Creates the specified directory (with all dirs in between)
  1860. NOOP for existing directory.
  1861. """
  1862. if os.path.isdir(path):
  1863. return
  1864. try:
  1865. os.makedirs(path)
  1866. except OSError as error:
  1867. grass.fatal(_("Unable to create '%s': %s") % (path, error))
  1868. grass.debug("'%s' created" % path)
  1869. def check_dirs():
  1870. """Ensure that the necessary directories in prefix path exist"""
  1871. create_dir(os.path.join(options["prefix"], "bin"))
  1872. create_dir(os.path.join(options["prefix"], "docs", "html"))
  1873. create_dir(os.path.join(options["prefix"], "docs", "rest"))
  1874. check_style_file("grass_logo.png")
  1875. check_style_file("grassdocs.css")
  1876. create_dir(os.path.join(options["prefix"], "etc"))
  1877. create_dir(os.path.join(options["prefix"], "docs", "man", "man1"))
  1878. create_dir(os.path.join(options["prefix"], "scripts"))
  1879. # fix file URI in manual page
  1880. def update_manual_page(module):
  1881. """Fix manual page for addons which are at different directory
  1882. than core modules"""
  1883. if module.split(".", 1)[0] == "wx":
  1884. return # skip for GUI modules
  1885. grass.verbose(_("Manual page for <%s> updated") % module)
  1886. # read original html file
  1887. htmlfile = os.path.join(options["prefix"], "docs", "html", module + ".html")
  1888. try:
  1889. oldfile = open(htmlfile)
  1890. shtml = oldfile.read()
  1891. except IOError as error:
  1892. gscript.fatal(_("Unable to read manual page: %s") % error)
  1893. else:
  1894. oldfile.close()
  1895. pos = []
  1896. # fix logo URL
  1897. pattern = r'''<a href="([^"]+)"><img src="grass_logo.png"'''
  1898. for match in re.finditer(pattern, shtml):
  1899. if match.group(1)[:4] == "http":
  1900. continue
  1901. pos.append(match.start(1))
  1902. # find URIs
  1903. pattern = r"""<a href="([^"]+)">([^>]+)</a>"""
  1904. addons = get_installed_extensions(force=True)
  1905. # Multi-addon
  1906. if len(addons) > 1:
  1907. for a in get_multi_addon_addons_which_install_only_html_man_page():
  1908. # Add multi-addon addons which install only manual html page
  1909. addons.append(a)
  1910. for match in re.finditer(pattern, shtml):
  1911. if match.group(1)[:4] == "http":
  1912. continue
  1913. if match.group(1).replace(".html", "") in addons:
  1914. continue
  1915. pos.append(match.start(1))
  1916. if not pos:
  1917. return # no match
  1918. # replace file URIs
  1919. prefix = "file://" + "/".join([os.getenv("GISBASE"), "docs", "html"])
  1920. ohtml = shtml[: pos[0]]
  1921. for i in range(1, len(pos)):
  1922. ohtml += prefix + "/" + shtml[pos[i - 1] : pos[i]]
  1923. ohtml += prefix + "/" + shtml[pos[-1] :]
  1924. # write updated html file
  1925. try:
  1926. newfile = open(htmlfile, "w")
  1927. newfile.write(ohtml)
  1928. except IOError as error:
  1929. gscript.fatal(_("Unable for write manual page: %s") % error)
  1930. else:
  1931. newfile.close()
  1932. def resolve_install_prefix(path, to_system):
  1933. """Determine and check the path for installation"""
  1934. if to_system:
  1935. path = os.environ["GISBASE"]
  1936. if path == "$GRASS_ADDON_BASE":
  1937. if not os.getenv("GRASS_ADDON_BASE"):
  1938. grass.warning(
  1939. _("GRASS_ADDON_BASE is not defined, " "installing to ~/.grass%s/addons")
  1940. % version[0]
  1941. )
  1942. path = os.path.join(os.environ["HOME"], ".grass%s" % version[0], "addons")
  1943. else:
  1944. path = os.environ["GRASS_ADDON_BASE"]
  1945. if os.path.exists(path) and not os.access(path, os.W_OK):
  1946. grass.fatal(
  1947. _(
  1948. "You don't have permission to install extension to <{0}>."
  1949. " Try to run {1} with administrator rights"
  1950. " (su or sudo)."
  1951. ).format(path, "g.extension")
  1952. )
  1953. # ensure dir sep at the end for cases where path is used as URL and pasted
  1954. # together with file names
  1955. if not path.endswith(os.path.sep):
  1956. path = path + os.path.sep
  1957. return os.path.abspath(path) # make likes absolute paths
  1958. def resolve_xmlurl_prefix(url, source=None):
  1959. """Determine and check the URL where the XML metadata files are stored
  1960. It ensures that there is a single slash at the end of URL, so we can attach
  1961. file name easily:
  1962. >>> resolve_xmlurl_prefix('https://grass.osgeo.org/addons')
  1963. 'https://grass.osgeo.org/addons/'
  1964. >>> resolve_xmlurl_prefix('https://grass.osgeo.org/addons/')
  1965. 'https://grass.osgeo.org/addons/'
  1966. """
  1967. gscript.debug("resolve_xmlurl_prefix(url={0}, source={1})".format(url, source))
  1968. if source in ("official", "official_fork"):
  1969. # use pregenerated modules XML file
  1970. # Define branch to fetch from (latest or current version)
  1971. version_branch = get_version_branch(version[0])
  1972. url = "https://grass.osgeo.org/addons/{}/".format(version_branch)
  1973. # else try to get extensions XMl from SVN repository (provided URL)
  1974. # the exact action depends on subsequent code (somewhere)
  1975. if not url.endswith("/"):
  1976. url = url + "/"
  1977. return url
  1978. KNOWN_HOST_SERVICES_INFO = {
  1979. "OSGeo Trac": {
  1980. "domain": "trac.osgeo.org",
  1981. "ignored_suffixes": ["format=zip"],
  1982. "possible_starts": ["", "https://", "http://"],
  1983. "url_start": "https://",
  1984. "url_end": "?format=zip",
  1985. },
  1986. "GitHub": {
  1987. "domain": "github.com",
  1988. "ignored_suffixes": [".zip", ".tar.gz"],
  1989. "possible_starts": ["", "https://", "http://"],
  1990. "url_start": "https://",
  1991. "url_end": "/archive/{branch}.zip",
  1992. },
  1993. "GitLab": {
  1994. "domain": "gitlab.com",
  1995. "ignored_suffixes": [".zip", ".tar.gz", ".tar.bz2", ".tar"],
  1996. "possible_starts": ["", "https://", "http://"],
  1997. "url_start": "https://",
  1998. "url_end": "/-/archive/{branch}/{name}-{branch}.zip",
  1999. },
  2000. "Bitbucket": {
  2001. "domain": "bitbucket.org",
  2002. "ignored_suffixes": [".zip", ".tar.gz", ".gz", ".bz2"],
  2003. "possible_starts": ["", "https://", "http://"],
  2004. "url_start": "https://",
  2005. "url_end": "/get/{branch}.zip",
  2006. },
  2007. }
  2008. # TODO: support ZIP URLs which don't end with zip
  2009. # https://gitlab.com/user/reponame/repository/archive.zip?ref=b%C3%A9po
  2010. def resolve_known_host_service(url, name, branch):
  2011. """Determine source type and full URL for known hosting service
  2012. If the service is not determined from the provided URL, tuple with
  2013. is two ``None`` values is returned.
  2014. :param url: URL
  2015. :param name: module name
  2016. """
  2017. match = None
  2018. actual_start = None
  2019. for key, value in KNOWN_HOST_SERVICES_INFO.items():
  2020. for start in value["possible_starts"]:
  2021. if url.startswith(start + value["domain"]):
  2022. match = value
  2023. actual_start = start
  2024. gscript.verbose(
  2025. _("Identified {0} as known hosting service").format(key)
  2026. )
  2027. for suffix in value["ignored_suffixes"]:
  2028. if url.endswith(suffix):
  2029. gscript.verbose(
  2030. _(
  2031. "Not using {service} as known hosting service"
  2032. " because the URL ends with '{suffix}'"
  2033. ).format(service=key, suffix=suffix)
  2034. )
  2035. return None, None
  2036. if match:
  2037. if not actual_start:
  2038. actual_start = match["url_start"]
  2039. else:
  2040. actual_start = ""
  2041. if "branch" in match["url_end"]:
  2042. suffix = match["url_end"].format(
  2043. name=name,
  2044. branch=branch if branch else get_default_branch(url),
  2045. )
  2046. else:
  2047. suffix = match["url_end"].format(name=name)
  2048. url = "{prefix}{base}{suffix}".format(
  2049. prefix=actual_start, base=url.rstrip("/"), suffix=suffix
  2050. )
  2051. gscript.verbose(_("Will use the following URL for download: {0}").format(url))
  2052. return "remote_zip", url
  2053. else:
  2054. return None, None
  2055. # TODO: add also option to enforce the source type
  2056. # TODO: workaround, https://github.com/OSGeo/grass-addons/issues/528
  2057. def resolve_source_code(url=None, name=None, branch=None, fork=False):
  2058. """Return type and URL or path of the source code
  2059. Local paths are not presented as URLs to be usable in standard functions.
  2060. Path is identified as local path if the directory of file exists which
  2061. has the unfortunate consequence that the not existing files are evaluated
  2062. as remote URLs. When path is not evaluated, Subversion is assumed for
  2063. backwards compatibility. When GitHub repository is specified, ZIP file
  2064. link is returned. The ZIP is for {branch} branch, not the default one because
  2065. GitHub does not provide the default branch in the URL (July 2015).
  2066. :returns: tuple with type of source and full URL or path
  2067. Official repository:
  2068. >>> resolve_source_code(name='g.example') # doctest: +SKIP
  2069. ('official', 'https://trac.osgeo.org/.../general/g.example')
  2070. Subversion:
  2071. >>> resolve_source_code('https://svn.osgeo.org/grass/grass-addons/grass7')
  2072. ('svn', 'https://svn.osgeo.org/grass/grass-addons/grass7')
  2073. ZIP files online:
  2074. >>> resolve_source_code('https://trac.osgeo.org/.../r.modis?format=zip') # doctest: +SKIP
  2075. ('remote_zip', 'https://trac.osgeo.org/.../r.modis?format=zip')
  2076. Local directories and ZIP files:
  2077. >>> resolve_source_code(os.path.expanduser("~")) # doctest: +ELLIPSIS
  2078. ('dir', '...')
  2079. >>> resolve_source_code('/local/directory/downloaded.zip') # doctest: +SKIP
  2080. ('zip', '/local/directory/downloaded.zip')
  2081. OSGeo Trac:
  2082. >>> resolve_source_code('trac.osgeo.org/.../r.agent.aco') # doctest: +SKIP
  2083. ('remote_zip', 'https://trac.osgeo.org/.../r.agent.aco?format=zip')
  2084. >>> resolve_source_code('https://trac.osgeo.org/.../r.agent.aco') # doctest: +SKIP
  2085. ('remote_zip', 'https://trac.osgeo.org/.../r.agent.aco?format=zip')
  2086. GitHub:
  2087. >>> resolve_source_code('github.com/user/g.example') # doctest: +SKIP
  2088. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  2089. >>> resolve_source_code('github.com/user/g.example/') # doctest: +SKIP
  2090. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  2091. >>> resolve_source_code('https://github.com/user/g.example') # doctest: +SKIP
  2092. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  2093. >>> resolve_source_code('https://github.com/user/g.example/') # doctest: +SKIP
  2094. ('remote_zip', 'https://github.com/user/g.example/archive/master.zip')
  2095. GitLab:
  2096. >>> resolve_source_code('gitlab.com/JoeUser/GrassModule') # doctest: +SKIP
  2097. ('remote_zip', 'https://gitlab.com/JoeUser/GrassModule/-/archive/master/GrassModule-master.zip')
  2098. >>> resolve_source_code('https://gitlab.com/JoeUser/GrassModule') # doctest: +SKIP
  2099. ('remote_zip', 'https://gitlab.com/JoeUser/GrassModule/-/archive/master/GrassModule-master.zip')
  2100. Bitbucket:
  2101. >>> resolve_source_code('bitbucket.org/joe-user/grass-module') # doctest: +SKIP
  2102. ('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
  2103. >>> resolve_source_code('https://bitbucket.org/joe-user/grass-module') # doctest: +SKIP
  2104. ('remote_zip', 'https://bitbucket.org/joe-user/grass-module/get/default.zip')
  2105. """
  2106. # Handle URL for the offical repo
  2107. if name and (not url or fork):
  2108. module_class = get_module_class_name(name)
  2109. # note: 'trunk' is required to make URL usable for 'svn export' call
  2110. # and fetches the default branch
  2111. if not branch:
  2112. # Fetch from default branch
  2113. version_branch = get_version_branch(version[0])
  2114. try:
  2115. url = url.rstrip("/") if url else GIT_URL
  2116. urlrequest.urlopen(
  2117. "{url}/tree/{version_branch}/src".format(
  2118. url=url, version_branch=version_branch
  2119. )
  2120. )
  2121. svn_reference = "branches/{}".format(version_branch)
  2122. except URLError:
  2123. svn_reference = "trunk"
  2124. else:
  2125. svn_reference = "branches/{}".format(branch)
  2126. if not url or url == GIT_URL:
  2127. # Set URL for the given GRASS version
  2128. git_url = "{GIT_URL}/{svn_reference}/src/{module_class}/{name}".format(
  2129. GIT_URL=GIT_URL,
  2130. svn_reference=svn_reference,
  2131. module_class=module_class,
  2132. name=name,
  2133. )
  2134. return "official", git_url
  2135. else:
  2136. # Forks from the official repo should reflect the current structure
  2137. url = url.rstrip("/")
  2138. git_url = "{url}/{svn_reference}/src/{module_class}/{name}".format(
  2139. url=url,
  2140. svn_reference=svn_reference,
  2141. module_class=module_class,
  2142. name=name,
  2143. )
  2144. return "official_fork", git_url
  2145. # Check if URL can be found
  2146. # Catch corner case if local URL is given starting with file://
  2147. url = url[6:] if url.startswith("file://") else url
  2148. if not os.path.exists(url):
  2149. url_validated = False
  2150. if url.startswith("http"):
  2151. try:
  2152. open_url = urlopen(url)
  2153. open_url.close()
  2154. url_validated = True
  2155. except URLError:
  2156. pass
  2157. else:
  2158. try:
  2159. open_url = urlopen("http://" + url)
  2160. open_url.close()
  2161. url_validated = True
  2162. except URLError:
  2163. pass
  2164. try:
  2165. open_url = urlopen("https://" + url)
  2166. open_url.close()
  2167. url_validated = True
  2168. except URLError:
  2169. pass
  2170. if not url_validated:
  2171. grass.fatal(_("Cannot open URL: {}".format(url)))
  2172. # Handle local URLs
  2173. if os.path.isdir(url):
  2174. return "dir", os.path.abspath(url)
  2175. elif os.path.exists(url):
  2176. if url.endswith(".zip"):
  2177. return "zip", os.path.abspath(url)
  2178. for suffix in extract_tar.supported_formats:
  2179. if url.endswith("." + suffix):
  2180. return suffix, os.path.abspath(url)
  2181. # Handle remote URLs
  2182. else:
  2183. source, resolved_url = resolve_known_host_service(url, name, branch)
  2184. if source:
  2185. return source, resolved_url
  2186. # we allow URL to end with =zip or ?zip and not only .zip
  2187. # unfortunately format=zip&version=89612 would require something else
  2188. # special option to force the source type would solve it
  2189. if url.endswith("zip"):
  2190. return "remote_zip", url
  2191. for suffix in extract_tar.supported_formats:
  2192. if url.endswith(suffix):
  2193. return "remote_" + suffix, url
  2194. # fallback to the classic behavior
  2195. return "svn", url
  2196. def get_addons_paths(gg_addons_base_dir):
  2197. """Get and save addons paths from GRASS GIS Addons GitHub repo API
  2198. as 'addons_paths.json' file in the gg_addons_base_dir. The file
  2199. serves as a list of all addons, and their paths (required for
  2200. mkhmtl.py tool)
  2201. :param str gg_addons_base_dir: dir path where addons are installed
  2202. """
  2203. # Define branch to fetch from (latest or current version)
  2204. addons_branch = get_version_branch(version[0])
  2205. url = "https://api.github.com/repos/OSGeo/grass-addons/git/trees/{}?recursive=1".format(
  2206. addons_branch
  2207. )
  2208. response = download_addons_paths_file(
  2209. url=url,
  2210. response_format="application/json",
  2211. )
  2212. if response:
  2213. addons_paths = json.loads(gscript.decode(response.read()))
  2214. with open(
  2215. os.path.join(gg_addons_base_dir, get_addons_paths.json_file), "w"
  2216. ) as f:
  2217. json.dump(addons_paths, f)
  2218. get_addons_paths.json_file = "addons_paths.json"
  2219. def main():
  2220. # check dependencies
  2221. if not flags["a"] and sys.platform != "win32":
  2222. check_progs()
  2223. original_url = options["url"]
  2224. branch = options["branch"]
  2225. # manage proxies
  2226. global PROXIES
  2227. if options["proxy"]:
  2228. PROXIES = {}
  2229. for ptype, purl in (p.split("=") for p in options["proxy"].split(",")):
  2230. PROXIES[ptype] = purl
  2231. proxy = urlrequest.ProxyHandler(PROXIES)
  2232. opener = urlrequest.build_opener(proxy)
  2233. urlrequest.install_opener(opener)
  2234. # Required for mkhtml.py script (get addon git commit from GitHub API server)
  2235. os.environ["GRASS_PROXY"] = options["proxy"]
  2236. # define path
  2237. options["prefix"] = resolve_install_prefix(
  2238. path=options["prefix"], to_system=flags["s"]
  2239. )
  2240. if flags["j"]:
  2241. get_addons_paths(gg_addons_base_dir=options["prefix"])
  2242. return 0
  2243. # list available extensions
  2244. if flags["l"] or flags["c"] or (flags["g"] and not flags["a"]):
  2245. # using dummy extension, we don't need any extension URL now,
  2246. # but will work only as long as the function does not check
  2247. # if the URL is actually valid or something
  2248. source, url = resolve_source_code(
  2249. name="dummy", url=original_url, branch=branch, fork=flags["o"]
  2250. )
  2251. xmlurl = resolve_xmlurl_prefix(original_url, source=source)
  2252. list_available_extensions(xmlurl)
  2253. return 0
  2254. elif flags["a"]:
  2255. list_installed_extensions(toolboxes=flags["t"])
  2256. return 0
  2257. if flags["d"] or flags["i"]:
  2258. flag = "d" if flags["d"] else "i"
  2259. if options["operation"] != "add":
  2260. grass.warning(
  2261. _(
  2262. "Flag '{}' is relevant only to"
  2263. " 'operation=add'. Ignoring this flag."
  2264. ).format(flag)
  2265. )
  2266. else:
  2267. global REMOVE_TMPDIR
  2268. REMOVE_TMPDIR = False
  2269. if options["operation"] == "add":
  2270. check_dirs()
  2271. if original_url == "" or flags["o"]:
  2272. """
  2273. Query GitHub API only if extension will be downloaded
  2274. from official GRASS GIS addon repository
  2275. """
  2276. get_addons_paths(gg_addons_base_dir=options["prefix"])
  2277. source, url = resolve_source_code(
  2278. name=options["extension"], url=original_url, branch=branch, fork=flags["o"]
  2279. )
  2280. xmlurl = resolve_xmlurl_prefix(original_url, source=source)
  2281. install_extension(source=source, url=url, xmlurl=xmlurl, branch=branch)
  2282. else: # remove
  2283. remove_extension(force=flags["f"])
  2284. return 0
  2285. if __name__ == "__main__":
  2286. if len(sys.argv) == 2 and sys.argv[1] == "--doctest":
  2287. import doctest
  2288. sys.exit(doctest.testmod().failed)
  2289. options, flags = grass.parser()
  2290. global TMPDIR
  2291. TMPDIR = tempfile.mkdtemp()
  2292. atexit.register(cleanup)
  2293. grass_version = grass.version()
  2294. version = grass_version["version"].split(".")
  2295. build_platform = grass_version["build_platform"].split("-", 1)[0]
  2296. sys.exit(main())