commands.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. # coding: utf-8
  2. """JupyterLab entry points"""
  3. # Copyright (c) Jupyter Development Team.
  4. # Distributed under the terms of the Modified BSD License.
  5. from __future__ import print_function
  6. from distutils.version import LooseVersion
  7. import errno
  8. import json
  9. import logging
  10. import pipes
  11. import os
  12. import glob
  13. from os import path as osp
  14. from os.path import join as pjoin
  15. from subprocess import check_output, CalledProcessError, STDOUT
  16. import shutil
  17. import sys
  18. import tarfile
  19. from jupyter_core.paths import ENV_JUPYTER_PATH, jupyter_config_path
  20. from notebook.nbextensions import (
  21. GREEN_ENABLED, GREEN_OK, RED_DISABLED, RED_X
  22. )
  23. from .semver import Range, gte, lt, lte, gt
  24. from ._version import __version__
  25. if sys.platform == 'win32':
  26. from subprocess import list2cmdline
  27. else:
  28. def list2cmdline(cmd_list):
  29. return ' '.join(map(pipes.quote, cmd_list))
  30. here = osp.dirname(osp.abspath(__file__))
  31. logging.basicConfig(format='%(message)s', level=logging.INFO)
  32. def get_app_dir(app_dir=None):
  33. """Get the configured JupyterLab app directory.
  34. """
  35. app_dir = app_dir or os.environ.get('JUPYTERLAB_DIR')
  36. app_dir = app_dir or pjoin(ENV_JUPYTER_PATH[0], 'lab')
  37. return os.path.realpath(app_dir)
  38. def get_user_settings_dir():
  39. """Get the configured JupyterLab app directory.
  40. """
  41. settings_dir = os.environ.get('JUPYTERLAB_SETTINGS_DIR')
  42. settings_dir = settings_dir or pjoin(
  43. jupyter_config_path()[0], 'lab', 'user-settings'
  44. )
  45. return os.path.realpath(settings_dir)
  46. def run(cmd, **kwargs):
  47. """Run a command in the given working directory.
  48. """
  49. logger = kwargs.pop('logger', logging) or logging
  50. logger.info('> ' + list2cmdline(cmd))
  51. kwargs.setdefault('shell', sys.platform == 'win32')
  52. kwargs.setdefault('env', os.environ)
  53. kwargs.setdefault('stderr', STDOUT)
  54. try:
  55. return check_output(cmd, **kwargs)
  56. except CalledProcessError as error:
  57. output = error.output.decode('utf-8')
  58. logger.info(output)
  59. raise error
  60. def install_extension(extension, app_dir=None, logger=None):
  61. """Install an extension package into JupyterLab.
  62. Follows the semantics of https://docs.npmjs.com/cli/install.
  63. The extension is first validated.
  64. If link is true, the source directory is linked using `npm link`.
  65. """
  66. app_dir = get_app_dir(app_dir)
  67. if app_dir == here:
  68. raise ValueError('Cannot install extensions in core app')
  69. extension = _normalize_path(extension)
  70. # Check for a core extensions here.
  71. data = _get_core_data()
  72. if extension in _get_core_extensions():
  73. config = _get_build_config(app_dir)
  74. uninstalled = config.get('uninstalled_core_extensions', [])
  75. if extension in uninstalled:
  76. uninstalled.remove(extension)
  77. config['uninstalled_core_extensions'] = uninstalled
  78. _write_build_config(config, app_dir, logger=logger)
  79. return
  80. _ensure_package(app_dir, logger=logger)
  81. target = pjoin(app_dir, 'extensions', 'temp')
  82. if os.path.exists(target):
  83. shutil.rmtree(target)
  84. os.makedirs(target)
  85. # npm pack the extension
  86. run(['npm', 'pack', extension], cwd=target, logger=logger)
  87. fname = os.path.basename(glob.glob(pjoin(target, '*.*'))[0])
  88. data = _read_package(pjoin(target, fname))
  89. # Remove the tarball if the package is not an extension.
  90. if not _is_extension(data):
  91. shutil.rmtree(target)
  92. msg = '%s is not a valid JupyterLab extension' % extension
  93. raise ValueError(msg)
  94. # Remove the tarball if the package is not compatible.
  95. core_data = _get_core_data()
  96. deps = data.get('dependencies', dict())
  97. errors = _validate_compatibility(extension, deps, core_data)
  98. if errors:
  99. shutil.rmtree(target)
  100. msg = _format_compatibility_errors(
  101. data['name'], data['version'], errors
  102. )
  103. raise ValueError(msg)
  104. # Remove an existing extension tarball.
  105. ext_path = pjoin(app_dir, 'extensions', fname)
  106. if os.path.exists(ext_path):
  107. os.remove(ext_path)
  108. shutil.move(pjoin(target, fname), pjoin(app_dir, 'extensions'))
  109. shutil.rmtree(target)
  110. # Remove any existing package from staging/node_modules
  111. target = pjoin(app_dir, 'staging', 'node_modules', data['name'])
  112. target = target.replace('/', os.sep)
  113. if os.path.exists(target):
  114. shutil.rmtree(target)
  115. # Handle any schemas.
  116. schema_data = data['jupyterlab'].get('schema_data', dict())
  117. for (key, value) in schema_data.items():
  118. path = pjoin(app_dir, 'schemas', key + '.json')
  119. with open(path, 'w') as fid:
  120. fid.write(value)
  121. def link_package(path, app_dir=None, logger=None):
  122. """Link a package against the JupyterLab build.
  123. """
  124. logger = logger or logging
  125. app_dir = get_app_dir(app_dir)
  126. if app_dir == here:
  127. raise ValueError('Cannot link packages in core app')
  128. path = _normalize_path(path)
  129. _ensure_package(app_dir, logger=logger)
  130. # Verify the package.json data.
  131. pkg_path = osp.join(path, 'package.json')
  132. if not osp.exists(pkg_path):
  133. msg = 'Linked package must point to a directory with package.json'
  134. raise ValueError(msg)
  135. with open(pkg_path) as fid:
  136. data = json.load(fid)
  137. # Check for a core extensions here.
  138. core_extensions = _get_core_extensions()
  139. if data['name'] in core_extensions:
  140. raise ValueError('Cannot link a core extension')
  141. is_extension = _is_extension(data)
  142. if is_extension:
  143. install_extension(path, app_dir)
  144. else:
  145. msg = ('*** Note: Linking non-extension package "%s" (lacks ' +
  146. '`jupyterlab.extension` metadata)')
  147. logger.info(msg % data['name'])
  148. core_data = _get_core_data()
  149. deps = data.get('dependencies', dict())
  150. name = data['name']
  151. errors = _validate_compatibility(name, deps, core_data)
  152. if errors:
  153. msg = _format_compatibility_errors(name, data['version'], errors)
  154. raise ValueError(msg)
  155. config = _get_build_config(app_dir)
  156. config.setdefault('linked_packages', dict())
  157. config['linked_packages'][data['name']] = path
  158. _write_build_config(config, app_dir, logger=logger)
  159. def unlink_package(package, app_dir=None, logger=None):
  160. """Unlink a package from JupyterLab by path or name.
  161. """
  162. logger = logger or logging
  163. package = _normalize_path(package)
  164. name = None
  165. app_dir = get_app_dir(app_dir)
  166. if app_dir == here:
  167. raise ValueError('Cannot link packages in core app')
  168. config = _get_build_config(app_dir)
  169. linked = config.setdefault('linked_packages', dict())
  170. for (key, value) in linked.items():
  171. if value == package or key == package:
  172. name = key
  173. break
  174. if not name:
  175. logger.warn('No package matching "%s" is linked' % package)
  176. return False
  177. del linked[name]
  178. config['linked_packages'] = linked
  179. _write_build_config(config, app_dir, logger=logger)
  180. extensions = _get_extensions(app_dir)
  181. if name in extensions:
  182. uninstall_extension(name, app_dir)
  183. return True
  184. def enable_extension(extension, app_dir=None, logger=None):
  185. """Enable a JupyterLab extension.
  186. """
  187. _toggle_extension(extension, False, app_dir, logger)
  188. def disable_extension(extension, app_dir=None, logger=None):
  189. """Disable a JupyterLab package.
  190. """
  191. _toggle_extension(extension, True, app_dir, logger)
  192. def check_node():
  193. """Check for the existence of node and whether it is the right version.
  194. """
  195. try:
  196. run(['node', 'node-version-check.js'], cwd=here)
  197. except Exception:
  198. raise ValueError('`node` version 5+ is required, see extensions in README')
  199. def should_build(app_dir=None, logger=None):
  200. """Determine whether JupyterLab should be built.
  201. Note: Linked packages should be updated by manually building.
  202. Returns a tuple of whether a build is necessary, and an associated message.
  203. """
  204. app_dir = get_app_dir(app_dir)
  205. # Check for installed extensions
  206. extensions = _get_extensions(app_dir)
  207. # No linked and no extensions and no built version.
  208. if not extensions and not os.path.exists(pjoin(app_dir, 'static')):
  209. return False, ''
  210. pkg_path = pjoin(app_dir, 'static', 'package.json')
  211. if not os.path.exists(pkg_path):
  212. return True, 'Installed extensions with no built application'
  213. with open(pkg_path) as fid:
  214. data = json.load(fid)
  215. # Look for mismatched version.
  216. version = data['jupyterlab'].get('version', '')
  217. if LooseVersion(version) != LooseVersion(__version__):
  218. msg = 'Version mismatch: %s (built), %s (current)'
  219. return True, msg % (version, __version__)
  220. # Look for mismatched extensions.
  221. _ensure_package(app_dir, logger=logger)
  222. staging_path = pjoin(app_dir, 'staging', 'package.json')
  223. with open(staging_path) as fid:
  224. staging_data = json.load(fid)
  225. staging_exts = staging_data['jupyterlab']['extensions']
  226. if set(staging_exts) != set(data['jupyterlab']['extensions']):
  227. return True, 'Installed extensions changed'
  228. staging_mime_exts = staging_data['jupyterlab']['mimeExtensions']
  229. if set(staging_mime_exts) != set(data['jupyterlab']['mimeExtensions']):
  230. return True, 'Installed extensions changed'
  231. deps = data.get('dependencies', dict())
  232. # Look for mismatched extension paths.
  233. for name in extensions:
  234. # Check for dependencies that were rejected as incompatible.
  235. if name not in staging_data['dependencies']:
  236. continue
  237. path = deps[name]
  238. if path.startswith('file:'):
  239. path = path.replace('file:', '')
  240. path = os.path.abspath(pjoin(app_dir, 'staging', path))
  241. staging_path = staging_data['dependencies'][name]
  242. if sys.platform == 'win32':
  243. path = path.lower()
  244. staging_path = staging_path.lower()
  245. if path != staging_path:
  246. return True, 'Installed extensions changed'
  247. return False, ''
  248. def validate_compatibility(extension, app_dir=None, logger=None):
  249. """Validate the compatibility of an extension.
  250. """
  251. app_dir = get_app_dir(app_dir)
  252. extensions = _get_extensions(app_dir)
  253. if extension not in extensions:
  254. raise ValueError('%s is not an installed extension')
  255. deps = extensions[extension].get('dependencies', dict())
  256. core_data = _get_core_data()
  257. return _validate_compatibility(extension, deps, core_data)
  258. def uninstall_extension(name, app_dir=None, logger=None):
  259. """Uninstall an extension by name.
  260. """
  261. logger = logger or logging
  262. app_dir = get_app_dir(app_dir)
  263. if app_dir == here:
  264. raise ValueError('Cannot install packages in core app')
  265. # Allow for uninstalled core extensions here.
  266. data = _get_core_data()
  267. if name in _get_core_extensions():
  268. logger.info('Uninstalling core extension %s' % name)
  269. config = _get_build_config(app_dir)
  270. uninstalled = config.get('uninstalled_core_extensions', [])
  271. if name not in uninstalled:
  272. uninstalled.append(name)
  273. config['uninstalled_core_extensions'] = uninstalled
  274. _write_build_config(config, app_dir, logger=logger)
  275. return True
  276. for (extname, data) in _get_extensions(app_dir).items():
  277. path = data['path']
  278. if extname == name:
  279. msg = 'Uninstalling %s from %s' % (name, os.path.dirname(path))
  280. logger.info(msg)
  281. os.remove(path)
  282. return True
  283. logger.warn('No labextension named "%s" installed' % name)
  284. return False
  285. def list_extensions(app_dir=None, logger=None):
  286. """List the extensions.
  287. """
  288. logger = logger or logging
  289. app_dir = get_app_dir(app_dir)
  290. extensions = _get_extensions(app_dir)
  291. disabled = _get_disabled(app_dir)
  292. linked = _get_linked_packages(app_dir, logger=logger)
  293. app = []
  294. sys = []
  295. linked = []
  296. errors = dict()
  297. core_data = _get_core_data()
  298. # We want to organize by dir.
  299. sys_path = pjoin(get_app_dir(), 'extensions')
  300. for (key, value) in extensions.items():
  301. deps = extensions[key].get('dependencies', dict())
  302. errors[key] = _validate_compatibility(key, deps, core_data)
  303. if key in linked:
  304. linked.append(key)
  305. if value['path'] == sys_path and sys_path != app_dir:
  306. sys.append(key)
  307. continue
  308. app.append(key)
  309. logger.info('JupyterLab v%s' % __version__)
  310. logger.info('Known labextensions:')
  311. if app:
  312. logger.info(' app dir: %s' % app_dir)
  313. for item in sorted(app):
  314. extra = ''
  315. if item in disabled:
  316. extra += ' %s' % RED_DISABLED
  317. else:
  318. extra += ' %s' % GREEN_ENABLED
  319. if errors[item]:
  320. extra += ' %s' % RED_X
  321. else:
  322. extra += ' %s' % GREEN_OK
  323. if item in linked:
  324. extra += '*'
  325. logger.info(' %s%s' % (item, extra))
  326. version = extensions[item]['version']
  327. if errors[item]:
  328. msg = _format_compatibility_errors(item, version, errors[item])
  329. logger.warn(msg + '\n')
  330. if sys:
  331. logger.info(' sys dir: %s' % sys_path)
  332. for item in sorted(sys):
  333. extra = ''
  334. if item in disabled:
  335. extra += ' %s' % RED_DISABLED
  336. else:
  337. extra += ' %s' % GREEN_ENABLED
  338. logger.info(' %s%s' % (item, extra))
  339. if errors[item]:
  340. extra += ' %s' % RED_X
  341. else:
  342. extra += ' %s' % GREEN_OK
  343. if item in linked:
  344. extra += '*'
  345. logger.info(' %s%s' % (item, extra))
  346. version = extensions[item]['version']
  347. if errors[item]:
  348. msg = _format_compatibility_errors(item, version, errors[item])
  349. logger.warn(msg + '\n')
  350. if linked:
  351. logger.info('* Denotes linked extensions. Use `jupyter labextension listlinked` to see details')
  352. # Handle uninstalled and disabled core packages
  353. uninstalled_core = _get_uinstalled_core_extensions(app_dir)
  354. if uninstalled_core:
  355. logger.info('\nUninstalled core extensions:')
  356. [logger.info(' %s' % item) for item in sorted(uninstalled_core)]
  357. core_extensions = _get_core_extensions()
  358. disabled_core = []
  359. for key in core_extensions:
  360. if key in disabled:
  361. disabled_core.append(key)
  362. if disabled_core:
  363. logger.info('\nDisabled core extensions:')
  364. [logger.info(' %s' % item) for item in sorted(disabled_core)]
  365. def clean(app_dir=None):
  366. """Clean the JupyterLab application directory."""
  367. app_dir = get_app_dir(app_dir)
  368. if app_dir == here:
  369. raise ValueError('Cannot clean the core app')
  370. for name in ['static', 'staging']:
  371. target = pjoin(app_dir, name)
  372. if osp.exists(target):
  373. shutil.rmtree(target)
  374. def build(app_dir=None, name=None, version=None, logger=None):
  375. """Build the JupyterLab application."""
  376. # Set up the build directory.
  377. check_node()
  378. logger = logger or logging
  379. app_dir = get_app_dir(app_dir)
  380. if app_dir == here:
  381. raise ValueError('Cannot build extensions in the core app')
  382. _ensure_package(app_dir, name=name, version=version, logger=logger)
  383. staging = pjoin(app_dir, 'staging')
  384. extensions = _get_extensions(app_dir)
  385. # Install the linked packages.
  386. for (name, path) in _get_linked_packages(app_dir, logger=logger).items():
  387. # Handle linked extensions.
  388. if name in extensions:
  389. install_extension(path, app_dir)
  390. # Handle linked packages that are not extensions.
  391. else:
  392. # Remove any existing package from staging/node_modules
  393. target = pjoin(app_dir, 'staging', 'node_modules', name)
  394. target = target.replace('/', os.sep)
  395. if os.path.exists(target):
  396. shutil.rmtree(target)
  397. # Make sure packages are installed.
  398. run(['npm', 'install'], cwd=staging, logger=logger)
  399. # Build the app.
  400. run(['npm', 'run', 'build'], cwd=staging, logger=logger)
  401. # Move the app to the static dir.
  402. static = pjoin(app_dir, 'static')
  403. if os.path.exists(static):
  404. shutil.rmtree(static)
  405. shutil.copytree(pjoin(staging, 'build'), static)
  406. def _get_build_config(app_dir):
  407. """Get the build config data for the given app dir
  408. """
  409. target = pjoin(app_dir, 'settings', 'build_config.json')
  410. if not os.path.exists(target):
  411. return {}
  412. else:
  413. with open(target) as fid:
  414. return json.load(fid)
  415. def _get_page_config(app_dir):
  416. """Get the page config data for the given app dir
  417. """
  418. target = pjoin(app_dir, 'settings', 'page_config.json')
  419. if not os.path.exists(target):
  420. return {}
  421. else:
  422. with open(target) as fid:
  423. return json.load(fid)
  424. def _validate_compatibility(extension, deps, core_data):
  425. """Validate the compatibility of an extension.
  426. """
  427. core_deps = core_data['dependencies']
  428. singletons = core_data['jupyterlab']['singletonPackages']
  429. errors = []
  430. for (key, value) in deps.items():
  431. if key in singletons:
  432. overlap = _test_overlap(core_deps[key], value)
  433. if overlap is False:
  434. errors.append((key, core_deps[key], value))
  435. return errors
  436. def _get_core_data():
  437. """Get the data for the app template.
  438. """
  439. with open(pjoin(here, 'package.app.json')) as fid:
  440. return json.load(fid)
  441. def _test_overlap(spec1, spec2):
  442. """Test whether two version specs overlap.
  443. Returns `None` if we cannot determine compatibility,
  444. otherwise whether there is an overlap
  445. """
  446. # Test for overlapping semver ranges.
  447. r1 = Range(spec1, True)
  448. r2 = Range(spec2, True)
  449. # If either range is empty, we cannot verify.
  450. if not r1.range or not r2.range:
  451. return
  452. x1 = r1.set[0][0].semver
  453. x2 = r1.set[0][-1].semver
  454. y1 = r2.set[0][0].semver
  455. y2 = r2.set[0][-1].semver
  456. o1 = r1.set[0][0].operator
  457. o2 = r2.set[0][0].operator
  458. # We do not handle (<) specifiers.
  459. if (o1.startswith('<') or o2.startswith('<')):
  460. return
  461. # Handle single value specifiers.
  462. lx = lte if x1 == x2 else lt
  463. ly = lte if y1 == y2 else lt
  464. gx = gte if x1 == x2 else gt
  465. gy = gte if x1 == x2 else gt
  466. # Handle unbounded (>) specifiers.
  467. def noop(x, y, z):
  468. return True
  469. if x1 == x2 and o1.startswith('>'):
  470. lx = noop
  471. if y1 == y2 and o2.startswith('>'):
  472. ly = noop
  473. # Check for overlap.
  474. return (
  475. gte(x1, y1, True) and ly(x1, y2, True) or
  476. gy(x2, y1, True) and ly(x2, y2, True) or
  477. gte(y1, x1, True) and lx(y1, x2, True) or
  478. gx(y2, x1, True) and lx(y2, x2, True)
  479. )
  480. def _format_compatibility_errors(name, version, errors):
  481. """Format a message for compatibility errors.
  482. """
  483. msg = '\n"%s@%s" is not compatible with the current JupyterLab'
  484. msg = msg % (name, version)
  485. msg += '\nConflicting Dependencies:'
  486. msg += '\nRequired\tActual\tPackage'
  487. for error in errors:
  488. msg += '\n%s \t%s\t%s' % (error[1], error[2], error[0])
  489. return msg
  490. def _toggle_extension(extension, value, app_dir=None, logger=None):
  491. """Enable or disable a lab extension.
  492. """
  493. app_dir = get_app_dir(app_dir)
  494. config = _get_page_config(app_dir)
  495. extensions = _get_extensions(app_dir)
  496. core_extensions = _get_core_extensions()
  497. if extension not in extensions and extension not in core_extensions:
  498. raise ValueError('Extension %s is not installed' % extension)
  499. disabled = config.get('disabledExtensions', [])
  500. if value and extension not in disabled:
  501. disabled.append(extension)
  502. if not value and extension in disabled:
  503. disabled.remove(extension)
  504. # Prune extensions that are not installed.
  505. disabled = [ext for ext in disabled
  506. if (ext in extensions or ext in core_extensions)]
  507. config['disabledExtensions'] = disabled
  508. _write_page_config(config, app_dir, logger=logger)
  509. def _write_build_config(config, app_dir, logger):
  510. """Write the build config to the app dir.
  511. """
  512. _ensure_package(app_dir, logger=logger)
  513. target = pjoin(app_dir, 'settings', 'build_config.json')
  514. with open(target, 'w') as fid:
  515. json.dump(config, fid, indent=4)
  516. def _write_page_config(config, app_dir, logger):
  517. """Write the build config to the app dir.
  518. """
  519. _ensure_package(app_dir, logger=logger)
  520. target = pjoin(app_dir, 'settings', 'page_config.json')
  521. with open(target, 'w') as fid:
  522. json.dump(config, fid, indent=4)
  523. def _ensure_package(app_dir, name=None, version=None, logger=None):
  524. """Make sure the build dir is set up.
  525. """
  526. logger = logger or logging
  527. if not os.path.exists(pjoin(app_dir, 'extensions')):
  528. try:
  529. os.makedirs(pjoin(app_dir, 'extensions'))
  530. except OSError as e:
  531. if e.errno != errno.EEXIST:
  532. raise
  533. settings = pjoin(app_dir, 'settings')
  534. if not os.path.exists(settings):
  535. try:
  536. os.makedirs(settings)
  537. except OSError as e:
  538. if e.errno != errno.EEXIST:
  539. raise
  540. staging = pjoin(app_dir, 'staging')
  541. # Look for mismatched version.
  542. pkg_path = pjoin(staging, 'package.json')
  543. version_updated = False
  544. if os.path.exists(pkg_path):
  545. with open(pkg_path) as fid:
  546. data = json.load(fid)
  547. if data['jupyterlab'].get('version', '') != __version__:
  548. shutil.rmtree(staging)
  549. version_updated = True
  550. if not os.path.exists(staging):
  551. os.makedirs(staging)
  552. for fname in ['index.app.js', 'webpack.config.js']:
  553. dest = pjoin(staging, fname.replace('.app', ''))
  554. shutil.copy(pjoin(here, fname), dest)
  555. # Template the package.json file.
  556. data = _get_core_data()
  557. extensions = _get_extensions(app_dir)
  558. for (key, value) in extensions.items():
  559. # Reject incompatible extensions with a message.
  560. deps = value.get('dependencies', dict())
  561. errors = _validate_compatibility(key, deps, data)
  562. if errors:
  563. msg = _format_compatibility_errors(key, value['version'], errors)
  564. logger.warn(msg + '\n')
  565. continue
  566. data['dependencies'][key] = value['path']
  567. jlab_data = value['jupyterlab']
  568. if jlab_data.get('extension', False):
  569. data['jupyterlab']['extensions'].append(key)
  570. else:
  571. data['jupyterlab']['mimeExtensions'].append(key)
  572. # Handle linked packages that are not extensions.
  573. for (key, path) in _get_linked_packages(app_dir).items():
  574. if key in extensions:
  575. continue
  576. data['dependencies'][key] = path
  577. for item in _get_uinstalled_core_extensions(app_dir):
  578. if item in data['jupyterlab']['extensions']:
  579. data['jupyterlab']['extensions'].remove(item)
  580. else:
  581. data['jupyterlab']['mimeExtensions'].remove(item)
  582. data['jupyterlab']['name'] = name or 'JupyterLab'
  583. if version:
  584. data['jupyterlab']['version'] = version
  585. pkg_path = pjoin(staging, 'package.json')
  586. with open(pkg_path, 'w') as fid:
  587. json.dump(data, fid, indent=4)
  588. # Copy any missing or outdated schema files.
  589. schema_local = pjoin(here, 'schemas')
  590. schema_app = pjoin(app_dir, 'schemas')
  591. if not os.path.exists(schema_app):
  592. os.makedirs(schema_app)
  593. for schema in os.listdir(schema_local):
  594. dest = pjoin(schema_app, schema)
  595. if version_updated or not os.path.exists(dest):
  596. shutil.copy(pjoin(schema_local, schema), dest)
  597. def _is_extension(data):
  598. """Detect if a package is an extension using its metadata.
  599. """
  600. if 'jupyterlab' not in data:
  601. return False
  602. if not isinstance(data['jupyterlab'], dict):
  603. return False
  604. is_extension = data['jupyterlab'].get('extension', False)
  605. is_mime_extension = data['jupyterlab'].get('mimeExtension', False)
  606. return is_extension or is_mime_extension
  607. def _get_uinstalled_core_extensions(app_dir):
  608. """Get the uninstalled core extensions.
  609. """
  610. config = _get_build_config(app_dir)
  611. return config.get('uninstalled_core_extensions', [])
  612. def _validate_package(data, extension):
  613. """Validate package.json data.
  614. """
  615. msg = '%s is not a valid JupyterLab extension' % extension
  616. if not _is_extension(data):
  617. raise ValueError(msg)
  618. def _get_disabled(app_dir):
  619. """Get the disabled extensions.
  620. """
  621. config = _get_page_config(app_dir)
  622. return config.get('disabledExtensions', [])
  623. def _get_core_extensions():
  624. """Get the core extensions.
  625. """
  626. data = _get_core_data()['jupyterlab']
  627. return data['extensions'] + data['mimeExtensions']
  628. def _get_extensions(app_dir):
  629. """Get the extensions in a given app dir.
  630. """
  631. extensions = dict()
  632. # Get system level packages
  633. sys_path = pjoin(get_app_dir(), 'extensions')
  634. for target in glob.glob(pjoin(sys_path, '*.tgz')):
  635. data = _read_package(target)
  636. deps = data.get('dependencies', dict())
  637. extensions[data['name']] = dict(path=os.path.realpath(target),
  638. version=data['version'],
  639. jupyterlab=data['jupyterlab'],
  640. dependencies=deps)
  641. # Look in app_dir if different
  642. app_path = pjoin(app_dir, 'extensions')
  643. if app_path == sys_path or not os.path.exists(app_path):
  644. return extensions
  645. for target in glob.glob(pjoin(app_path, '*.tgz')):
  646. data = _read_package(target)
  647. deps = data.get('dependencies', dict())
  648. extensions[data['name']] = dict(path=os.path.realpath(target),
  649. version=data['version'],
  650. jupyterlab=data['jupyterlab'],
  651. dependencies=deps)
  652. return extensions
  653. def _get_linked_packages(app_dir=None, logger=None):
  654. """Get the linked packages metadata.
  655. """
  656. logger = logger or logging
  657. app_dir = get_app_dir(app_dir)
  658. config = _get_build_config(app_dir)
  659. linked = config.get('linked_packages', dict())
  660. dead = []
  661. for (name, path) in linked.items():
  662. if not os.path.exists(path):
  663. dead.append(name)
  664. if dead:
  665. extensions = _get_extensions(app_dir)
  666. for name in dead:
  667. path = linked[name]
  668. if name in extensions:
  669. uninstall_extension(name)
  670. logger.warn('**Note: Removing dead linked extension "%s"' % name)
  671. else:
  672. logger.warn('**Note: Removing dead linked package "%s"' % name)
  673. del linked[name]
  674. if dead:
  675. config['linked_packages'] = linked
  676. _write_build_config(config, app_dir, logger=logger)
  677. return config.get('linked_packages', dict())
  678. def _read_package(target):
  679. """Read the package data in a given target tarball.
  680. """
  681. tar = tarfile.open(target, "r:gz")
  682. f = tar.extractfile('package/package.json')
  683. data = json.loads(f.read().decode('utf8'))
  684. jlab = data.get('jupyterlab', None)
  685. if not jlab:
  686. return data
  687. schemas = jlab.get('schemas', None)
  688. if not schemas:
  689. return data
  690. schema_data = dict()
  691. for schema in schemas:
  692. f = tar.extractfile('package/' + schema)
  693. schema_data[schema] = f.read().decode('utf8')
  694. data['jupyterlab']['schema_data'] = schema_data
  695. return data
  696. def _normalize_path(extension):
  697. """Normalize a given extension if it is a path.
  698. """
  699. extension = osp.expanduser(extension)
  700. if osp.exists(extension):
  701. extension = osp.abspath(extension)
  702. return extension