install.py 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152
  1. #!/usr/bin/env python
  2. """\
  3. @file install.py
  4. @author Phoenix
  5. @date 2008-01-27
  6. @brief Install files into an indra checkout.
  7. Install files as specified by:
  8. https://wiki.lindenlab.com/wiki/User:Phoenix/Library_Installation
  9. $LicenseInfo:firstyear=2007&license=mit$
  10. Copyright (c) 2007-2009, Linden Research, Inc, (c) 2009-2024 Henri Beauchamp
  11. Permission is hereby granted, free of charge, to any person obtaining a copy
  12. of this software and associated documentation files (the "Software"), to deal
  13. in the Software without restriction, including without limitation the rights
  14. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  15. copies of the Software, and to permit persons to whom the Software is
  16. furnished to do so, subject to the following conditions:
  17. The above copyright notice and this permission notice shall be included in
  18. all copies or substantial portions of the Software.
  19. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. THE SOFTWARE.
  26. $/LicenseInfo$
  27. """
  28. from __future__ import print_function
  29. import sys
  30. import os.path
  31. # Look for scripts/lib/python in all possible parent directories ...
  32. # This is an improvement over the setup-path.py method used previously:
  33. # * the script may be located anywhere inside the source tree
  34. # * it does not depend on the current directory
  35. # * it does not depend on another file being present.
  36. def add_indra_lib_path():
  37. root = os.path.realpath(__file__)
  38. # always insert the directory of the script in the search path
  39. dir = os.path.dirname(root)
  40. if dir not in sys.path:
  41. sys.path.insert(0, dir)
  42. # Now go look for scripts/lib/python in the parent dirs
  43. while root != os.path.sep:
  44. root = os.path.dirname(root)
  45. dir = os.path.join(root, 'scripts', 'lib', 'python')
  46. if os.path.isdir(dir):
  47. if dir not in sys.path:
  48. sys.path.insert(0, dir)
  49. return root
  50. else:
  51. print("This script is not inside a valid source tree.", file=sys.stderr)
  52. sys.exit(1)
  53. base_dir = add_indra_lib_path()
  54. import copy
  55. import optparse
  56. import os
  57. import platform
  58. import pprint
  59. import shutil
  60. import tarfile
  61. import tempfile
  62. import textwrap
  63. if sys.version_info[0] == 3:
  64. import urllib.request as urllib_request
  65. import urllib.parse as urllib_parse
  66. else:
  67. import urllib2 as urllib_request
  68. import urlparse as urllib_parse
  69. from hashlib import md5
  70. from indra import llsd
  71. class Formatter(optparse.IndentedHelpFormatter):
  72. def __init__(
  73. self,
  74. p_indentIncrement = 2,
  75. p_maxHelpPosition = 24,
  76. p_width = 79,
  77. p_shortFirst = 1) :
  78. optparse.HelpFormatter.__init__(
  79. self,
  80. p_indentIncrement,
  81. p_maxHelpPosition,
  82. p_width,
  83. p_shortFirst)
  84. def format_description(self, p_description):
  85. t_descWidth = self.width - self.current_indent
  86. t_indent = " " * (self.current_indent + 2)
  87. return "\n".join(
  88. [textwrap.fill(descr, t_descWidth, initial_indent = t_indent,
  89. subsequent_indent = t_indent)
  90. for descr in p_description.split("\n")] )
  91. class InstallFile(object):
  92. "This is just a handy way to throw around details on a file in memory."
  93. def __init__(self, pkgname, url, md5sum, cache_dir, platform_path):
  94. self.pkgname = pkgname
  95. self.url = url
  96. self.md5sum = md5sum
  97. filename = urllib_parse.urlparse(url)[2].split('/')[-1]
  98. self.filename = os.path.join(cache_dir, filename)
  99. self.platform_path = platform_path
  100. def __str__(self):
  101. return "ifile{%s:%s}" % (self.pkgname, self.url)
  102. def _is_md5sum_match(self):
  103. hasher = md5(open(self.filename, 'rb').read())
  104. if hasher.hexdigest() == self.md5sum:
  105. return True
  106. return False
  107. def is_match(self, platform):
  108. """@brief Test to see if this ifile is part of platform
  109. @param platform The target platform. Eg, linux64 or windows64
  110. @return Returns True if the ifile is in the platform.
  111. """
  112. if self.platform_path[0] == 'common':
  113. return True
  114. req_platform_path = platform.split('/')
  115. #print "platform:",req_platform_path
  116. #print "path:",self.platform_path
  117. # to match, every path part much match
  118. match_count = min(len(req_platform_path), len(self.platform_path))
  119. for ii in range(0, match_count):
  120. if req_platform_path[ii] != self.platform_path[ii]:
  121. return False
  122. #print "match!"
  123. return True
  124. def fetch_local(self):
  125. #print "Looking for:",self.filename
  126. if not os.path.exists(self.filename):
  127. pass
  128. elif self.md5sum and not self._is_md5sum_match():
  129. print("md5 mismatch:", self.filename)
  130. os.remove(self.filename)
  131. else:
  132. print("Found matching package:", self.filename)
  133. return
  134. print("Downloading",self.url,"to local file",self.filename)
  135. open(self.filename, 'wb').write(urllib_request.urlopen(self.url).read())
  136. if self.md5sum and not self._is_md5sum_match():
  137. raise RuntimeError("Error matching md5 for %s" % self.url)
  138. class LicenseDefinition(object):
  139. def __init__(self, definition):
  140. #probably looks like:
  141. # { text : ...,
  142. # url : ...
  143. # blessed : ...
  144. # }
  145. self._definition = definition
  146. class InstallableDefinition(object):
  147. def __init__(self, definition):
  148. #probably looks like:
  149. # { packages : {platform...},
  150. # copyright : ...
  151. # license : ...
  152. # description: ...
  153. # }
  154. self._definition = definition
  155. def _ifiles_from(self, tree, pkgname, cache_dir):
  156. return self._ifiles_from_path(tree, pkgname, cache_dir, [])
  157. def _ifiles_from_path(self, tree, pkgname, cache_dir, path):
  158. ifiles = []
  159. if 'url' in tree:
  160. ifiles.append(InstallFile(
  161. pkgname,
  162. tree['url'],
  163. tree.get('md5sum', None),
  164. cache_dir,
  165. path))
  166. else:
  167. for key in tree:
  168. platform_path = copy.copy(path)
  169. platform_path.append(key)
  170. ifiles.extend(
  171. self._ifiles_from_path(
  172. tree[key],
  173. pkgname,
  174. cache_dir,
  175. platform_path))
  176. return ifiles
  177. def ifiles(self, pkgname, platform, cache_dir):
  178. """@brief return a list of appropriate InstallFile instances to install
  179. @param pkgname The name of the package to be installed, eg 'tut'
  180. @param platform The target platform. Eg, linux64 or windows64
  181. @param cache_dir The directory to cache downloads.
  182. @return Returns a list of InstallFiles which are part of this install
  183. """
  184. if 'packages' not in self._definition:
  185. return []
  186. all_ifiles = self._ifiles_from(
  187. self._definition['packages'],
  188. pkgname,
  189. cache_dir)
  190. if platform == 'all':
  191. return all_ifiles
  192. #print "Considering", len(all_ifiles), "packages for", pkgname
  193. # split into 2 lines because pychecker thinks it might return none.
  194. files = [ifile for ifile in all_ifiles if ifile.is_match(platform)]
  195. return files
  196. class InstalledPackage(object):
  197. def __init__(self, definition):
  198. # looks like:
  199. # { url1 : { files: [file1,file2,...], md5sum:... },
  200. # url2 : { files: [file1,file2,...], md5sum:... },...
  201. # }
  202. self._installed = {}
  203. for url in definition:
  204. self._installed[url] = definition[url]
  205. def urls(self):
  206. return list(self._installed.keys())
  207. def files_in(self, url):
  208. return self._installed[url].get('files', [])
  209. def get_md5sum(self, url):
  210. return self._installed[url].get('md5sum', None)
  211. def remove(self, url):
  212. self._installed.pop(url)
  213. def add_files(self, url, files):
  214. if url not in self._installed:
  215. self._installed[url] = {}
  216. self._installed[url]['files'] = files
  217. def set_md5sum(self, url, md5sum):
  218. if url not in self._installed:
  219. self._installed[url] = {}
  220. self._installed[url]['md5sum'] = md5sum
  221. class Installer(object):
  222. def __init__(self, install_filename, installed_filename, dryrun):
  223. self._install_filename = install_filename
  224. self._install_changed = False
  225. self._installed_filename = installed_filename
  226. self._installed_changed = False
  227. self._dryrun = dryrun
  228. self._installables = {}
  229. self._licenses = {}
  230. self._installed = {}
  231. self.load()
  232. def load(self):
  233. if os.path.exists(self._install_filename):
  234. install = llsd.parse(open(self._install_filename, 'rb').read())
  235. try:
  236. for name in install['installables']:
  237. self._installables[name] = InstallableDefinition(
  238. install['installables'][name])
  239. except KeyError:
  240. pass
  241. try:
  242. for name in install['licenses']:
  243. self._licenses[name] = LicenseDefinition(install['licenses'][name])
  244. except KeyError:
  245. pass
  246. if os.path.exists(self._installed_filename):
  247. installed = llsd.parse(open(self._installed_filename, 'rb').read())
  248. try:
  249. bins = installed['installables']
  250. for name in bins:
  251. self._installed[name] = InstalledPackage(bins[name])
  252. except KeyError:
  253. pass
  254. def _write(self, filename, state):
  255. print("Writing state to",filename)
  256. if not self._dryrun:
  257. open(filename, 'wb').write(llsd.format_pretty_xml(state))
  258. def save(self):
  259. if self._install_changed:
  260. state = {}
  261. state['licenses'] = {}
  262. for name in self._licenses:
  263. state['licenses'][name] = self._licenses[name]._definition
  264. #print "self._installables:",self._installables
  265. state['installables'] = {}
  266. for name in self._installables:
  267. state['installables'][name] = \
  268. self._installables[name]._definition
  269. self._write(self._install_filename, state)
  270. if self._installed_changed:
  271. state = {}
  272. state['installables'] = {}
  273. bin = state['installables']
  274. for name in self._installed:
  275. #print "installed:",name,self._installed[name]._installed
  276. bin[name] = self._installed[name]._installed
  277. self._write(self._installed_filename, state)
  278. def is_valid_license(self, bin):
  279. "@brief retrun true if we have valid license info for installable."
  280. installable = self._installables[bin]._definition
  281. if 'license' not in installable:
  282. print("No license info found for", bin, file=sys.stderr)
  283. print('Please add the license with the', end=' ', file=sys.stderr)
  284. print('--add-installable option. See', \
  285. sys.argv[0], '--help', file=sys.stderr)
  286. return False
  287. if installable['license'] not in self._licenses:
  288. lic = installable['license']
  289. print("Missing license info for '" + lic + "'.", end=' ', file=sys.stderr)
  290. print('Please add the license with the', end=' ', file=sys.stderr)
  291. print('--add-license option. See', sys.argv[0], end=' ', file=sys.stderr)
  292. print('--help', file=sys.stderr)
  293. return False
  294. return True
  295. def list_installables(self):
  296. "Return a list of all known installables."
  297. return sorted(self._installables.keys())
  298. def detail_installable(self, name):
  299. "Return a installable definition detail"
  300. return self._installables[name]._definition
  301. def list_licenses(self):
  302. "Return a list of all known licenses."
  303. return sorted(self._licenses.keys())
  304. def detail_license(self, name):
  305. "Return a license definition detail"
  306. return self._licenses[name]._definition
  307. def list_installed(self):
  308. "Return a list of installed packages."
  309. return sorted(self._installed.keys())
  310. def detail_installed(self, name):
  311. "Return file list for specific installed package."
  312. filelist = []
  313. for url in list(self._installed[name]._installed.keys()):
  314. filelist.extend(self._installed[name].files_in(url))
  315. return filelist
  316. def _update_field(self, description, field, value, multiline=False):
  317. """Given a block and a field name, add or update it.
  318. @param description a dict containing all the details of a description.
  319. @param field the name of the field to update.
  320. @param value the value of the field to update; if omitted, interview
  321. will ask for value.
  322. @param multiline boolean specifying whether field is multiline or not.
  323. """
  324. if value:
  325. description[field] = value
  326. else:
  327. if field in description:
  328. print("Update value for '" + field + "'")
  329. print("(Leave blank to keep current value)")
  330. print("Current Value: '" + description[field] + "'")
  331. else:
  332. print("Specify value for '" + field + "'")
  333. if not multiline:
  334. new_value = input("Enter New Value: ")
  335. else:
  336. print("Please enter " + field + ". End input with EOF (^D).")
  337. new_value = sys.stdin.read()
  338. if field in description and not new_value:
  339. pass
  340. elif new_value:
  341. description[field] = new_value
  342. self._install_changed = True
  343. return True
  344. def _update_installable(self, name, platform, url, md5sum):
  345. """Update installable entry with specific package information.
  346. @param installable[in,out] a dict containing installable details.
  347. @param platform Platform info, i.e. linux64 or windows64, etc.
  348. @param url URL of tar file
  349. @param md5sum md5sum of tar file
  350. """
  351. installable = self._installables[name]._definition
  352. path = platform.split('/')
  353. if 'packages' not in installable:
  354. installable['packages'] = {}
  355. update = installable['packages']
  356. for child in path:
  357. if child not in update:
  358. update[child] = {}
  359. parent = update
  360. update = update[child]
  361. parent[child]['url'] = llsd.uri(url)
  362. parent[child]['md5sum'] = md5sum
  363. self._install_changed = True
  364. return True
  365. def add_installable_package(self, name, **kwargs):
  366. """Add an url for a platform path to the installable.
  367. @param installable[in,out] a dict containing installable details.
  368. """
  369. platform_help_str = """\
  370. Please enter a new package location and url. Some examples:
  371. common -- specify a package for all platforms
  372. darwin64 -- specify a package for macOS
  373. linux64 -- specify a package for linux
  374. windows64 -- specify a package for windows"""
  375. if name not in self._installables:
  376. print("Error: must add library with --add-installable or " \
  377. +"--add-installable-metadata before using " \
  378. +"--add-installable-package option")
  379. return False
  380. else:
  381. print("Updating installable '" + name + "'.")
  382. for arg in ('platform', 'url', 'md5sum'):
  383. if not kwargs[arg]:
  384. if arg == 'platform':
  385. print(platform_help_str)
  386. kwargs[arg] = input("Package "+arg+":")
  387. #path = kwargs['platform'].split('/')
  388. return self._update_installable(name, kwargs['platform'],
  389. kwargs['url'], kwargs['md5sum'])
  390. def add_installable_metadata(self, name, **kwargs):
  391. """Interactively add (only) library metadata into install,
  392. w/o adding installable"""
  393. if name not in self._installables:
  394. print("Adding installable '" + name + "'.")
  395. self._installables[name] = InstallableDefinition({})
  396. else:
  397. print("Updating installable '" + name + "'.")
  398. installable = self._installables[name]._definition
  399. for field in ('copyright', 'license', 'description'):
  400. self._update_field(installable, field, kwargs[field])
  401. print("Added installable '" + name + "':")
  402. pprint.pprint(self._installables[name])
  403. return True
  404. def add_installable(self, name, **kwargs):
  405. "Interactively pull a new installable into the install"
  406. ret_a = self.add_installable_metadata(name, **kwargs)
  407. ret_b = self.add_installable_package(name, **kwargs)
  408. return (ret_a and ret_b)
  409. def remove_installable(self, name):
  410. self._installables.pop(name)
  411. self._install_changed = True
  412. def add_license(self, name, **kwargs):
  413. if name not in self._licenses:
  414. print("Adding license '" + name + "'.")
  415. self._licenses[name] = LicenseDefinition({})
  416. else:
  417. print("Updating license '" + name + "'.")
  418. the_license = self._licenses[name]._definition
  419. for field in ('url', 'text'):
  420. multiline = False
  421. if field == 'text':
  422. multiline = True
  423. self._update_field(the_license, field, kwargs[field], multiline)
  424. self._install_changed = True
  425. return True
  426. def remove_license(self, name):
  427. self._licenses.pop(name)
  428. self._install_changed = True
  429. def _uninstall(self, installables):
  430. """@brief Do the actual removal of files work.
  431. *NOTE: This method is not transactionally safe -- ie, if it
  432. raises an exception, internal state may be inconsistent. How
  433. should we address this?
  434. @param installables The package names to remove
  435. """
  436. remove_file_list = []
  437. for pkgname in installables:
  438. for url in self._installed[pkgname].urls():
  439. remove_file_list.extend(
  440. self._installed[pkgname].files_in(url))
  441. self._installed[pkgname].remove(url)
  442. if not self._dryrun:
  443. self._installed_changed = True
  444. if not self._dryrun:
  445. self._installed.pop(pkgname)
  446. remove_dir_set = set()
  447. for filename in remove_file_list:
  448. print("rm",filename)
  449. if not self._dryrun:
  450. if os.path.lexists(filename):
  451. remove_dir_set.add(os.path.dirname(filename))
  452. try:
  453. os.remove(filename)
  454. except OSError:
  455. # This is just for cleanup, so we don't care
  456. # about normal failures.
  457. pass
  458. for dirname in remove_dir_set:
  459. try:
  460. os.removedirs(dirname)
  461. except OSError:
  462. # This is just for cleanup, so we don't care about
  463. # normal failures.
  464. pass
  465. def uninstall(self, installables, install_dir):
  466. """@brief Remove the packages specified.
  467. @param installables The package names to remove
  468. @param install_dir The directory to work from
  469. """
  470. print("uninstall",installables,"from",install_dir)
  471. cwd = os.getcwd()
  472. os.chdir(install_dir)
  473. try:
  474. self._uninstall(installables)
  475. finally:
  476. os.chdir(cwd)
  477. def _build_ifiles(self, platform, cache_dir):
  478. """@brief determine what files to install
  479. @param platform The target platform. Eg, linux64 or windows64
  480. @param cache_dir The directory to cache downloads.
  481. @return Returns the ifiles to install
  482. """
  483. ifiles = []
  484. for bin in self._installables:
  485. ifiles.extend(self._installables[bin].ifiles(bin,
  486. platform,
  487. cache_dir))
  488. to_install = []
  489. #print "self._installed",self._installed
  490. for ifile in ifiles:
  491. if ifile.pkgname not in self._installed:
  492. to_install.append(ifile)
  493. elif ifile.url not in self._installed[ifile.pkgname].urls():
  494. to_install.append(ifile)
  495. elif ifile.md5sum != \
  496. self._installed[ifile.pkgname].get_md5sum(ifile.url):
  497. # *TODO: We may want to uninstall the old version too
  498. # when we detect it is installed, but the md5 sum is
  499. # different.
  500. to_install.append(ifile)
  501. else:
  502. #print "Installation up to date:",
  503. # ifile.pkgname,ifile.platform_path
  504. pass
  505. #print "to_install",to_install
  506. return to_install
  507. def _install(self, to_install, install_dir):
  508. for ifile in to_install:
  509. print("Extracting",ifile.filename,"to",install_dir)
  510. tfile=ifile.filename
  511. # Note; I do not like the idea to impose the pyzst module
  512. # installation to build the Cool VL Viewer, so I adopted another
  513. # solution to still be able to use the newest zst-compressed
  514. # pre-built libraries from LL: For Windows, the build system
  515. # automatically downloads a zstd.exe tool and we use it here to
  516. # uncompress the *.tar.zst archives into *.tar ones which Python
  517. # can *natively* process by itself. HB
  518. if tfile.endswith(".zst"):
  519. utfile=tfile[:-4]
  520. if not os.path.isfile(utfile):
  521. print('Calling zstd to uncompress',tfile,'to',utfile)
  522. if _get_platform() == 'windows' or _get_platform() == 'darwin':
  523. zstd='zstd'
  524. if _get_platform() == 'windows':
  525. zstd='zstd.exe'
  526. # Make sure there will be no unquoted space in file
  527. # names and, since our zstd[.exe] utility is in the
  528. # bin/ subdirectory of the viewer sources tree, and the
  529. # latter might be held in a folder with space in its
  530. # name, we use chdir() to overcome this issue. HB
  531. cmd=os.path.join(".","bin",zstd)
  532. cmd=cmd+' -d "'+tfile+'" -o "'+utfile+'"'
  533. cudir=os.getcwd()
  534. os.chdir(base_dir)
  535. os.system(cmd)
  536. os.chdir(cudir)
  537. else:
  538. # We boldly assume that zstd is installed by default on
  539. # the build system... But since we actually do not use
  540. # *.tar.zst archives for our own Linux pre-build
  541. # libraries, this cannot be an issue for now. HB
  542. os.system('zstd -d "'+tfile+'"')
  543. tfile=utfile
  544. tar = tarfile.open(tfile, 'r')
  545. if not self._dryrun:
  546. tar.extractall(path=install_dir)
  547. if ifile.pkgname in self._installed:
  548. self._installed[ifile.pkgname].add_files(ifile.url,tar.getnames())
  549. self._installed[ifile.pkgname].set_md5sum(ifile.url,ifile.md5sum)
  550. else:
  551. # *HACK: this understands the installed package syntax.
  552. definition = { ifile.url :
  553. {'files': tar.getnames(),
  554. 'md5sum' : ifile.md5sum } }
  555. self._installed[ifile.pkgname] = InstalledPackage(definition)
  556. self._installed_changed = True
  557. tar.close()
  558. if tfile != ifile.filename:
  559. os.remove(tfile)
  560. def install(self, installables, platform, install_dir, cache_dir):
  561. """@brief Do the installation for for the platform.
  562. @param installables The requested installables to install.
  563. @param platform The target platform. Eg, linux64 or windows64
  564. @param install_dir The root directory to install into. Created
  565. if missing.
  566. @param cache_dir The directory to cache downloads. Created if
  567. missing.
  568. """
  569. # The ordering of steps in the method is to help reduce the
  570. # likelihood that we break something.
  571. install_dir = os.path.realpath(install_dir)
  572. cache_dir = os.path.realpath(cache_dir)
  573. _mkdir(install_dir)
  574. _mkdir(cache_dir)
  575. to_install = self._build_ifiles(platform, cache_dir)
  576. # Filter for files which we actually requested to install.
  577. to_install = [ifl for ifl in to_install if ifl.pkgname in installables]
  578. for ifile in to_install:
  579. ifile.fetch_local()
  580. self._install(to_install, install_dir)
  581. def do_install(self, installables, platform, install_dir, cache_dir=None,
  582. check_license=True, scp=None):
  583. """Determine what installables should be installed. If they were
  584. passed in on the command line, use them, otherwise install
  585. all known installables.
  586. """
  587. if not cache_dir:
  588. cache_dir = _default_installable_cache()
  589. all_installables = self.list_installables()
  590. if not len(installables):
  591. install_installables = all_installables
  592. else:
  593. # passed in on the command line. We'll need to verify we
  594. # know about them here.
  595. install_installables = installables
  596. for installable in install_installables:
  597. if installable not in all_installables:
  598. raise RuntimeError('Unknown installable: %s' %
  599. (installable,))
  600. if check_license:
  601. # *TODO: check against a list of 'known good' licenses.
  602. # *TODO: check for urls which conflict -- will lead to
  603. # problems.
  604. for installable in install_installables:
  605. if not self.is_valid_license(installable):
  606. return 1
  607. # Set up the 'scp' handler
  608. opener = urllib_request.build_opener()
  609. scp_or_http = SCPOrHTTPHandler(scp)
  610. opener.add_handler(scp_or_http)
  611. urllib_request.install_opener(opener)
  612. # Do the work of installing the requested installables.
  613. self.install(
  614. install_installables,
  615. platform,
  616. install_dir,
  617. cache_dir)
  618. scp_or_http.cleanup()
  619. # Verify that requested packages are installed
  620. for pkg in installables:
  621. if pkg not in self._installed:
  622. raise RuntimeError("No '%s' available for '%s'." %
  623. (pkg, platform))
  624. def do_uninstall(self, installables, install_dir):
  625. # Do not bother to check license if we're uninstalling.
  626. all_installed = self.list_installed()
  627. if not len(installables):
  628. uninstall_installables = all_installed
  629. else:
  630. # passed in on the command line. We'll need to verify we
  631. # know about them here.
  632. uninstall_installables = installables
  633. for installable in uninstall_installables:
  634. if installable not in all_installed:
  635. raise RuntimeError('Installable not installed: %s' %
  636. (installable,))
  637. self.uninstall(uninstall_installables, install_dir)
  638. class SCPOrHTTPHandler(urllib_request.BaseHandler):
  639. """Evil hack to allow both the build system and developers consume
  640. proprietary binaries.
  641. To use http, export the environment variable:
  642. INSTALL_USE_HTTP_FOR_SCP=true
  643. """
  644. def __init__(self, scp_binary):
  645. self._scp = scp_binary
  646. self._dir = None
  647. def scp_open(self, request):
  648. #scp:codex.lindenlab.com:/local/share/install_pkgs/package.tar.bz2
  649. remote = request.get_full_url()[4:]
  650. if os.getenv('INSTALL_USE_HTTP_FOR_SCP', None) == 'true':
  651. return self.do_http(remote)
  652. try:
  653. return self.do_scp(remote)
  654. except:
  655. self.cleanup()
  656. raise
  657. def do_http(self, remote):
  658. url = remote.split(':',1)
  659. if not url[1].startswith('/'):
  660. # in case it's in a homedir or something
  661. url.insert(1, '/')
  662. url.insert(0, "http://")
  663. url = ''.join(url)
  664. print("Using HTTP:",url)
  665. return urllib_request.urlopen(url)
  666. def do_scp(self, remote):
  667. if not self._dir:
  668. self._dir = tempfile.mkdtemp()
  669. local = os.path.join(self._dir, remote.split('/')[-1:][0])
  670. command = []
  671. for part in (self._scp, remote, local):
  672. if ' ' in part:
  673. # I hate shell escaping.
  674. part.replace('\\', '\\\\')
  675. part.replace('"', '\\"')
  676. command.append('"%s"' % part)
  677. else:
  678. command.append(part)
  679. #print "forking:", command
  680. rv = os.system(' '.join(command))
  681. if rv != 0:
  682. raise RuntimeError("Cannot fetch: %s" % remote)
  683. return open(local, 'rb')
  684. def cleanup(self):
  685. if self._dir:
  686. shutil.rmtree(self._dir)
  687. def _mkdir(directory):
  688. "Safe, repeatable way to make a directory."
  689. if not os.path.exists(directory):
  690. os.makedirs(directory)
  691. def _get_platform():
  692. "Return appropriate platform packages for the environment."
  693. platform_map = {
  694. 'darwin': 'darwin',
  695. 'darwin64': 'darwin',
  696. 'linux': 'linux',
  697. 'linux1': 'linux',
  698. 'linux2': 'linux',
  699. 'win32' : 'windows',
  700. 'win64' : 'windows',
  701. 'windows2' : 'windows',
  702. 'windows64' : 'windows',
  703. 'cygwin' : 'windows',
  704. }
  705. this_platform = platform_map[sys.platform]
  706. return this_platform
  707. def _getuser():
  708. "Get the user"
  709. try:
  710. # Unix-only.
  711. import getpass
  712. return getpass.getuser()
  713. except ImportError:
  714. import win32api
  715. return win32api.GetUserName()
  716. def _default_installable_cache():
  717. """In general, the installable files do not change much, so find a
  718. host/user specific location to cache files."""
  719. user = _getuser()
  720. cache_dir = "/var/tmp/%s/install.cache" % user
  721. if _get_platform() == 'windows':
  722. cache_dir = os.path.join(tempfile.gettempdir(), 'install.cache')
  723. return cache_dir
  724. def parse_args():
  725. parser = optparse.OptionParser(
  726. usage="usage: %prog [options] [installable1 [installable2...]]",
  727. formatter = Formatter(),
  728. description="""This script fetches and installs installable packages.
  729. It also handles uninstalling those packages and manages the mapping between
  730. packages and their license.
  731. The process is to open and read an install manifest file which specifies
  732. what files should be installed. For each installable to be installed.
  733. * make sure it has a license
  734. * check the installed version
  735. ** if not installed and needs to be, download and install
  736. ** if installed version differs, download & install
  737. If no installables are specified on the command line, then the defaut
  738. behavior is to install all known installables appropriate for the platform
  739. specified or uninstall all installables if --uninstall is set. You can specify
  740. more than one installable on the command line.
  741. When specifying a platform, you can specify 'all' to install all
  742. packages, or any platform of the form:
  743. OS[/arch[/compiler[/compiler_version]]]
  744. Where the supported values for each are:
  745. OS: darwin, linux, windows
  746. compiler: vs, gcc
  747. compiler_version: 2017, 5.5, 10.2, etc.
  748. No checks are made to ensure a valid combination of platform
  749. parts. Some examples of valid platforms:
  750. darwin64
  751. linux64
  752. windows64
  753. """)
  754. parser.add_option(
  755. '--dry-run',
  756. action='store_true',
  757. default=False,
  758. dest='dryrun',
  759. help='Do not actually install files. Downloads will still happen.')
  760. parser.add_option(
  761. '--install-manifest',
  762. type='string',
  763. default=os.path.join(base_dir, 'install.xml'),
  764. dest='install_filename',
  765. help='The file used to describe what should be installed.')
  766. parser.add_option(
  767. '--installed-manifest',
  768. type='string',
  769. default=os.path.join(base_dir, 'installed.xml'),
  770. dest='installed_filename',
  771. help='The file used to record what is installed.')
  772. parser.add_option(
  773. '--export-manifest',
  774. action='store_true',
  775. default=False,
  776. dest='export_manifest',
  777. help="Print the install manifest to stdout and exit.")
  778. parser.add_option(
  779. '-p', '--platform',
  780. type='string',
  781. default=_get_platform(),
  782. dest='platform',
  783. help="""Override the automatically determined platform. \
  784. You can specify 'all' to do a installation of installables for all platforms.""")
  785. parser.add_option(
  786. '--cache-dir',
  787. type='string',
  788. default=_default_installable_cache(),
  789. dest='cache_dir',
  790. help='Where to download files. Default: %s'% \
  791. (_default_installable_cache()))
  792. parser.add_option(
  793. '--install-dir',
  794. type='string',
  795. default=base_dir,
  796. dest='install_dir',
  797. help='Where to unpack the installed files.')
  798. parser.add_option(
  799. '--list-installed',
  800. action='store_true',
  801. default=False,
  802. dest='list_installed',
  803. help="List the installed package names and exit.")
  804. parser.add_option(
  805. '--skip-license-check',
  806. action='store_false',
  807. default=True,
  808. dest='check_license',
  809. help="Do not perform the license check.")
  810. parser.add_option(
  811. '--list-licenses',
  812. action='store_true',
  813. default=False,
  814. dest='list_licenses',
  815. help="List known licenses and exit.")
  816. parser.add_option(
  817. '--detail-license',
  818. type='string',
  819. default=None,
  820. dest='detail_license',
  821. help="Get detailed information on specified license and exit.")
  822. parser.add_option(
  823. '--add-license',
  824. type='string',
  825. default=None,
  826. dest='new_license',
  827. help="""Add a license to the install file. Argument is the name of \
  828. license. Specify --license-url if the license is remote or specify \
  829. --license-text, otherwse the license text will be read from standard \
  830. input.""")
  831. parser.add_option(
  832. '--license-url',
  833. type='string',
  834. default=None,
  835. dest='license_url',
  836. help="""Put the specified url into an added license. \
  837. Ignored if --add-license is not specified.""")
  838. parser.add_option(
  839. '--license-text',
  840. type='string',
  841. default=None,
  842. dest='license_text',
  843. help="""Put the text into an added license. \
  844. Ignored if --add-license is not specified.""")
  845. parser.add_option(
  846. '--remove-license',
  847. type='string',
  848. default=None,
  849. dest='remove_license',
  850. help="Remove a named license.")
  851. parser.add_option(
  852. '--remove-installable',
  853. type='string',
  854. default=None,
  855. dest='remove_installable',
  856. help="Remove a installable from the install file.")
  857. parser.add_option(
  858. '--add-installable',
  859. type='string',
  860. default=None,
  861. dest='add_installable',
  862. help="""Add a installable into the install file. Argument is \
  863. the name of the installable to add.""")
  864. parser.add_option(
  865. '--add-installable-metadata',
  866. type='string',
  867. default=None,
  868. dest='add_installable_metadata',
  869. help="""Add package for library into the install file. Argument is \
  870. the name of the library to add.""")
  871. parser.add_option(
  872. '--installable-copyright',
  873. type='string',
  874. default=None,
  875. dest='installable_copyright',
  876. help="""Copyright for specified new package. Ignored if \
  877. --add-installable is not specified.""")
  878. parser.add_option(
  879. '--installable-license',
  880. type='string',
  881. default=None,
  882. dest='installable_license',
  883. help="""Name of license for specified new package. Ignored if \
  884. --add-installable is not specified.""")
  885. parser.add_option(
  886. '--installable-description',
  887. type='string',
  888. default=None,
  889. dest='installable_description',
  890. help="""Description for specified new package. Ignored if \
  891. --add-installable is not specified.""")
  892. parser.add_option(
  893. '--add-installable-package',
  894. type='string',
  895. default=None,
  896. dest='add_installable_package',
  897. help="""Add package for library into the install file. Argument is \
  898. the name of the library to add.""")
  899. parser.add_option(
  900. '--package-platform',
  901. type='string',
  902. default=None,
  903. dest='package_platform',
  904. help="""Platform for specified new package. \
  905. Ignored if --add-installable or --add-installable-package is not specified.""")
  906. parser.add_option(
  907. '--package-url',
  908. type='string',
  909. default=None,
  910. dest='package_url',
  911. help="""URL for specified package. \
  912. Ignored if --add-installable or --add-installable-package is not specified.""")
  913. parser.add_option(
  914. '--package-md5',
  915. type='string',
  916. default=None,
  917. dest='package_md5',
  918. help="""md5sum for new package. \
  919. Ignored if --add-installable or --add-installable-package is not specified.""")
  920. parser.add_option(
  921. '--list',
  922. action='store_true',
  923. default=False,
  924. dest='list_installables',
  925. help="List the installables in the install manifest and exit.")
  926. parser.add_option(
  927. '--detail',
  928. type='string',
  929. default=None,
  930. dest='detail_installable',
  931. help="Get detailed information on specified installable and exit.")
  932. parser.add_option(
  933. '--detail-installed',
  934. type='string',
  935. default=None,
  936. dest='detail_installed',
  937. help="Get list of files for specified installed installable and exit.")
  938. parser.add_option(
  939. '--uninstall',
  940. action='store_true',
  941. default=False,
  942. dest='uninstall',
  943. help="""Remove the installables specified in the arguments. Just like \
  944. during installation, if no installables are listed then all installed \
  945. installables are removed.""")
  946. parser.add_option(
  947. '--scp',
  948. type='string',
  949. default='scp',
  950. dest='scp',
  951. help="Specify the path to your scp program.")
  952. return parser.parse_args()
  953. def main():
  954. options, args = parse_args()
  955. installer = Installer(
  956. options.install_filename,
  957. options.installed_filename,
  958. options.dryrun)
  959. #
  960. # Handle the queries for information
  961. #
  962. if options.list_installed:
  963. print("installed list:", installer.list_installed())
  964. return 0
  965. if options.list_installables:
  966. print("installable list:", installer.list_installables())
  967. return 0
  968. if options.detail_installable:
  969. try:
  970. detail = installer.detail_installable(options.detail_installable)
  971. print("Detail on installable",options.detail_installable+":")
  972. pprint.pprint(detail)
  973. except KeyError:
  974. print("Installable '"+options.detail_installable+"' not found in", end=' ')
  975. print("install file.")
  976. return 0
  977. if options.detail_installed:
  978. try:
  979. detail = installer.detail_installed(options.detail_installed)
  980. #print "Detail on installed",options.detail_installed+":"
  981. for line in detail:
  982. print(line)
  983. except:
  984. raise
  985. print("Installable '"+options.detail_installed+"' not found in ", end=' ')
  986. print("install file.")
  987. return 0
  988. if options.list_licenses:
  989. print("license list:", installer.list_licenses())
  990. return 0
  991. if options.detail_license:
  992. try:
  993. detail = installer.detail_license(options.detail_license)
  994. print("Detail on license",options.detail_license+":")
  995. pprint.pprint(detail)
  996. except KeyError:
  997. print("License '"+options.detail_license+"' not defined in", end=' ')
  998. print("install file.")
  999. return 0
  1000. if options.export_manifest:
  1001. # *HACK: just re-parse the install manifest and pretty print
  1002. # it. easier than looking at the datastructure designed for
  1003. # actually determining what to install
  1004. install = llsd.parse(open(options.install_filename, 'rb').read())
  1005. pprint.pprint(install)
  1006. return 0
  1007. #
  1008. # Handle updates -- can only do one of these
  1009. # *TODO: should this change the command line syntax?
  1010. #
  1011. if options.new_license:
  1012. if not installer.add_license(
  1013. options.new_license,
  1014. text=options.license_text,
  1015. url=options.license_url):
  1016. return 1
  1017. elif options.remove_license:
  1018. installer.remove_license(options.remove_license)
  1019. elif options.remove_installable:
  1020. installer.remove_installable(options.remove_installable)
  1021. elif options.add_installable:
  1022. if not installer.add_installable(
  1023. options.add_installable,
  1024. copyright=options.installable_copyright,
  1025. license=options.installable_license,
  1026. description=options.installable_description,
  1027. platform=options.package_platform,
  1028. url=options.package_url,
  1029. md5sum=options.package_md5):
  1030. return 1
  1031. elif options.add_installable_metadata:
  1032. if not installer.add_installable_metadata(
  1033. options.add_installable_metadata,
  1034. copyright=options.installable_copyright,
  1035. license=options.installable_license,
  1036. description=options.installable_description):
  1037. return 1
  1038. elif options.add_installable_package:
  1039. if not installer.add_installable_package(
  1040. options.add_installable_package,
  1041. platform=options.package_platform,
  1042. url=options.package_url,
  1043. md5sum=options.package_md5):
  1044. return 1
  1045. elif options.uninstall:
  1046. installer.do_uninstall(args, options.install_dir)
  1047. else:
  1048. installer.do_install(args, options.platform, options.install_dir,
  1049. options.cache_dir, options.check_license,
  1050. options.scp)
  1051. # save out any changes
  1052. installer.save()
  1053. return 0
  1054. if __name__ == '__main__':
  1055. #print sys.argv
  1056. sys.exit(main())