diff --git a/.eslintignore b/.eslintignore index bd505289..c16e7917 100644 --- a/.eslintignore +++ b/.eslintignore @@ -19,3 +19,5 @@ typedoc-theme/ .vscode/ env_installer + +src/assets/uFuzzy.iife.min.js diff --git a/.prettierignore b/.prettierignore index 49c1ac07..28758f2e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,3 +11,5 @@ .vscode/ env_installer + +src/assets/uFuzzy.iife.min.js diff --git a/.vscode/launch.json b/.vscode/launch.json index 8f30393e..3672c4af 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -25,7 +25,20 @@ "env": { "NODE_ENV": "development" } - } + }, + { + "name": "Debug CLI", + "type": "node", + "request": "launch", + "cwd": "${workspaceFolder}", + "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", + "args" : [".", "env", "list"], + "outputCapture": "std", + "preLaunchTask": "npm: build", + "env": { + "NODE_ENV": "development" + } + } ] } \ No newline at end of file diff --git a/env_installer/conda-linux-64.lock b/env_installer/conda-linux-64.lock index b323b545..a26b31a6 100644 --- a/env_installer/conda-linux-64.lock +++ b/env_installer/conda-linux-64.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: b0bc926d98175b3a6e2c78129c99b7c2c0db86132f246b27e8bb0915464669eb +# input_hash: b62b27af5c80ee49992715b3ac6ef942d473c872c0003c1b2aca8c5c0c71bde5 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2023.11.17-hbcca054_0.conda#01ffc8d36f9eba0ce0b3c1955fa780ee @@ -61,7 +61,7 @@ https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.1.0-hd590300_1.cond https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda#9ae35c3d96db2c94ce0cef86efdfa2cb https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda#cd95826dbd331ed1be26bdf401432844 https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.7.2-h2aa1ff5_1.conda#3bf887827d1968275978361a6e405e4f -https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.25-pthreads_h413a1c8_0.conda#d172b34a443b95f86089e8229ddc9a17 +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.26-pthreads_h413a1c8_0.conda#760ae35415f5ba8b15d09df5afe8b23a https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-ha9c0a0a_2.conda#55ed21669b2015f77c180feb1dd41930 https://conda.anaconda.org/conda-forge/linux-64/python-3.8.18-hd12c33a_1_cpython.conda#134e8a55d40a80865a092c0a8bdb0c40 https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.2-pyhd8ed1ab_0.conda#0dc2fce00a160271714647c019e3a8a8 @@ -89,13 +89,13 @@ https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-2.4-py38h578d9bd_3.c https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.9-pyhd8ed1ab_0.conda#8370e0a9dc443f9b45a23fd30e7a6b3b https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.4.5-py38h7f3f72f_1.conda#b66dcd4f710628fc5563ad56f02ca89b https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda#51bb7010fc86f70eee639b4bb7a894f5 -https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-20_linux64_openblas.conda#2b7bb4f7562c8cf334fc2e20c2d28abc +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-21_linux64_openblas.conda#0ac9f44fc096772b0aa092119b00c3ca https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.5.0-hca28451_0.conda#7144d5a828e2cae218e0e3c98d8a0aeb -https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.3-py38h01eb140_1.conda#2dabf287937cd631e292096cc6d0867e +https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.4-py38h01eb140_0.conda#4e4a02ad88d2ea7eddefa3a13994d8eb https://conda.anaconda.org/conda-forge/linux-64/menuinst-2.0.2-py38h578d9bd_0.conda#c1514fdcdcb7d1c4d1224e50e5e83773 https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda#5cbee699846772cc939bef23a0d524ed https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 -https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.5.9-pyhd8ed1ab_0.conda#6c59cb840d511a1a997998d55e68516c +https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_0.conda#6598c056f64dc8800d40add25e4e2c34 https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.0-h488ebb8_3.conda#128c25b7fe6a25286a48f3a6a9b5b6f3 https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda#79002079284aa895f883c6b7f3f88fd6 https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9 @@ -159,11 +159,11 @@ https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7 https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_0.conda#bfdb7c5c6ad1077c82a69a8642c87aff https://conda.anaconda.org/conda-forge/linux-64/jupyter_core-5.7.1-py38h578d9bd_0.conda#083d6afb9942e024c66e79f797ae6a38 https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_0.conda#3f0915b1fb2252ab73686a533c5f9d3f -https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-20_linux64_openblas.conda#36d486d72ab64ffea932329a1d3729a3 -https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-20_linux64_openblas.conda#6fabc51f5e647d09cc010c40061557e0 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-21_linux64_openblas.conda#4a3816d06451c4946e2db26b86472cb6 +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-21_linux64_openblas.conda#1a42f305615c3867684e049e85927531 https://conda.anaconda.org/conda-forge/linux-64/libmamba-1.5.6-had39da4_0.conda#d6213d8b3abe12c556f007894752ef41 https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.6-pyhd8ed1ab_0.tar.bz2#b21613793fcc81d944c76c9f2864a7de -https://conda.anaconda.org/conda-forge/noarch/overrides-7.4.0-pyhd8ed1ab_0.conda#4625b7b01d7f4ac9c96300a5515acfaa +https://conda.anaconda.org/conda-forge/noarch/overrides-7.6.0-pyhd8ed1ab_0.conda#3ed0205566229c23c70fd9e6318e0568 https://conda.anaconda.org/conda-forge/noarch/pexpect-4.8.0-pyh1a96a4e_2.tar.bz2#330448ce4403cc74990ac07c555942a1 https://conda.anaconda.org/conda-forge/linux-64/pillow-10.2.0-py38ha43c96d_0.conda#6d41caaaba25a751cdab3fc61e69de0d https://conda.anaconda.org/conda-forge/noarch/pip-23.3.2-pyhd8ed1ab_0.conda#8591c748f98dcc02253003533bc2e4b1 diff --git a/env_installer/conda-linux-aarch64.lock b/env_installer/conda-linux-aarch64.lock new file mode 100644 index 00000000..13fa5687 --- /dev/null +++ b/env_installer/conda-linux-aarch64.lock @@ -0,0 +1,218 @@ +# Generated by conda-lock. +# platform: linux-aarch64 +# input_hash: d9afcfd859a57c84fdf25fb42cad70b9dfd5875a73f0d3970b56bdc55fecdc06 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-aarch64/ca-certificates-2023.11.17-hcefe29a_0.conda#695a28440b58e3ba920bcac4ac7c73c6 +https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.40-h2d8c526_0.conda#16246d69e945d0b1969a6099e7c5d457 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-13.2.0-hf8544c7_3.conda#191eb9058c6e97ca5fea3552e348a237 +https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-ng-13.2.0-h9a76618_3.conda#7ad2164936c4975d94ca883d34809c0f +https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2#878f923dd6acc8aeb47a75da6c4098be +https://conda.anaconda.org/conda-forge/linux-aarch64/python_abi-3.8-4_cp38.conda#8107237a22bf62f12b4a7c8e0fa3b842 +https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-2_gnu.tar.bz2#6168d71addc746e8f2b8d57dfd2edcea +https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-ng-13.2.0-hf8544c7_3.conda#00f021ee1a24c798ae53c87ee79597f1 +https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h31becfc_5.conda#a64e35f01e0b7a2a152eca87d33b9c87 +https://conda.anaconda.org/conda-forge/linux-aarch64/c-ares-1.25.0-h31becfc_0.conda#0892c14dc88498d4cb727098141d365c +https://conda.anaconda.org/conda-forge/linux-aarch64/fmt-10.1.1-h2a328a1_1.conda#49695e320c2a672846a90ca623dce1da +https://conda.anaconda.org/conda-forge/linux-aarch64/icu-73.2-h787c7f5_0.conda#9d3c29d71f28452a2e843aff8cbe09d2 +https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.1-h4e544f5_0.tar.bz2#1f24853e59c68892452ef94ddd8afd4b +https://conda.anaconda.org/conda-forge/linux-aarch64/lerc-4.0.0-h4de3ea5_0.tar.bz2#1a0ffc65e03ce81559dbcb0695ad1476 +https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlicommon-1.1.0-h31becfc_1.conda#1b219fd801eddb7a94df5bd001053ad9 +https://conda.anaconda.org/conda-forge/linux-aarch64/libdeflate-1.19-h31becfc_0.conda#014e57e35f2dc95c9a12f63d4378e093 +https://conda.anaconda.org/conda-forge/linux-aarch64/libev-4.33-h31becfc_2.conda#a9a13cb143bbaa477b1ebaefbe47a302 +https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.4.2-h3557bc0_5.tar.bz2#dddd85f4d52121fab0a8b099c5e06501 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran5-13.2.0-h582850c_3.conda#d81dcb787465447542ad9c4cf0bab65e +https://conda.anaconda.org/conda-forge/linux-aarch64/libiconv-1.17-h31becfc_2.conda#9a8eb13f14de7d761555a98712e6df65 +https://conda.anaconda.org/conda-forge/linux-aarch64/libjpeg-turbo-3.0.0-h31becfc_1.conda#ed24e702928be089d9ba3f05618515c6 +https://conda.anaconda.org/conda-forge/linux-aarch64/libnsl-2.0.1-h31becfc_0.conda#c14f32510f694e3185704d89967ec422 +https://conda.anaconda.org/conda-forge/linux-aarch64/libsodium-1.0.18-hb9de7d4_1.tar.bz2#d09ab3c60eebb6f14eb4d07e172775cc +https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.38.1-hb4cce97_0.conda#000e30b09db0b7c775b21695dff30969 +https://conda.anaconda.org/conda-forge/linux-aarch64/libwebp-base-1.3.2-h31becfc_0.conda#1490de434d2a2c06a98af27641a2ffff +https://conda.anaconda.org/conda-forge/linux-aarch64/libxcrypt-4.4.36-h31becfc_1.conda#b4df5d7d4b63579d081fd3a4cf99740e +https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.2.13-h31becfc_5.conda#b213aa87eea9491ef7b129179322e955 +https://conda.anaconda.org/conda-forge/linux-aarch64/lz4-c-1.9.4-hd600fc2_0.conda#500145a83ed07ce79c8cef24252f366b +https://conda.anaconda.org/conda-forge/linux-aarch64/lzo-2.10-h516909a_1000.tar.bz2#ef5661339990c399c68c71cfb341e6d7 +https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.4-h0425590_2.conda#4ff0a396150dedad4269e16e5810f769 +https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.2.0-h31becfc_1.conda#b24247441ed7ce138382de2ec51200e4 +https://conda.anaconda.org/conda-forge/linux-aarch64/pthread-stubs-0.4-hb9de7d4_1001.tar.bz2#d0183ec6ce0b5aaa3486df25fa5f0ded +https://conda.anaconda.org/conda-forge/linux-aarch64/reproc-14.2.4.post0-h31becfc_1.conda#c148bb4ba029a018527d3e4d5c7b63fa +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxau-1.0.11-h31becfc_0.conda#13de34f69cb73165dbe08c1e9148bedb +https://conda.anaconda.org/conda-forge/linux-aarch64/xorg-libxdmcp-1.1.3-h3557bc0_0.tar.bz2#a6c9016ae1ca5c47a3603ed4cd65fedd +https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.2.6-h9cdd2b7_0.tar.bz2#83baad393a31d59c20b63ba4da6592df +https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-hf897c2e_2.tar.bz2#b853307650cb226731f653aa623936a4 +https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-cpp-0.8.0-h2f0025b_0.conda#b5da38ee183c1e50e3e7ffb171a2eca5 +https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlidec-1.1.0-h31becfc_1.conda#8db7cff89510bec0b863a0a8ee6a7bce +https://conda.anaconda.org/conda-forge/linux-aarch64/libbrotlienc-1.1.0-h31becfc_1.conda#ad3d3a826b5848d99936e4466ebbaa26 +https://conda.anaconda.org/conda-forge/linux-aarch64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#29371161d77933a54fccf1bb66b96529 +https://conda.anaconda.org/conda-forge/linux-aarch64/libgfortran-ng-13.2.0-he9431aa_3.conda#6c292066bb9876d7ba35c590868baaeb +https://conda.anaconda.org/conda-forge/linux-aarch64/libnghttp2-1.58.0-hb0e430d_1.conda#8f724cdddffa79152de61f5564a3526b +https://conda.anaconda.org/conda-forge/linux-aarch64/libpng-1.6.39-hf9034f9_0.conda#5ec9052384a6ac85e9111e9ac7c5ec4c +https://conda.anaconda.org/conda-forge/linux-aarch64/libsolv-0.7.27-hd84c7bf_0.conda#7e092bca53956dd2fddb1eed62c22c29 +https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.44.2-h194ca79_0.conda#464a0dedd1131669324946ee1c13c1a5 +https://conda.anaconda.org/conda-forge/linux-aarch64/libssh2-1.11.0-h492db2e_0.conda#45532845e121677ad328c9af9953f161 +https://conda.anaconda.org/conda-forge/linux-aarch64/libxcb-1.15-h2a766a3_0.conda#eb3d8c8170e3d03f2564ed2024aa00c8 +https://conda.anaconda.org/conda-forge/linux-aarch64/libxml2-2.12.4-h3091e33_1.conda#351d2cd7093fbc38dac409e95e3f55be +https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.2-h8fc344f_1.conda#105eb1e16bf83bfb2eb380a48032b655 +https://conda.anaconda.org/conda-forge/linux-aarch64/reproc-cpp-14.2.4.post0-h2f0025b_1.conda#35148ef0f190022ca52cf6edd6bdc814 +https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda#f75105e0585851f818e0009dd1dde4dc +https://conda.anaconda.org/conda-forge/linux-aarch64/zeromq-4.3.5-h2f0025b_0.conda#88905c542167163a0dea6bdad01c3366 +https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.5-h4c53e97_0.conda#b74eb9dbb5c3c15cb3cee7cbdf198c75 +https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-bin-1.1.0-h31becfc_1.conda#9e4a13596ab651ea8d77aae023d0ce3f +https://conda.anaconda.org/conda-forge/linux-aarch64/freetype-2.12.1-hf0a5ef3_2.conda#a5ab74c5bd158c3d5532b66d8d83d907 +https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.2-hc419048_0.conda#55b51af37bf6fdcfe06f140e62e8c8db +https://conda.anaconda.org/conda-forge/linux-aarch64/libarchive-3.7.2-hd2f85e0_1.conda#a0f2e7adbcdf4041d6ee273d07ca171e +https://conda.anaconda.org/conda-forge/linux-aarch64/libopenblas-0.3.26-pthreads_h5a5ec62_0.conda#2ea496754b596063335b3aeaa2b982ac +https://conda.anaconda.org/conda-forge/linux-aarch64/libtiff-4.6.0-h1708d11_2.conda#d5638e110e7f22e2602a8edd20656720 +https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.8.18-hbbe8eec_1_cpython.conda#c070aab88cfec2d531762b134a9f6c13 +https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.2-pyhd8ed1ab_0.conda#0dc2fce00a160271714647c019e3a8a8 +https://conda.anaconda.org/conda-forge/noarch/attrs-23.2.0-pyh71513ae_0.conda#5e4c0743c70186509d1412e03c2d8dfa +https://conda.anaconda.org/conda-forge/noarch/backcall-0.2.0-pyh9f0ad1d_0.tar.bz2#6006a6d08a3fa99268a2681c7fb55213 +https://conda.anaconda.org/conda-forge/noarch/boltons-23.1.1-pyhd8ed1ab_0.conda#56febe65315cc388a5d20adf2b39a74d +https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-1.1.0-h31becfc_1.conda#e41f5862ac746428407f3fd44d2ed01f +https://conda.anaconda.org/conda-forge/linux-aarch64/brotli-python-1.1.0-py38hb83fbf6_1.conda#481316b31f7fba6fa88cb613f37fecf7 +https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2#576d629e47797577ab0f1b351297ef4a +https://conda.anaconda.org/conda-forge/noarch/certifi-2023.11.17-pyhd8ed1ab_0.conda#2011bcf45376341dd1d690263fdbc789 +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda#7f4a9e3fcff3f6356ae99244a014da6a +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2#3faab06a954c2a04039983f2c4a50d99 +https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhd8ed1ab_0.conda#5cd86562580f274031ede6aa6aa24441 +https://conda.anaconda.org/conda-forge/linux-aarch64/debugpy-1.8.0-py38hb83fbf6_1.conda#bb4b478f78b779eaaaa5c9a1c4038d61 +https://conda.anaconda.org/conda-forge/noarch/decorator-5.1.1-pyhd8ed1ab_0.tar.bz2#43afe5ab04e35e17ba28649471dd7364 +https://conda.anaconda.org/conda-forge/noarch/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2#961b3a227b437d82ad7054484cfa71b2 +https://conda.anaconda.org/conda-forge/noarch/distro-1.9.0-pyhd8ed1ab_0.conda#bbdb409974cd6cb30071b1d978302726 +https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_0.tar.bz2#3cf04868fee0a029769bd41f4b2fbf2d +https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.2.0-pyhd8ed1ab_2.conda#8d652ea2ee8eaee02ed8dc820bc794aa +https://conda.anaconda.org/conda-forge/noarch/executing-2.0.1-pyhd8ed1ab_0.conda#e16be50e378d8a4533b989035b196ab8 +https://conda.anaconda.org/conda-forge/noarch/idna-3.6-pyhd8ed1ab_0.conda#1a76f09108576397c41c0b0c5bd84134 +https://conda.anaconda.org/conda-forge/noarch/ipython_genutils-0.2.0-py_1.tar.bz2#5071c982548b3a20caf70462f04f5287 +https://conda.anaconda.org/conda-forge/noarch/json5-0.9.14-pyhd8ed1ab_0.conda#dac1dabba2b5a9d1aee175c5fcc7b436 +https://conda.anaconda.org/conda-forge/linux-aarch64/jsonpointer-2.4-py38he3eb160_3.conda#69fd075f06a8a58f731ebbb262378df7 +https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.9-pyhd8ed1ab_0.conda#8370e0a9dc443f9b45a23fd30e7a6b3b +https://conda.anaconda.org/conda-forge/linux-aarch64/kiwisolver-1.4.5-py38h7e3a353_1.conda#7a0bdcd2a67eb35b54ca5d26073d598c +https://conda.anaconda.org/conda-forge/linux-aarch64/lcms2-2.16-h922389a_0.conda#ffdd8267a04c515e7ce69c727b051414 +https://conda.anaconda.org/conda-forge/linux-aarch64/libblas-3.9.0-21_linuxaarch64_openblas.conda#7358230781e5d6e76e6adacf5201bcdf +https://conda.anaconda.org/conda-forge/linux-aarch64/libcurl-8.5.0-h4e8248e_0.conda#fa0f5edc06ffc25a01eed005c6dc3d8c +https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-2.1.4-py38hea3b116_0.conda#06e67b573f72d52153df0af883c51636 +https://conda.anaconda.org/conda-forge/linux-aarch64/menuinst-2.0.2-py38he3eb160_0.conda#f193fd309cff4d6e84688ed05a46f158 +https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda#5cbee699846772cc939bef23a0d524ed +https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 +https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_0.conda#6598c056f64dc8800d40add25e4e2c34 +https://conda.anaconda.org/conda-forge/linux-aarch64/openjpeg-2.5.0-h0d9d63b_3.conda#123f5df3bc7f0e23c6950fddb97d1f43 +https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda#79002079284aa895f883c6b7f3f88fd6 +https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9 +https://conda.anaconda.org/conda-forge/noarch/parso-0.8.3-pyhd8ed1ab_0.tar.bz2#17a565a0c3899244e938cdf417e7b094 +https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-py_1003.tar.bz2#415f0ebb6198cc2801c73438a9fb5761 +https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_1.conda#405678b942f2481cecdb3e010f4925d9 +https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.1.0-pyhd8ed1ab_0.conda#45a5065664da0d1dfa8f8cd2eaf05ab9 +https://conda.anaconda.org/conda-forge/noarch/pluggy-1.3.0-pyhd8ed1ab_0.conda#2390bd10bed1f3fdc7a537fb5a447d8d +https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.19.0-pyhd8ed1ab_0.conda#7baa10fa8073c371155cf451b71b848d +https://conda.anaconda.org/conda-forge/linux-aarch64/psutil-5.9.8-py38h9579f32_0.conda#48ba0498b0437fadaae891a0851d1223 +https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd3deb0d_0.tar.bz2#359eeb6536da0e687af562ed265ec263 +https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.2-pyhd8ed1ab_0.tar.bz2#6784285c7e55cb7212efabc79e4c2883 +https://conda.anaconda.org/conda-forge/linux-aarch64/pycosat-0.6.6-py38h9579f32_0.conda#614fb7fd080f1d78ac521a7a526d15f6 +https://conda.anaconda.org/conda-forge/noarch/pycparser-2.21-pyhd8ed1ab_0.tar.bz2#076becd9e05608f8dc72757d5f3a91ff +https://conda.anaconda.org/conda-forge/noarch/pygments-2.17.2-pyhd8ed1ab_0.conda#140a7f159396547e9799aa98f9f0742e +https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.1.1-pyhd8ed1ab_0.conda#176f7d56f0cfe9008bdf1bccd7de02fb +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2#2a7de29fb590ca14b5243c4c812c8025 +https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.19.1-pyhd8ed1ab_0.conda#4d3ceee3af4b0f9a1f48f57176bf8625 +https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda#a61bf9ec79426938ff785eb69dbb1960 +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2023.4-pyhd8ed1ab_0.conda#c79cacf8a06a51552fc651652f170208 +https://conda.anaconda.org/conda-forge/noarch/pytz-2023.3.post1-pyhd8ed1ab_0.conda#c93346b446cd08c169d843ae5fc0da97 +https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.1-py38h9579f32_1.conda#84a50ac70736cc75812b2a49b580415b +https://conda.anaconda.org/conda-forge/linux-aarch64/pyzmq-25.1.2-py38ha083373_0.conda#cfc022afe7d90a031feba5b199a48775 +https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2#912a71cc01012ee38e6b90ddd561e36f +https://conda.anaconda.org/conda-forge/linux-aarch64/rpds-py-0.17.1-py38hf532af8_0.conda#12c614d9c1950942aa9921228eaefbc3 +https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml.clib-0.2.7-py38h9579f32_2.conda#0f2220c05bc739b3d390a0bd12c9029a +https://conda.anaconda.org/conda-forge/noarch/send2trash-1.8.2-pyh41d4057_0.conda#ada5a17adcd10be4fc7e37e4166ba0e2 +https://conda.anaconda.org/conda-forge/noarch/setuptools-69.0.3-pyhd8ed1ab_0.conda#40695fdfd15a92121ed2922900d0308b +https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2#e5f25f8dbc060e9a8d912e432202afc2 +https://conda.anaconda.org/conda-forge/noarch/sniffio-1.3.0-pyhd8ed1ab_0.tar.bz2#dd6cbc539e74cb1f430efbd4575b9303 +https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.5-pyhd8ed1ab_1.conda#3f144b2c34f8cb5a9abd9ed23a39c561 +https://conda.anaconda.org/conda-forge/noarch/tomli-2.0.1-pyhd8ed1ab_0.tar.bz2#5844808ffab9ebdb694585b50ba02a96 +https://conda.anaconda.org/conda-forge/linux-aarch64/tornado-6.3.3-py38hea3b116_1.conda#ad8257a473627cd17562cb7264f12dcb +https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.1-pyhd8ed1ab_0.conda#1c6acfdc7ecbfe09954c4216da99c146 +https://conda.anaconda.org/conda-forge/noarch/types-python-dateutil-2.8.19.20240106-pyhd8ed1ab_0.conda#c9096a546660b9079dce531c0039e074 +https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.9.0-pyha770c72_0.conda#a92a6440c3fe7052d63244f3aba2a4a7 +https://conda.anaconda.org/conda-forge/noarch/typing_utils-0.1.0-pyhd8ed1ab_0.tar.bz2#eb67e3cace64c66233e2d35949e20f92 +https://conda.anaconda.org/conda-forge/linux-aarch64/unicodedata2-15.1.0-py38h9579f32_0.conda#b9ed292773b0126e79d4cf5e6eeeab8a +https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_0.conda#0944dc65cb4a9b5b68522c3bb585d41c +https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda#68f0738df502a14213624b288c60c9ad +https://conda.anaconda.org/conda-forge/noarch/webcolors-1.13-pyhd8ed1ab_0.conda#166212fe82dad8735550030488a01d03 +https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_2.conda#daf5160ff9cde3a468556965329085b9 +https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.7.0-pyhd8ed1ab_0.conda#50ad31e07d706aae88b14a4ac9c73f23 +https://conda.anaconda.org/conda-forge/noarch/wheel-0.42.0-pyhd8ed1ab_0.conda#1cdea58981c5cbc17b51973bcaddcea7 +https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.9-pyhd8ed1ab_0.conda#82617d07b2f5f5a96296d3c19684b37a +https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda#2e4d6bc0b14e10f895fc6791a7d9b26a +https://conda.anaconda.org/conda-forge/noarch/anyio-4.2.0-pyhd8ed1ab_0.conda#81ce9f3d9697b534d95118bb86c8a07e +https://conda.anaconda.org/conda-forge/noarch/asttokens-2.4.1-pyhd8ed1ab_0.conda#5f25798dcefd8252ce5f9dc494d5f571 +https://conda.anaconda.org/conda-forge/noarch/async-lru-2.0.4-pyhd8ed1ab_0.conda#3d081de3a6ea9f894bbb585e8e3a4dcb +https://conda.anaconda.org/conda-forge/noarch/babel-2.14.0-pyhd8ed1ab_0.conda#9669586875baeced8fc30c0826c3270e +https://conda.anaconda.org/conda-forge/noarch/beautifulsoup4-4.12.3-pyha770c72_0.conda#332493000404d8411859539a5a630865 +https://conda.anaconda.org/conda-forge/noarch/bleach-6.1.0-pyhd8ed1ab_0.conda#0ed9d7c0e9afa7c025807a9a8136ea3e +https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2#9b347a7ec10940d3f7941ff6c460b551 +https://conda.anaconda.org/conda-forge/linux-aarch64/cffi-1.16.0-py38h4ab679b_0.conda#601106b459f7b9300e8b7b90ab0e1f54 +https://conda.anaconda.org/conda-forge/noarch/comm-0.2.1-pyhd8ed1ab_0.conda#f4385072f4909bc974f6675a36e76796 +https://conda.anaconda.org/conda-forge/linux-aarch64/fonttools-4.47.2-py38h9579f32_0.conda#ef0839bdfa775707a503b24a99f1110c +https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-7.0.1-pyha770c72_0.conda#746623a787e06191d80a2133e5daff17 +https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.1.1-pyhd8ed1ab_0.conda#3d5fa25cf42f3f32a12b2d874ace8574 +https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.1-pyhd8ed1ab_0.conda#81a3be0b2023e1ea8555781f0ad904a2 +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7d8df6509ba635247ff9aea31134262 +https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_0.conda#bfdb7c5c6ad1077c82a69a8642c87aff +https://conda.anaconda.org/conda-forge/linux-aarch64/jupyter_core-5.7.1-py38he3eb160_0.conda#f2a549f6d22e245912aaaac41b2edf4c +https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_0.conda#3f0915b1fb2252ab73686a533c5f9d3f +https://conda.anaconda.org/conda-forge/linux-aarch64/libcblas-3.9.0-21_linuxaarch64_openblas.conda#7eb9aa7a90f067f8dbfede586cdc55cd +https://conda.anaconda.org/conda-forge/linux-aarch64/liblapack-3.9.0-21_linuxaarch64_openblas.conda#ab08b651e3630c20d3032e59859f34f7 +https://conda.anaconda.org/conda-forge/linux-aarch64/libmamba-1.5.6-hea3be6c_0.conda#feb0be90db1e014dde541cafa78efc15 +https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.6-pyhd8ed1ab_0.tar.bz2#b21613793fcc81d944c76c9f2864a7de +https://conda.anaconda.org/conda-forge/noarch/overrides-7.6.0-pyhd8ed1ab_0.conda#3ed0205566229c23c70fd9e6318e0568 +https://conda.anaconda.org/conda-forge/noarch/pexpect-4.8.0-pyh1a96a4e_2.tar.bz2#330448ce4403cc74990ac07c555942a1 +https://conda.anaconda.org/conda-forge/linux-aarch64/pillow-10.2.0-py38hf904494_0.conda#8e6d79fca43eb00bf822c18db58e6ab2 +https://conda.anaconda.org/conda-forge/noarch/pip-23.3.2-pyhd8ed1ab_0.conda#8591c748f98dcc02253003533bc2e4b1 +https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda#0bf64bf10eee21f46ac83c161917fa86 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2#dd999d1cc9f79e67dbb855c8924c7984 +https://conda.anaconda.org/conda-forge/noarch/referencing-0.32.1-pyhd8ed1ab_0.conda#753a592b4e99d7d2cde6a8fd0797f414 +https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_0.tar.bz2#fed45fc5ea0813240707998abe49f520 +https://conda.anaconda.org/conda-forge/linux-aarch64/ruamel.yaml-0.18.5-py38h9579f32_0.conda#9ce349ff3a0c46af59796e9ea2014c4b +https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.0-pyh0d859eb_0.conda#e463f348b8b0eb62c9f7c6fbc780286c +https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.2.1-pyhd8ed1ab_0.tar.bz2#7234c9eefff659501cd2fe0d2ede4d48 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.1-pyhd8ed1ab_0.conda#03c97908b976498dcae97eb4e4f3149c +https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.9.0-hd8ed1ab_0.conda#c16524c1b7227dc80b36b4fa6f77cc86 +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.1.0-pyhd8ed1ab_0.conda#f8ced8ee63830dec7ecc1be048d1470a +https://conda.anaconda.org/conda-forge/linux-aarch64/argon2-cffi-bindings-21.2.0-py38h9579f32_4.conda#3460f222597376592b6e1ac5eac0bdce +https://conda.anaconda.org/conda-forge/noarch/arrow-1.3.0-pyhd8ed1ab_0.conda#b77d8c2313158e6e461ca0efb1c2c508 +https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_0.tar.bz2#642d35437078749ef23a5dca2c9bb1f3 +https://conda.anaconda.org/conda-forge/noarch/importlib-resources-6.1.1-pyhd8ed1ab_0.conda#d04bd1b5bed9177dd7c3cef15e2b6710 +https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.0.1-hd8ed1ab_0.conda#4a2f43a20fa404b998859c6a470ba316 +https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2023.12.1-pyhd8ed1ab_0.conda#a0e4efb5f35786a05af4809a2fb1f855 +https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.1-pyhd8ed1ab_0.conda#919e6d570f8b3839f3a1ed99b25088af +https://conda.anaconda.org/conda-forge/linux-aarch64/libmambapy-1.5.6-py38h513f8d8_0.conda#58b6a04a2bebe603ab1fdeaac7c1362b +https://conda.anaconda.org/conda-forge/linux-aarch64/numpy-1.24.4-py38he3f4005_0.conda#da4a2794009cc9a747ed1e5d16bbdf77 +https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.42-hd8ed1ab_0.conda#85a2189ecd2fcdd86e92b2d4ea8fe461 +https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b +https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda#e7df0fdd404616638df5ece6e69ba7af +https://conda.anaconda.org/conda-forge/linux-aarch64/zstandard-0.22.0-py38hc2e9a1f_0.conda#93dac68ac541ed7cc1ba83ed34691ebf +https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-23.1.0-pyhd8ed1ab_0.conda#3afef1f55a1366b4d3b6a0d92e2235e4 +https://conda.anaconda.org/conda-forge/noarch/conda-package-streaming-0.9.0-pyhd8ed1ab_0.conda#38253361efb303deead3eab39ae9269b +https://conda.anaconda.org/conda-forge/linux-aarch64/contourpy-1.1.1-py38hb4b5b6f_1.conda#6e4c4fe499bfbd85d40fa5de0f82a837 +https://conda.anaconda.org/conda-forge/noarch/ipython-8.12.2-pyh41d4057_0.conda#acebfd89278ecac2a67b60b657e00d5c +https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_0.tar.bz2#4cb68948e0b8429534380243d063a27a +https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.21.1-pyhd8ed1ab_0.conda#8a3a3d01629da20befa340919e3dd2c4 +https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.0-pyhd8ed1ab_0.conda#6bd3f1069cdebb44c7ae9efb900e312d +https://conda.anaconda.org/conda-forge/linux-aarch64/pandas-2.0.3-py38h958bb2c_1.conda#1e30c2de3a3ce99ab1a4ab068a7551b9 +https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.0-pyhd8ed1ab_0.conda#134b2b57b7865d2316a7cce1915a51ed +https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.2.0-pyh38be061_0.conda#8a3ae7f6318376aa08ea753367bb7dd6 +https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.0-pyhd33586a_0.conda#10915bfa94b94f4ad0f347efd124a339 +https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.1-pyhd8ed1ab_0.conda#2605fae5ee27100e5f10037baebf4d41 +https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.21.1-pyhd8ed1ab_0.conda#26bce4b5405738c09304d4f4796b2c2a +https://conda.anaconda.org/conda-forge/linux-aarch64/matplotlib-base-3.7.3-py38h7709db9_0.conda#e4b21e67e5e4f7532501a8f83ef6c107 +https://conda.anaconda.org/conda-forge/noarch/nbformat-5.9.2-pyhd8ed1ab_0.conda#61ba076de6530d9301a0053b02f093d2 +https://conda.anaconda.org/conda-forge/linux-aarch64/scipy-1.10.1-py38he3f4005_3.conda#ea7219314f2ffcbaefeec1e42a15122f +https://conda.anaconda.org/conda-forge/noarch/ipympl-0.9.3-pyhd8ed1ab_0.conda#da113e1ecd782afd5ed2f7b5187aaea8 +https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.9.0-pyhd8ed1ab_0.conda#00ba25993f0dba38cf72a7224e33289f +https://conda.anaconda.org/conda-forge/noarch/nbclient-0.8.0-pyhd8ed1ab_0.conda#e78da91cf428faaf05701ce8cc8f2f9b +https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.14.2-pyhd8ed1ab_0.conda#631800aa8cc7ccf61e70087355d95827 +https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.12.5-pyhd8ed1ab_0.conda#755177a956fa6dd90d5cfcbbb5084de2 +https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.2.2-pyhd8ed1ab_0.conda#ed56b103cac2db68f22909e9f5cca6b6 +https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.25.2-pyhd8ed1ab_0.conda#f45557d5551b54dc2a74133a310bc1ba +https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.3-pyhd8ed1ab_0.conda#67e0fe74c156267d9159e9133df7fd37 +https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.0.11-pyhd8ed1ab_0.conda#42370604825af7396ef4317b67b22e2c +https://conda.anaconda.org/conda-forge/linux-aarch64/conda-23.11.0-py38he3eb160_1.conda#d9d7c83ff042fab98220a5cc7edac213 +https://conda.anaconda.org/conda-forge/noarch/conda-libmamba-solver-23.12.0-pyhd8ed1ab_0.conda#e877d5150e73a0844ea2939be110c3b1 diff --git a/env_installer/conda-osx-64.lock b/env_installer/conda-osx-64.lock index ec8547ba..79a1057f 100644 --- a/env_installer/conda-osx-64.lock +++ b/env_installer/conda-osx-64.lock @@ -30,7 +30,7 @@ https://conda.anaconda.org/conda-forge/osx-64/fmt-10.1.1-h7728843_1.conda#de48ed https://conda.anaconda.org/conda-forge/osx-64/lerc-4.0.0-hb486fe8_0.tar.bz2#f9d6a4c82889d5ecedec1d90eb673c55 https://conda.anaconda.org/conda-forge/osx-64/libbrotlidec-1.1.0-h0dc2134_1.conda#9ee0bab91b2ca579e10353738be36063 https://conda.anaconda.org/conda-forge/osx-64/libbrotlienc-1.1.0-h0dc2134_1.conda#8a421fe09c6187f0eb5e2338a8a8be6d -https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-13.2.0-h2873a65_1.conda#3af564516b5163cd8cc08820413854bc +https://conda.anaconda.org/conda-forge/osx-64/libgfortran5-13.2.0-h2873a65_2.conda#d510329afae76a26709e23b8509d2d48 https://conda.anaconda.org/conda-forge/osx-64/libpng-1.6.39-ha978bb4_0.conda#35e4928794c5391aec14ffdf1deaaee5 https://conda.anaconda.org/conda-forge/osx-64/libsolv-0.7.27-hf4d7fad_0.conda#dda8df0e28d488cc20dec52903dddcad https://conda.anaconda.org/conda-forge/osx-64/libsqlite-3.44.2-h92b6c6a_0.conda#d4419f90019e6a2b152cd4d32f73a82f @@ -48,7 +48,7 @@ https://conda.anaconda.org/conda-forge/osx-64/brotli-bin-1.1.0-h0dc2134_1.conda# https://conda.anaconda.org/conda-forge/osx-64/freetype-2.12.1-h60636b9_2.conda#25152fce119320c980e5470e64834b50 https://conda.anaconda.org/conda-forge/osx-64/libarchive-3.7.2-hd35d340_1.conda#8c7b79b20a67287a87b39df8a8c8dcc4 https://conda.anaconda.org/conda-forge/osx-64/libedit-3.1.20191231-h0678c8f_2.tar.bz2#6016a8a1d0e63cac3de2c352cd40208b -https://conda.anaconda.org/conda-forge/osx-64/libgfortran-5.0.0-13_2_0_h97931a8_1.conda#b55fd11ab6318a6e67ac191309701d5a +https://conda.anaconda.org/conda-forge/osx-64/libgfortran-5.0.0-13_2_0_h97931a8_2.conda#b8e969b34c05efc0c7d6bcd4f6bf5612 https://conda.anaconda.org/conda-forge/osx-64/libnghttp2-1.58.0-h64cf6d3_1.conda#faecc55c2a8155d9ff1c0ff9a0fef64f https://conda.anaconda.org/conda-forge/osx-64/libssh2-1.11.0-hd019ec5_0.conda#ca3a72efba692c59a90d4b9fc0dfe774 https://conda.anaconda.org/conda-forge/osx-64/libtiff-4.6.0-h684deea_2.conda#2ca10a325063e000ad6d2a5900061e0d @@ -56,7 +56,7 @@ https://conda.anaconda.org/conda-forge/osx-64/readline-8.2-h9e318b2_1.conda#f17f https://conda.anaconda.org/conda-forge/osx-64/brotli-1.1.0-h0dc2134_1.conda#9272dd3b19c4e8212f8542cefd5c3d67 https://conda.anaconda.org/conda-forge/osx-64/krb5-1.21.2-hb884880_0.conda#80505a68783f01dc8d7308c075261b2f https://conda.anaconda.org/conda-forge/osx-64/lcms2-2.16-ha2f27b4_0.conda#1442db8f03517834843666c422238c9b -https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.25-openmp_hfef2a42_0.conda#a01b96f00c3155c830d98a518c7dcbfb +https://conda.anaconda.org/conda-forge/osx-64/libopenblas-0.3.26-openmp_hfef2a42_0.conda#9df60162aea811087267b515f359536c https://conda.anaconda.org/conda-forge/osx-64/openjpeg-2.5.0-ha4da562_3.conda#40a36f8e9a6fdf6a78c6428ee6c44188 https://conda.anaconda.org/conda-forge/osx-64/python-3.8.18-h5ba8234_1_cpython.conda#34024ccde0d6a5f42b2ef4c3d08b8f65 https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.3-pyhd8ed1ab_0.tar.bz2#54ac328d703bff191256ffa1183126d1 @@ -83,13 +83,13 @@ https://conda.anaconda.org/conda-forge/noarch/json5-0.9.14-pyhd8ed1ab_0.conda#da https://conda.anaconda.org/conda-forge/osx-64/jsonpointer-2.4-py38h50d1736_3.conda#587da7fcb04fb5d49d7caf0f0010e17f https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.9-pyhd8ed1ab_0.conda#8370e0a9dc443f9b45a23fd30e7a6b3b https://conda.anaconda.org/conda-forge/osx-64/kiwisolver-1.4.5-py38h15a1a5b_1.conda#a288aa741a88b5242389f05dcd9e6670 -https://conda.anaconda.org/conda-forge/osx-64/libblas-3.9.0-20_osx64_openblas.conda#1673476d205d14a9042172be795f63cb +https://conda.anaconda.org/conda-forge/osx-64/libblas-3.9.0-21_osx64_openblas.conda#23286066c595986aa0df6452a8416c08 https://conda.anaconda.org/conda-forge/osx-64/libcurl-8.5.0-h726d00d_0.conda#86d749e27fe00fa6b7d790a6feaa22a2 -https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.3-py38hcafd530_1.conda#ed178f435d4626880e8f5dd5d5f0e65c +https://conda.anaconda.org/conda-forge/osx-64/markupsafe-2.1.4-py38hae2e43d_0.conda#dd751925b9144049990a5a2653c2aa78 https://conda.anaconda.org/conda-forge/osx-64/menuinst-2.0.2-py38h50d1736_0.conda#d46a02b39478d67c710e2878af2a8ec1 https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda#5cbee699846772cc939bef23a0d524ed https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 -https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.5.9-pyhd8ed1ab_0.conda#6c59cb840d511a1a997998d55e68516c +https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_0.conda#6598c056f64dc8800d40add25e4e2c34 https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda#79002079284aa895f883c6b7f3f88fd6 https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9 https://conda.anaconda.org/conda-forge/noarch/parso-0.8.3-pyhd8ed1ab_0.tar.bz2#17a565a0c3899244e938cdf417e7b094 @@ -152,11 +152,11 @@ https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7 https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_0.conda#bfdb7c5c6ad1077c82a69a8642c87aff https://conda.anaconda.org/conda-forge/osx-64/jupyter_core-5.7.1-py38h50d1736_0.conda#ba23db53d88425efc9ff8e4e78ea6d5e https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_0.conda#3f0915b1fb2252ab73686a533c5f9d3f -https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.9.0-20_osx64_openblas.conda#b324ad206d39ce529fb9073f9d062062 -https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.9.0-20_osx64_openblas.conda#704bfc2af1288ea973b6755281e6ad32 +https://conda.anaconda.org/conda-forge/osx-64/libcblas-3.9.0-21_osx64_openblas.conda#7a1b54774bad723e8ba01ca48eb301b5 +https://conda.anaconda.org/conda-forge/osx-64/liblapack-3.9.0-21_osx64_openblas.conda#cf0e4d82cfca6cd9d6c9ed3df45907c9 https://conda.anaconda.org/conda-forge/osx-64/libmamba-1.5.6-ha449628_0.conda#549575d7237a1aba16119969b4a6b8a7 https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.6-pyhd8ed1ab_0.tar.bz2#b21613793fcc81d944c76c9f2864a7de -https://conda.anaconda.org/conda-forge/noarch/overrides-7.4.0-pyhd8ed1ab_0.conda#4625b7b01d7f4ac9c96300a5515acfaa +https://conda.anaconda.org/conda-forge/noarch/overrides-7.6.0-pyhd8ed1ab_0.conda#3ed0205566229c23c70fd9e6318e0568 https://conda.anaconda.org/conda-forge/noarch/pexpect-4.8.0-pyh1a96a4e_2.tar.bz2#330448ce4403cc74990ac07c555942a1 https://conda.anaconda.org/conda-forge/noarch/pip-23.3.2-pyhd8ed1ab_0.conda#8591c748f98dcc02253003533bc2e4b1 https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda#0bf64bf10eee21f46ac83c161917fa86 diff --git a/env_installer/conda-osx-arm64.lock b/env_installer/conda-osx-arm64.lock index b3f35af5..26d507f3 100644 --- a/env_installer/conda-osx-arm64.lock +++ b/env_installer/conda-osx-arm64.lock @@ -30,7 +30,7 @@ https://conda.anaconda.org/conda-forge/osx-arm64/fmt-10.1.1-h2ffa867_1.conda#16b https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-h9a09cb3_0.tar.bz2#de462d5aacda3b30721b512c5da4e742 https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.1.0-hb547adb_1.conda#ee1a519335cc10d0ec7e097602058c0a https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.1.0-hb547adb_1.conda#d7e077f326a98b2cc60087eaff7c730b -https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-13.2.0-hf226fd6_1.conda#4480d71b98c87faafab132d33e23135e +https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-13.2.0-hf226fd6_2.conda#55c6859a3606c1516d89768a05ce9074 https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.39-h76d750c_0.conda#0078e6327c13cfdeae6ff7601e360383 https://conda.anaconda.org/conda-forge/osx-arm64/libsolv-0.7.27-h9e231a4_0.conda#f5568a2094eed1fc5742dc6d4b91e222 https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.44.2-h091b4b1_0.conda#d7e1af696cfadec251a0abdd7b79ed77 @@ -48,7 +48,7 @@ https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.1.0-hb547adb_1.con https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.12.1-hadb7bae_2.conda#e6085e516a3e304ce41a8ee08b9b89ad https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.7.2-hcacb583_1.conda#1c8c447ce71bf5f769674b621142a73a https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20191231-hc8eb9b7_2.tar.bz2#30e4362988a2623e9eb34337b83e01f9 -https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-13_2_0_hd922786_1.conda#1ad37a5c60c250bb2b4a9f75563e181c +https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-13_2_0_hd922786_2.conda#50c44da4cd89e99a5b18382f565585d8 https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.58.0-ha4dd798_1.conda#1813e066bfcef82de579a0be8a766df4 https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.0-h7a5bd25_0.conda#029f7dc931a3b626b94823bc77830b01 https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.6.0-ha8a6c65_2.conda#596d6d949bab9a75a492d451f521f457 @@ -56,7 +56,7 @@ https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h92ec313_1.conda#8 https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.1.0-hb547adb_1.conda#a33aa58d448cbc054f887e39dd1dfaea https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.2-h92f50d5_0.conda#92f1cff174a538e0722bf2efb16fc0b2 https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.16-ha0e7c42_0.conda#66f6c134e76fe13cce8a9ea5814b5dd5 -https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.25-openmp_h6c19121_0.conda#a1843550403212b9dedeeb31466ade03 +https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.26-openmp_h6c19121_0.conda#000970261d954431ccca3cce68d873d8 https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.0-h4c1507b_3.conda#4127dd217a010d9c6cbefdaae07d9f19 https://conda.anaconda.org/conda-forge/osx-arm64/python-3.8.18-h2469fbe_1_cpython.conda#1b29de497116d0fd28043aabf8127774 https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.3-pyhd8ed1ab_0.tar.bz2#54ac328d703bff191256ffa1183126d1 @@ -83,13 +83,13 @@ https://conda.anaconda.org/conda-forge/noarch/json5-0.9.14-pyhd8ed1ab_0.conda#da https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-2.4-py38h10201cd_3.conda#a2358babcb4baaa338eb94cc3683816b https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.9-pyhd8ed1ab_0.conda#8370e0a9dc443f9b45a23fd30e7a6b3b https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.4.5-py38h9afee92_1.conda#1730b76fb22b82403f5033e1450d9580 -https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-20_osxarm64_openblas.conda#49bc8dec26663241ee064b2d7116ec2d +https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-21_osxarm64_openblas.conda#b3804f4af39eca9d77360b12811e6d1d https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.5.0-h2d989ff_0.conda#f1211ed00947a84e15a964a8f459f620 -https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-2.1.3-py38hb192615_1.conda#44bdf93e4d7019f5a64eff659ad86801 +https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-2.1.4-py38h336bac9_0.conda#d97606965ac212842983a866c910e720 https://conda.anaconda.org/conda-forge/osx-arm64/menuinst-2.0.2-py38h10201cd_0.conda#d4f2e5a108aa0476819e364dcdd45307 https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda#5cbee699846772cc939bef23a0d524ed https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 -https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.5.9-pyhd8ed1ab_0.conda#6c59cb840d511a1a997998d55e68516c +https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_0.conda#6598c056f64dc8800d40add25e4e2c34 https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda#79002079284aa895f883c6b7f3f88fd6 https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9 https://conda.anaconda.org/conda-forge/noarch/parso-0.8.3-pyhd8ed1ab_0.tar.bz2#17a565a0c3899244e938cdf417e7b094 @@ -153,11 +153,11 @@ https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.3-pyhd8ed1ab_0.conda#e7 https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_0.conda#bfdb7c5c6ad1077c82a69a8642c87aff https://conda.anaconda.org/conda-forge/osx-arm64/jupyter_core-5.7.1-py38h10201cd_0.conda#e344da63c93d91af29ffa4a56582ed6b https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_0.conda#3f0915b1fb2252ab73686a533c5f9d3f -https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-20_osxarm64_openblas.conda#89f4718753c08afe8cda4dd5791ba94c -https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-20_osxarm64_openblas.conda#1fefac78f2315455ce2d7f34782eac0a +https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-21_osxarm64_openblas.conda#48e9d42c65ce664d8fccef2ac6af853c +https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-21_osxarm64_openblas.conda#a4510e3913ef552d69ab2080a0048523 https://conda.anaconda.org/conda-forge/osx-arm64/libmamba-1.5.6-h90c426b_0.conda#d17c7a5761e8983d97ccd650f5e6baaa https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.6-pyhd8ed1ab_0.tar.bz2#b21613793fcc81d944c76c9f2864a7de -https://conda.anaconda.org/conda-forge/noarch/overrides-7.4.0-pyhd8ed1ab_0.conda#4625b7b01d7f4ac9c96300a5515acfaa +https://conda.anaconda.org/conda-forge/noarch/overrides-7.6.0-pyhd8ed1ab_0.conda#3ed0205566229c23c70fd9e6318e0568 https://conda.anaconda.org/conda-forge/noarch/pexpect-4.8.0-pyh1a96a4e_2.tar.bz2#330448ce4403cc74990ac07c555942a1 https://conda.anaconda.org/conda-forge/noarch/pip-23.3.2-pyhd8ed1ab_0.conda#8591c748f98dcc02253003533bc2e4b1 https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda#0bf64bf10eee21f46ac83c161917fa86 diff --git a/env_installer/conda-win-64.lock b/env_installer/conda-win-64.lock index 036fb2f2..8573916c 100644 --- a/env_installer/conda-win-64.lock +++ b/env_installer/conda-win-64.lock @@ -3,7 +3,7 @@ # input_hash: 2497f806941a75dcb35434877fe80a16ab20675d5fce640b6a6a7cde05c6b5d9 @EXPLICIT https://conda.anaconda.org/conda-forge/win-64/ca-certificates-2023.11.17-h56e8100_0.conda#1163114b483f26761f993c709e65271f -https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2023.2.0-h57928b3_50497.conda#a401f3cae152deb75bbed766a90a6312 +https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.0.0-h57928b3_49840.conda#17969ee589e13b49e36f39549d42cd33 https://conda.anaconda.org/conda-forge/win-64/msys2-conda-epoch-20160418-1.tar.bz2#b0309b72560df66f71a9d5e34a5efdfa https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2#878f923dd6acc8aeb47a75da6c4098be https://conda.anaconda.org/conda-forge/win-64/python_abi-3.8-4_cp38.conda#b1059de1664cef9a785dda079a50f1ed @@ -78,11 +78,11 @@ https://conda.anaconda.org/conda-forge/win-64/libarchive-3.7.2-h313118b_1.conda# https://conda.anaconda.org/conda-forge/win-64/libcurl-8.5.0-hd5e4a3a_0.conda#c95eb3d60266dd47b8eb864e10d6bcf3 https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.9.3-default_haede6df_1009.conda#87da045f6d26ce9fe20ad76a18f6a18a https://conda.anaconda.org/conda-forge/win-64/libtiff-4.6.0-h6e2ebb7_2.conda#08d653b74ee2dec0131ad4259ffbb126 -https://conda.anaconda.org/conda-forge/win-64/markupsafe-2.1.3-py38h91455d4_1.conda#c82da8f0baf093cc23546ce37f68df87 +https://conda.anaconda.org/conda-forge/win-64/markupsafe-2.1.4-py38h91455d4_0.conda#1e420c9ccc5f8a563c7302a34443852d https://conda.anaconda.org/conda-forge/win-64/menuinst-2.0.2-py38hd3f51b4_0.conda#bec189dd0a99f5a3e68475ad68636912 https://conda.anaconda.org/conda-forge/noarch/mistune-3.0.2-pyhd8ed1ab_0.conda#5cbee699846772cc939bef23a0d524ed https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyh9f0ad1d_0.tar.bz2#2ba8498c1018c1e9c61eb99b973dfe19 -https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.5.9-pyhd8ed1ab_0.conda#6c59cb840d511a1a997998d55e68516c +https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_0.conda#6598c056f64dc8800d40add25e4e2c34 https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda#79002079284aa895f883c6b7f3f88fd6 https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2#457c2c8c08e54905d6954e79cb5b5db9 https://conda.anaconda.org/conda-forge/noarch/parso-0.8.3-pyhd8ed1ab_0.tar.bz2#17a565a0c3899244e938cdf417e7b094 @@ -153,7 +153,7 @@ https://conda.anaconda.org/conda-forge/win-64/libmamba-1.5.6-h3f09ed1_0.conda#b9 https://conda.anaconda.org/conda-forge/win-64/libxcb-1.15-hcd874cb_0.conda#090d91b69396f14afef450c285f9758c https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.6-pyhd8ed1ab_0.tar.bz2#b21613793fcc81d944c76c9f2864a7de https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.0-h3d672ee_3.conda#45a9628a04efb6fc326fff0a8f47b799 -https://conda.anaconda.org/conda-forge/noarch/overrides-7.4.0-pyhd8ed1ab_0.conda#4625b7b01d7f4ac9c96300a5515acfaa +https://conda.anaconda.org/conda-forge/noarch/overrides-7.6.0-pyhd8ed1ab_0.conda#3ed0205566229c23c70fd9e6318e0568 https://conda.anaconda.org/conda-forge/noarch/pip-23.3.2-pyhd8ed1ab_0.conda#8591c748f98dcc02253003533bc2e4b1 https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.42-pyha770c72_0.conda#0bf64bf10eee21f46ac83c161917fa86 https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh0701188_6.tar.bz2#56cd9fe388baac0e90c7149cfac95b60 @@ -176,7 +176,7 @@ https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-7.0.1-hd8ed1ab_ https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2023.12.1-pyhd8ed1ab_0.conda#a0e4efb5f35786a05af4809a2fb1f855 https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.1-pyhd8ed1ab_0.conda#919e6d570f8b3839f3a1ed99b25088af https://conda.anaconda.org/conda-forge/win-64/libmambapy-1.5.6-py38h9d63bcc_0.conda#13036d13d71913b0b66dea5ccfc154ab -https://conda.anaconda.org/conda-forge/win-64/mkl-2023.2.0-h6a75c08_50497.conda#064cea9f45531e7b53584acf4bd8b044 +https://conda.anaconda.org/conda-forge/win-64/mkl-2024.0.0-h66d3029_49657.conda#006b65d9cd436247dfe053df772e041d https://conda.anaconda.org/conda-forge/win-64/pillow-10.2.0-py38hc375fad_0.conda#3111fc62687b4185f829215ac5c47a0e https://conda.anaconda.org/conda-forge/noarch/prompt_toolkit-3.0.42-hd8ed1ab_0.conda#85a2189ecd2fcdd86e92b2d4ea8fe461 https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.2-pyhd8ed1ab_0.conda#e7df0fdd404616638df5ece6e69ba7af @@ -188,14 +188,14 @@ https://conda.anaconda.org/conda-forge/noarch/ipython-8.12.2-pyh08f2357_0.conda# https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_0.tar.bz2#4cb68948e0b8429534380243d063a27a https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.21.1-pyhd8ed1ab_0.conda#8a3a3d01629da20befa340919e3dd2c4 https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.0-pyhd8ed1ab_0.conda#6bd3f1069cdebb44c7ae9efb900e312d -https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-20_win64_mkl.conda#6cad6cd2fbdeef4d651b8f752a4da960 +https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-21_win64_mkl.conda#ebba3846d11201fe54277e4965ba5250 https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda#a30144e4156cdbb236f99ebb49828f8b https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.2.0-pyh38be061_0.conda#8a3ae7f6318376aa08ea753367bb7dd6 https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.0-pyha63f2e9_0.conda#8b921cd22399207c2a6127a039d3dd20 https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.1-pyhd8ed1ab_0.conda#2605fae5ee27100e5f10037baebf4d41 https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.21.1-pyhd8ed1ab_0.conda#26bce4b5405738c09304d4f4796b2c2a -https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-20_win64_mkl.conda#e6d36cfcb2f2dff0f659d2aa0813eb2d -https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-20_win64_mkl.conda#9510d07424d70fcac553d86b3e4a7c14 +https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-21_win64_mkl.conda#38e5ec23bc2b62f9dd971143aa9dddb7 +https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-21_win64_mkl.conda#c4740f091cb75987390087934354a621 https://conda.anaconda.org/conda-forge/noarch/nbformat-5.9.2-pyhd8ed1ab_0.conda#61ba076de6530d9301a0053b02f093d2 https://conda.anaconda.org/conda-forge/noarch/pooch-1.8.0-pyhd8ed1ab_0.conda#134b2b57b7865d2316a7cce1915a51ed https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.9.0-pyhd8ed1ab_0.conda#00ba25993f0dba38cf72a7224e33289f diff --git a/env_installer/jlab_server.yaml b/env_installer/jlab_server.yaml index 0f64470f..e04fa306 100644 --- a/env_installer/jlab_server.yaml +++ b/env_installer/jlab_server.yaml @@ -14,6 +14,7 @@ dependencies: - scipy platforms: - linux-64 + - linux-aarch64 - osx-64 - osx-arm64 - win-64 diff --git a/env_installer/sign-osx-64.txt b/env_installer/sign-osx-64.txt index cc752c40..4d8f0d0d 100644 --- a/env_installer/sign-osx-64.txt +++ b/env_installer/sign-osx-64.txt @@ -145,7 +145,7 @@ lib/libncurses.6.dylib lib/libncursesw.6.dylib lib/libnghttp2.14.dylib lib/libomp.dylib -lib/libopenblasp-r0.3.25.dylib +lib/libopenblasp-r0.3.26.dylib lib/libopenjp2.2.5.0.dylib lib/libpanel.6.dylib lib/libpanelw.6.dylib diff --git a/package.json b/package.json index 47a5e828..4e5348b6 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "update_conda_lock": "cd env_installer && rimraf *.lock && conda-lock --kind explicit -f jlab_server.yaml && cd -", "clean_env_installer": "rimraf ./env_installer/jlab_server.tar.gz && rimraf ./env_installer/jlab_server", "create_env_installer:linux": "yarn clean_env_installer && conda-lock install --prefix ./env_installer/jlab_server ./env_installer/conda-linux-64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", + "create_env_installer:linux-aarch64": "yarn clean_env_installer && conda-lock install --prefix ./env_installer/jlab_server ./env_installer/conda-linux-aarch64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", "create_env_installer:osx-64": "yarn clean_env_installer && conda-lock install --prefix ./env_installer/jlab_server ./env_installer/conda-osx-64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", "create_env_installer:osx-arm64": "yarn clean_env_installer && conda-lock install --no-validate-platform --prefix ./env_installer/jlab_server ./env_installer/conda-osx-arm64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", "create_env_installer:win": "yarn clean_env_installer && conda-lock install --prefix ./env_installer/jlab_server ./env_installer/conda-win-64.lock && conda pack -p ./env_installer/jlab_server -o ./env_installer/jlab_server.tar.gz", @@ -47,7 +48,8 @@ "prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "prettier:check": "prettier --check \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", "stylelint": "yarn stylelint:check --fix", - "stylelint:check": "stylelint \"**/*.css\"" + "stylelint:check": "stylelint \"**/*.css\"", + "update-ufuzzy": "shx cp node_modules/@leeoniya/ufuzzy/dist/uFuzzy.iife.min.js src/assets/" }, "build": { "appId": "org.jupyter.jupyterlab-desktop", @@ -176,6 +178,7 @@ "@types/yargs": "^17.0.18", "@typescript-eslint/eslint-plugin": "~5.28.0", "@typescript-eslint/parser": "~5.28.0", + "@leeoniya/ufuzzy": "1.0.14", "electron": "^27.0.2", "electron-builder": "^24.6.4", "electron-notarize": "^1.2.2", @@ -184,13 +187,13 @@ "eslint-plugin-prettier": "~4.0.0", "eslint-plugin-react": "~7.29.4", "fs-extra": "~9.1.0", - "istextorbinary": "^6.0.0", "meow": "^6.0.1", "mini-css-extract-plugin": "^1.3.9", "node-watch": "^0.7.4", "prettier": "~2.1.1", "read-package-tree": "^5.1.6", "rimraf": "~3.0.0", + "shx": "^0.3.4", "stylelint": "^15.10.1", "stylelint-config-prettier": "^9.0.3", "stylelint-config-recommended": "^6.0.0", @@ -206,6 +209,7 @@ "electron-log": "^4.4.8", "fast-xml-parser": "^4.2.5", "fix-path": "^3.0.0", + "istextorbinary": "^6.0.0", "js-yaml": "^4.1.0", "node-fetch": "^2.6.7", "semver": "^7.5.4", diff --git a/scripts/copyassets.js b/scripts/copyassets.js index 28fc638b..22d5da25 100644 --- a/scripts/copyassets.js +++ b/scripts/copyassets.js @@ -49,22 +49,9 @@ function copyAssests() { path.join(dest, '../app-assets', 'titlebarview', 'titlebar.html') ); - fs.copySync( - path.join(srcDir, 'assets', 'icon.svg'), - path.join(dest, '../app-assets', 'icon.svg') - ); - fs.copySync( - path.join(srcDir, 'assets', 'progress-logo.svg'), - path.join(dest, '../app-assets', 'progress-logo.svg') - ); - fs.copySync( - path.join(srcDir, 'assets', 'jupyterlab-wordmark.svg'), - path.join(dest, '../app-assets', 'jupyterlab-wordmark.svg') - ); - fs.copySync( - path.join(srcDir, 'assets', 'copyable-span.js'), - path.join(dest, '../app-assets', 'copyable-span.js') - ); + fs.copySync(path.join(srcDir, 'assets'), path.join(dest, '../app-assets'), { + recursive: true + }); const toolkitPath = path.join( '../node_modules', diff --git a/src/assets/check-icon.svg b/src/assets/check-icon.svg new file mode 100644 index 00000000..56058719 --- /dev/null +++ b/src/assets/check-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/copy-icon.svg b/src/assets/copy-icon.svg new file mode 100644 index 00000000..dc341830 --- /dev/null +++ b/src/assets/copy-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ellipsis-vertical.svg b/src/assets/ellipsis-vertical.svg new file mode 100644 index 00000000..d5ef4e3b --- /dev/null +++ b/src/assets/ellipsis-vertical.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/info-icon.svg b/src/assets/info-icon.svg new file mode 100644 index 00000000..f5975635 --- /dev/null +++ b/src/assets/info-icon.svg @@ -0,0 +1 @@ + diff --git a/src/assets/rotate-right-icon.svg b/src/assets/rotate-right-icon.svg new file mode 100644 index 00000000..ae89da8c --- /dev/null +++ b/src/assets/rotate-right-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/uFuzzy.iife.min.js b/src/assets/uFuzzy.iife.min.js new file mode 100644 index 00000000..a0861c4d --- /dev/null +++ b/src/assets/uFuzzy.iife.min.js @@ -0,0 +1,2 @@ +/*! https://github.com/leeoniya/uFuzzy (v1.0.14) */ +var uFuzzy=function(){"use strict";const e=new Intl.Collator("en",{numeric:!0,sensitivity:"base"}).compare,t=1/0,l=e=>e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),n="eexxaacctt",r=(e,t,l)=>e.replace("A-Z",t).replace("a-z",l),i={unicode:!1,alpha:null,interSplit:"[^A-Za-z\\d']+",intraSplit:"[a-z][A-Z]",intraBound:"[A-Za-z]\\d|\\d[A-Za-z]|[a-z][A-Z]",interLft:0,interRgt:0,interChars:".",interIns:t,intraChars:"[a-z\\d']",intraIns:null,intraContr:"'[a-z]{1,2}\\b",intraMode:0,intraSlice:[1,t],intraSub:null,intraTrn:null,intraDel:null,intraFilt:()=>!0,sort:(t,l)=>{let{idx:n,chars:r,terms:i,interLft2:s,interLft1:a,start:g,intraIns:f,interIns:h}=t;return n.map(((e,t)=>t)).sort(((t,u)=>r[u]-r[t]||f[t]-f[u]||i[u]+s[u]+.5*a[u]-(i[t]+s[t]+.5*a[t])||h[t]-h[u]||g[t]-g[u]||e(l[n[t]],l[n[u]])))}},s=(e,l)=>0==l?"":1==l?e+"??":l==t?e+"*?":e+`{0,${l}}?`,a="(?:\\b|_)";function g(e){e=Object.assign({},i,e);let{unicode:t,interLft:g,interRgt:f,intraMode:u,intraSlice:c,intraIns:o,intraSub:p,intraTrn:d,intraDel:m,intraContr:x,intraSplit:b,interSplit:R,intraBound:L,intraChars:A}=e;o??=u,p??=u,d??=u,m??=u;let S=e.letters??e.alpha;if(null!=S){let e=S.toLocaleUpperCase(),t=S.toLocaleLowerCase();R=r(R,e,t),b=r(b,e,t),L=r(L,e,t),A=r(A,e,t),x=r(x,e,t)}let E=t?"u":"";const I='".+?"',z=RegExp(I,"gi"+E),C=RegExp(`(?:\\s+|^)-(?:${A}+|${I})`,"gi"+E);let{intraRules:y}=e;null==y&&(y=e=>{let t=i.intraSlice,l=0,n=0,r=0,s=0;if(/[^\d]/.test(e)){let i=e.length;i>4?(t=c,l=o,n=p,r=d,s=m):3>i||(r=Math.min(d,1),4==i&&(l=Math.min(o,1)))}return{intraSlice:t,intraIns:l,intraSub:n,intraTrn:r,intraDel:s}});let k=!!b,j=RegExp(b,"g"+E),$=RegExp(R,"g"+E),w=RegExp("^"+R+"|"+R+"$","g"+E),Z=RegExp(x,"gi"+E);const M=e=>{let t=[];e=(e=e.replace(z,(e=>(t.push(e),n)))).replace(w,"").toLocaleLowerCase(),k&&(e=e.replace(j,(e=>e[0]+" "+e[1])));let l=0;return e.split($).filter((e=>""!=e)).map((e=>e===n?t[l++]:e))},D=/[^\d]+|\d+/g,T=(t,n=0,r=!1)=>{let i=M(t);if(0==i.length)return[];let h,c=Array(i.length).fill("");if(i=i.map(((e,t)=>e.replace(Z,(e=>(c[t]=e,""))))),1==u)h=i.map(((e,t)=>{if('"'===e[0])return l(e.slice(1,-1));let n="";for(let l of e.matchAll(D)){let e=l[0],{intraSlice:r,intraIns:i,intraSub:a,intraTrn:g,intraDel:f}=y(e);if(i+a+g+f==0)n+=e+c[t];else{let[l,h]=r,u=e.slice(0,l),o=e.slice(h),p=e.slice(l,h);1==i&&1==u.length&&u!=p[0]&&(u+="(?!"+u+")");let d=p.length,m=[e];if(a)for(let e=0;d>e;e++)m.push(u+p.slice(0,e)+A+p.slice(e+1)+o);if(g)for(let e=0;d-1>e;e++)p[e]!=p[e+1]&&m.push(u+p.slice(0,e)+p[e+1]+p[e]+p.slice(e+2)+o);if(f)for(let e=0;d>e;e++)m.push(u+p.slice(0,e+1)+"?"+p.slice(e+1)+o);if(i){let e=s(A,1);for(let t=0;d>t;t++)m.push(u+p.slice(0,t)+e+p.slice(t)+o)}n+="(?:"+m.join("|")+")"+c[t]}}return n}));else{let e=s(A,o);2==n&&o>0&&(e=")("+e+")("),h=i.map(((t,n)=>'"'===t[0]?l(t.slice(1,-1)):t.split("").map(((e,t,l)=>(1==o&&0==t&&l.length>1&&e!=l[t+1]&&(e+="(?!"+e+")"),e))).join(e)+c[n]))}let p=2==g?a:"",d=2==f?a:"",m=d+s(e.interChars,e.interIns)+p;return n>0?r?h=p+"("+h.join(")"+d+"|"+p+"(")+")"+d:(h="("+h.join(")("+m+")(")+")",h="(.??"+p+")"+h+"("+d+".*)"):(h=h.join(m),h=p+h+d),[RegExp(h,"i"+E),i,c]},F=(e,t,l)=>{let[n]=T(t);if(null==n)return null;let r=[];if(null!=l)for(let t=0;l.length>t;t++){let i=l[t];n.test(e[i])&&r.push(i)}else for(let t=0;e.length>t;t++)n.test(e[t])&&r.push(t);return r};let O=!!L,v=RegExp(R,E),B=RegExp(L,E);const U=(t,l,n)=>{let[r,i,s]=T(n,1),[a]=T(n,2),h=i.length,u=t.length,c=Array(u).fill(0),o={idx:Array(u),start:c.slice(),chars:c.slice(),terms:c.slice(),interIns:c.slice(),intraIns:c.slice(),interLft2:c.slice(),interRgt2:c.slice(),interLft1:c.slice(),interRgt1:c.slice(),ranges:Array(u)},p=1==g||1==f,d=0;for(let n=0;t.length>n;n++){let u=l[t[n]],c=u.match(r),m=c.index+c[1].length,x=m,b=!1,R=0,L=0,A=0,S=0,I=0,z=0,C=0,y=0,k=[];for(let t=0,l=2;h>t;t++,l+=2){let n=c[l].toLocaleLowerCase(),r=i[t],a='"'==r[0]?r.slice(1,-1):r+s[t],o=a.length,d=n.length,j=n==a;if(!j&&c[l+1].length>=o){let e=c[l+1].toLocaleLowerCase().indexOf(a);e>-1&&(k.push(x,d,e,o),x+=N(c,l,e,o),n=a,d=o,j=!0,0==t&&(m=x))}if(p||j){let e=x-1,r=x+d,i=!1,s=!1;if(-1==e||v.test(u[e]))j&&R++,i=!0;else{if(2==g){b=!0;break}if(O&&B.test(u[e]+u[e+1]))j&&L++,i=!0;else if(1==g){let e=c[l+1],r=x+d;if(e.length>=o){let s,g=0,f=!1,h=RegExp(a,"ig"+E);for(;s=h.exec(e);){g=s.index;let e=r+g,t=e-1;if(-1==t||v.test(u[t])){R++,f=!0;break}if(B.test(u[t]+u[e])){L++,f=!0;break}}f&&(i=!0,k.push(x,d,g,o),x+=N(c,l,g,o),n=a,d=o,j=!0,0==t&&(m=x))}if(!i){b=!0;break}}}if(r==u.length||v.test(u[r]))j&&A++,s=!0;else{if(2==f){b=!0;break}if(O&&B.test(u[r-1]+u[r]))j&&S++,s=!0;else if(1==f){b=!0;break}}j&&(I+=o,i&&s&&z++)}if(d>o&&(y+=d-o),t>0&&(C+=c[l-1].length),!e.intraFilt(a,n,x)){b=!0;break}h-1>t&&(x+=d+c[l+1].length)}if(!b){o.idx[d]=t[n],o.interLft2[d]=R,o.interLft1[d]=L,o.interRgt2[d]=A,o.interRgt1[d]=S,o.chars[d]=I,o.terms[d]=z,o.interIns[d]=C,o.intraIns[d]=y,o.start[d]=m;let e=u.match(a),l=e.index+e[1].length,r=k.length,i=r>0?0:1/0,s=r-4;for(let t=2;e.length>t;)if(i>s||k[i]!=l)l+=e[t].length,t++;else{let n=k[i+1],r=k[i+2],s=k[i+3],a=t,g="";for(let t=0;n>t;a++)g+=e[a],t+=e[a].length;e.splice(t,a-t,g),l+=N(e,t,r,s),i+=4}l=e.index+e[1].length;let g=o.ranges[d]=[],f=l,h=l;for(let t=2;e.length>t;t++){let n=e[t].length;l+=n,t%2==0?h=l:n>0&&(g.push(f,h),f=h=l)}h>f&&g.push(f,h),d++}}if(t.length>d)for(let e in o)o[e]=o[e].slice(0,d);return o},N=(e,t,l,n)=>{let r=e[t]+e[t+1].slice(0,l);return e[t-1]+=r,e[t]=e[t+1].slice(l,l+n),e[t+1]=e[t+1].slice(l+n),r.length};return{search:(...t)=>((t,n,r,i=1e3,s)=>{r=r?!0===r?5:r:0;let a=null,g=null,f=[];n=n.replace(C,(e=>{let t=e.trim().slice(1);return'"'===t[0]&&(t=l(t.slice(1,-1))),f.push(t),""}));let u,c=M(n);if(f.length>0){if(u=RegExp(f.join("|"),"i"+E),0==c.length){let e=[];for(let l=0;t.length>l;l++)u.test(t[l])||e.push(l);return[e,null,null]}}else if(0==c.length)return[null,null,null];if(r>0){let e=M(n);if(e.length>1){let l=e.slice().sort(((e,t)=>t.length-e.length));for(let e=0;l.length>e;e++){if(0==s?.length)return[[],null,null];s=F(t,l[e],s)}if(e.length>r)return[s,null,null];a=h(e).map((e=>e.join(" "))),g=[];let n=new Set;for(let e=0;a.length>e;e++)if(s.length>n.size){let l=s.filter((e=>!n.has(e))),r=F(t,a[e],l);for(let e=0;r.length>e;e++)n.add(r[e]);g.push(r)}else g.push([])}}null==a&&(a=[n],g=[s?.length>0?s:F(t,n)]);let o=null,p=null;if(f.length>0&&(g=g.map((e=>e.filter((e=>!u.test(t[e])))))),i>=g.reduce(((e,t)=>e+t.length),0)){o={},p=[];for(let l=0;g.length>l;l++){let n=g[l];if(null==n||0==n.length)continue;let r=a[l],i=U(n,t,r),s=e.sort(i,t,r);if(l>0)for(let e=0;s.length>e;e++)s[e]+=p.length;for(let e in i)o[e]=(o[e]??[]).concat(i[e]);p=p.concat(s)}}return[[].concat(...g),o,p]})(...t),split:M,filter:F,info:U,sort:e.sort}}const f=(()=>{let e={A:"ÁÀÃÂÄĄ",a:"áàãâäą",E:"ÉÈÊËĖ",e:"éèêëę",I:"ÍÌÎÏĮ",i:"íìîïį",O:"ÓÒÔÕÖ",o:"óòôõö",U:"ÚÙÛÜŪŲ",u:"úùûüūų",C:"ÇČĆ",c:"çčć",L:"Ł",l:"ł",N:"ÑŃ",n:"ñń",S:"ŠŚ",s:"šś",Z:"ŻŹ",z:"żź"},t=new Map,l="";for(let n in e)e[n].split("").forEach((e=>{l+=e,t.set(e,n)}));let n=RegExp(`[${l}]`,"g"),r=e=>t.get(e);return e=>{if("string"==typeof e)return e.replace(n,r);let t=Array(e.length);for(let l=0;e.length>l;l++)t[l]=e[l].replace(n,r);return t}})();function h(e){let t,l,n=(e=e.slice()).length,r=[e.slice()],i=Array(n).fill(0),s=1;for(;n>s;)s>i[s]?(t=s%2&&i[s],l=e[s],e[s]=e[t],e[t]=l,++i[s],s=1,r.push(e.slice())):(i[s]=0,++s);return r}const u=(e,t)=>t?`${e}`:e,c=(e,t)=>e+t;return g.latinize=f,g.permute=e=>h([...Array(e.length).keys()]).sort(((e,t)=>{for(let l=0;e.length>l;l++)if(e[l]!=t[l])return e[l]-t[l];return 0})).map((t=>t.map((t=>e[t])))),g.highlight=function(e,t,l=u,n="",r=c){n=r(n,l(e.substring(0,t[0]),!1))??n;for(let i=0;t.length>i;i+=2)n=r(n,l(e.substring(t[i],t[i+1]),!0))??n,t.length-3>i&&(n=r(n,l(e.substring(t[i+1],t[i+2]),!1))??n);return r(n,l(e.substring(t[t.length-1]),!1))??n},g}(); diff --git a/src/assets/xmark-circle.svg b/src/assets/xmark-circle.svg new file mode 100644 index 00000000..e108b5d8 --- /dev/null +++ b/src/assets/xmark-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/xmark.svg b/src/assets/xmark.svg new file mode 100644 index 00000000..38549308 --- /dev/null +++ b/src/assets/xmark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/app.ts b/src/main/app.ts index 6612414c..20927e07 100644 --- a/src/main/app.ts +++ b/src/main/app.ts @@ -17,10 +17,12 @@ import * as semver from 'semver'; import * as fs from 'fs'; import { clearSession, + EnvironmentInstallStatus, getBundledPythonEnvPath, getBundledPythonPath, installBundledEnvironment, isDarkTheme, + pythonPathForEnvPath, waitForDuration } from './utils'; import { IServerFactory, JupyterServerFactory } from './server'; @@ -49,14 +51,29 @@ import { EventTypeMain, EventTypeRenderer } from './eventtypes'; import { SettingsDialog } from './settingsdialog/settingsdialog'; import { AboutDialog } from './aboutdialog/aboutdialog'; import { AuthDialog } from './authdialog/authdialog'; +import { ManagePythonEnvironmentDialog } from './pythonenvdialog/pythonenvdialog'; +import { addUserSetEnvironment, createPythonEnvironment } from './cli'; +import { + getNextPythonEnvName, + JUPYTER_ENV_REQUIREMENTS, + validateCondaChannels, + validateCondaPath, + validateNewPythonEnvironmentName, + validatePythonEnvironmentInstallDirectory, + validateSystemPythonPath +} from './env'; export interface IApplication { createNewEmptySession(): void; createFreeServersIfNeeded(): void; checkForUpdates(showDialog: 'on-new-version' | 'always'): void; showSettingsDialog(activateTab?: SettingsDialog.Tab): void; + showManagePythonEnvsDialog( + activateTab?: ManagePythonEnvironmentDialog.Tab + ): void; showAboutDialog(): void; cliArgs: ICLIArguments; + registry: IRegistry; } interface IClearHistoryOptions { @@ -185,6 +202,7 @@ class SessionWindowManager implements IDisposable { if (this._windows.length === 0) { this._options.app.closeSettingsDialog(); + this._options.app.closeManagePythonEnvDialog(); this._options.app.closeAboutDialog(); } } @@ -377,7 +395,6 @@ export class JupyterApplication implements IApplication, IDisposable { defaultWorkingDirectory: userSettings.getValue( SettingType.defaultWorkingDirectory ), - defaultPythonPath: userSettings.getValue(SettingType.pythonPath), logLevel: userSettings.getValue(SettingType.logLevel), activateTab: activateTab, serverArgs: userSettings.getValue(SettingType.serverArgs), @@ -399,6 +416,31 @@ export class JupyterApplication implements IApplication, IDisposable { dialog.load(); } + async showManagePythonEnvsDialog( + activateTab?: ManagePythonEnvironmentDialog.Tab + ) { + if (this._managePythonEnvDialog) { + this._managePythonEnvDialog.window.focus(); + return; + } + + const dialog = new ManagePythonEnvironmentDialog({ + envs: await this._registry.getEnvironmentList(false), + isDarkTheme: this._isDarkTheme, + defaultPythonPath: userSettings.getValue(SettingType.pythonPath), + app: this, + activateTab + }); + + this._managePythonEnvDialog = dialog; + + dialog.window.on('closed', () => { + this._managePythonEnvDialog = null; + }); + + dialog.load(); + } + closeSettingsDialog() { if (this._settingsDialog) { this._settingsDialog.window.close(); @@ -406,6 +448,13 @@ export class JupyterApplication implements IApplication, IDisposable { } } + closeManagePythonEnvDialog() { + if (this._managePythonEnvDialog) { + this._managePythonEnvDialog.window.close(); + this._managePythonEnvDialog = null; + } + } + showAboutDialog() { if (this._aboutDialog) { this._aboutDialog.window.window.focus(); @@ -430,12 +479,21 @@ export class JupyterApplication implements IApplication, IDisposable { } } + get registry(): IRegistry { + return this._registry; + } + + get serverFactory(): IServerFactory { + return this._serverFactory; + } + dispose(): Promise { if (this._disposePromise) { return this._disposePromise; } this.closeSettingsDialog(); + this.closeManagePythonEnvDialog(); this.closeAboutDialog(); this._disposePromise = new Promise((resolve, reject) => { @@ -665,15 +723,27 @@ export class JupyterApplication implements IApplication, IDisposable { this._evm.registerEventHandler( EventTypeMain.InstallBundledPythonEnv, - async event => { - const installPath = getBundledPythonEnvPath(); + async (event, envPath: string) => { + // for security, make sure event is sent from the dialog when path is specified + if ( + envPath && + event.sender !== this._managePythonEnvDialog?.window?.webContents + ) { + return; + } + const installPath = envPath || getBundledPythonEnvPath(); await installBundledEnvironment(installPath, { onInstallStatus: (status, message) => { event.sender.send( - EventTypeRenderer.InstallBundledPythonEnvStatus, + EventTypeRenderer.InstallPythonEnvStatus, status, message ); + if (status === EnvironmentInstallStatus.Success) { + addUserSetEnvironment(installPath, true); + const pythonPath = pythonPathForEnvPath(installPath, true); + this._registry.addEnvironment(pythonPath); + } }, get forceOverwrite() { return false; @@ -703,6 +773,97 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerEventHandler( + EventTypeMain.ShowManagePythonEnvironmentsDialog, + async (event, activateTab) => { + this.showManagePythonEnvsDialog(activateTab); + } + ); + + this._evm.registerEventHandler( + EventTypeMain.SetPythonEnvironmentInstallDirectory, + async (event, dirPath) => { + userSettings.setValue(SettingType.pythonEnvsPath, dirPath); + userSettings.save(); + } + ); + + this._evm.registerEventHandler( + EventTypeMain.SetCondaPath, + async (event, condaPath) => { + userSettings.setValue(SettingType.condaPath, condaPath); + userSettings.save(); + } + ); + + this._evm.registerEventHandler( + EventTypeMain.SetCondaChannels, + async (event, condaChannels) => { + const channelList = + condaChannels.trim() === '' ? [] : condaChannels.split(' '); + userSettings.setValue(SettingType.condaChannels, channelList); + userSettings.save(); + } + ); + + this._evm.registerEventHandler( + EventTypeMain.SetSystemPythonPath, + async (event, pythonPath) => { + userSettings.setValue(SettingType.systemPythonPath, pythonPath); + userSettings.save(); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.GetNextPythonEnvironmentName, + (event, path) => { + return getNextPythonEnvName(); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.SelectDirectoryPath, + (event, currentPath) => { + return new Promise((resolve, reject) => { + dialog + .showOpenDialog({ + properties: [ + 'openDirectory', + 'showHiddenFiles', + 'noResolveAliases', + 'createDirectory' + ], + buttonLabel: 'Use path', + defaultPath: currentPath + }) + .then(({ filePaths }) => { + if (filePaths.length > 0) { + resolve(filePaths[0]); + } + }); + }); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.SelectFilePath, + (event, currentPath) => { + return new Promise((resolve, reject) => { + dialog + .showOpenDialog({ + properties: ['openFile', 'showHiddenFiles', 'noResolveAliases'], + buttonLabel: 'Use path', + defaultPath: currentPath + }) + .then(({ filePaths }) => { + if (filePaths.length > 0) { + resolve(filePaths[0]); + } + }); + }); + } + ); + this._evm.registerSyncEventHandler( EventTypeMain.ValidatePythonPath, (event, path) => { @@ -710,6 +871,27 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerSyncEventHandler( + EventTypeMain.GetEnvironmentByPythonPath, + (event, pythonPath) => { + return this._registry.getEnvironmentByPath(pythonPath); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.AddEnvironmentByPythonPath, + (event, pythonPath) => { + return this._registry.addEnvironment(pythonPath); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.GetPythonEnvironmentList, + (event, cacheOK) => { + return this._registry.getEnvironmentList(cacheOK); + } + ); + this._evm.registerSyncEventHandler( EventTypeMain.ValidateRemoteServerUrl, (event, url) => { @@ -725,10 +907,47 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerSyncEventHandler( + EventTypeMain.ValidateNewPythonEnvironmentName, + (event, name) => { + return Promise.resolve(validateNewPythonEnvironmentName(name)); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.ValidatePythonEnvironmentInstallDirectory, + (event, dirPath) => { + return Promise.resolve( + validatePythonEnvironmentInstallDirectory(dirPath) + ); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.ValidateCondaPath, + (event, condaPath) => { + return validateCondaPath(condaPath); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.ValidateCondaChannels, + (event, condaChannels) => { + return Promise.resolve(validateCondaChannels(condaChannels)); + } + ); + + this._evm.registerSyncEventHandler( + EventTypeMain.ValidateSystemPythonPath, + (event, pythonPath) => { + return validateSystemPythonPath(pythonPath); + } + ); + this._evm.registerEventHandler( EventTypeMain.ShowInvalidPythonPathMessage, (event, path) => { - const requirements = this._registry.getRequirements(); + const requirements = JUPYTER_ENV_REQUIREMENTS; const reqVersions = requirements.map( req => `${req.name} ${req.versionRange.format()}` ); @@ -742,6 +961,8 @@ export class JupyterApplication implements IApplication, IDisposable { EventTypeMain.SetDefaultPythonPath, (event, path) => { userSettings.setValue(SettingType.pythonPath, path); + userSettings.save(); + this._registry.setDefaultPythonPath(path); } ); @@ -811,9 +1032,68 @@ export class JupyterApplication implements IApplication, IDisposable { } ); + this._evm.registerEventHandler( + EventTypeMain.CreateNewPythonEnvironment, + async (event, envPath: string, envType: string, packages: string) => { + // for security, make sure event is sent from the dialog + if (event.sender !== this._managePythonEnvDialog?.window?.webContents) { + return; + } + + // still check input to prevent chaining malicious commands + const invalidCharInputRegex = new RegExp('[&;|]'); + const invalidInputMessage = invalidCharInputRegex.test(envPath) + ? 'Invalid environment name input' + : invalidCharInputRegex.test(packages) + ? 'Invalid package list input' + : ''; + + if (invalidInputMessage) { + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Failure, + invalidInputMessage + ); + return; + } + + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Started + ); + try { + await createPythonEnvironment({ + envPath, + envType, + packageList: packages.split(' '), + callbacks: { + stdout: (msg: string) => { + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Running, + msg + ); + } + } + }); + const pythonPath = pythonPathForEnvPath(envPath); + this._registry.addEnvironment(pythonPath); + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Success + ); + } catch (error) { + event.sender.send( + EventTypeRenderer.InstallPythonEnvStatus, + EnvironmentInstallStatus.Failure + ); + } + } + ); + this._evm.registerSyncEventHandler( EventTypeMain.GetServerInfo, - (event): Promise => { + (event): IServerInfo => { for (const sessionWindow of this._sessionWindowManager.windows) { if ( event.sender === sessionWindow.titleBarView?.view?.webContents || @@ -936,6 +1216,7 @@ export class JupyterApplication implements IApplication, IDisposable { private _sessionWindowManager: SessionWindowManager; private _evm = new EventManager(); private _settingsDialog: SettingsDialog; + private _managePythonEnvDialog: ManagePythonEnvironmentDialog; private _aboutDialog: AboutDialog; private _isDarkTheme: boolean; } diff --git a/src/main/cli.ts b/src/main/cli.ts index 6b04b259..01e051dd 100644 --- a/src/main/cli.ts +++ b/src/main/cli.ts @@ -4,10 +4,12 @@ import { createTempFile, EnvironmentInstallStatus, envPathForPythonPath, + getBundledEnvInstallerPath, getBundledPythonEnvPath, getBundledPythonPath, - installBundledEnvironment, + installCondaPackEnvironment, isBaseCondaEnv, + isEnvInstalledByDesktopApp, markEnvironmentAsJupyterInstalled, pythonPathForEnvPath } from './utils'; @@ -18,6 +20,18 @@ import { appData } from './config/appdata'; import { IEnvironmentType, IPythonEnvironment } from './tokens'; import { SettingType, userSettings } from './config/settings'; import { Registry } from './registry'; +import { app } from 'electron'; +import { + condaEnvPathForCondaExePath, + getCondaChannels, + getCondaPath, + getPythonEnvsDirectory, + getSystemPythonPath, + ICommandRunCallbacks, + runCommandInEnvironment, + validateCondaPath, + validateSystemPythonPath +} from './env'; export function parseCLIArgs(argv: string[]) { return yargs(argv) @@ -35,15 +49,15 @@ export function parseCLIArgs(argv: string[]) { 'Launch in /data/nb and open /data/nb/test.ipynb and /data/nb/sub/test2.ipynb' ) .example( - 'jlab env install', + 'jlab env create', 'Install bundled Python environment to the default path' ) .example( - 'jlab env install --path /opt/jlab_server', + 'jlab env create --source bundle --prefix /opt/jlab_server', 'Install bundled Python environment to /opt/jlab_server' ) .example( - 'jlab env create --path /opt/jlab_server', + 'jlab env create --prefix /opt/jlab_server', 'Create new Python environment at /opt/jlab_server' ) .example( @@ -70,10 +84,13 @@ export function parseCLIArgs(argv: string[]) { }) .help('h') .alias({ - h: 'help' + h: 'help', + n: 'name', + c: 'channel', + p: 'prefix' }) .command( - 'env [path]', + 'env ', 'Manage Python environments', yargs => { yargs @@ -82,20 +99,47 @@ export function parseCLIArgs(argv: string[]) { type: 'string', default: '' }) - .positional('path', { + .option('name', { + describe: 'Environment name', + type: 'string', + default: '' + }) + .option('prefix', { + describe: 'Environment location', type: 'string', - default: '', - describe: 'Destination path' + default: '' + }) + .option('source', { + describe: 'Environment / package source', + type: 'string', + default: '' + }) + .option('source-type', { + describe: 'Environment / package source type', + choices: [ + 'registry', + 'bundle', + 'conda-pack', + 'conda-lock-file', + 'conda-env-file' + ], + default: 'registry' + }) + .option('channel', { + describe: 'conda package channels', + type: 'array', + default: [] }) .option('force', { describe: 'Force the action', type: 'boolean', default: false }) - .option('exclude-jlab', { - describe: 'Exclude jupyterlab Python package in env create', + .option('add-jupyterlab-package', { + describe: + 'Auto-add jupyterlab Python package to newly created environments', type: 'boolean', - default: false + default: true }) .option('env-type', { describe: 'Python environment type', @@ -115,7 +159,7 @@ export function parseCLIArgs(argv: string[]) { await handleEnvListCommand(argv); break; case 'install': - await handleEnvInstallCommand(argv); + console.log('Not implemented yet!'); break; case 'activate': await handleEnvActivateCommand(argv); @@ -123,8 +167,14 @@ export function parseCLIArgs(argv: string[]) { case 'create': await handleEnvCreateCommand(argv); break; - case 'set-base-conda-env-path': - await handleEnvSetBaseCondaCommand(argv); + case 'set-conda-path': + await handleEnvSetCondaPathCommand(argv); + break; + case 'set-conda-channels': + await handleEnvSetCondaChannelsCommand(argv); + break; + case 'set-system-python-path': + await handleEnvSetSystemPythonPathCommand(argv); break; case 'update-registry': await handleEnvUpdateRegistryCommand(argv); @@ -139,48 +189,63 @@ export function parseCLIArgs(argv: string[]) { } export async function handleEnvInfoCommand(argv: any) { - const bundledEnvPath = getBundledPythonEnvPath(); - const bundledEnvPathExists = - fs.existsSync(bundledEnvPath) && fs.statSync(bundledEnvPath).isDirectory(); + const bundledPythonPath = getBundledPythonPath(); + const bundledPythonPathExists = + fs.existsSync(bundledPythonPath) && + (fs.statSync(bundledPythonPath).isFile() || + fs.statSync(bundledPythonPath).isSymbolicLink()); + // TODO: move this logic to getJupyterLabPythonPath or similar let defaultPythonPath = userSettings.getValue(SettingType.pythonPath); - if (defaultPythonPath === '') { + if (!defaultPythonPath) { defaultPythonPath = getBundledPythonPath(); } - const defaultEnvPath = envPathForPythonPath(defaultPythonPath); - const defaultEnvPathExists = - fs.existsSync(defaultEnvPath) && fs.statSync(defaultEnvPath).isDirectory(); - const condaRootPath = appData.condaRootPath; - const condaRootPathExists = - condaRootPath && - fs.existsSync(condaRootPath) && - fs.statSync(condaRootPath).isDirectory(); - const systemPythonPath = appData.systemPythonPath; + if (!fs.existsSync(defaultPythonPath)) { + defaultPythonPath = appData.pythonPath; + } + const defaultPythonPathExists = + fs.existsSync(defaultPythonPath) && + (fs.statSync(defaultPythonPath).isFile() || + fs.statSync(defaultPythonPath).isSymbolicLink()); + const condaPath = getCondaPath(); + const condaChannels = userSettings.getValue(SettingType.condaChannels); + const condaPathExists = + condaPath && fs.existsSync(condaPath) && fs.statSync(condaPath).isFile(); + const systemPythonPath = getSystemPythonPath(); const systemPythonPathExists = systemPythonPath && fs.existsSync(systemPythonPath) && fs.statSync(systemPythonPath).isFile(); + const envsDir = getPythonEnvsDirectory(); + const envsDirExists = + envsDir && fs.existsSync(envsDir) && fs.statSync(envsDir).isDirectory(); const infoLines: string[] = []; infoLines.push( - `Default Python environment path:\n "${defaultEnvPath}" [${ - defaultEnvPathExists ? 'exists' : 'not found' + `Default Python path for JupyterLab Server:\n "${defaultPythonPath}" [${ + defaultPythonPathExists ? 'exists' : 'not found' }]` ); infoLines.push( - `Bundled Python environment installation path:\n "${bundledEnvPath}" [${ - bundledEnvPathExists ? 'exists' : 'not found' + `Bundled Python installation path:\n "${bundledPythonPath}" [${ + bundledPythonPathExists ? 'exists' : 'not found' }]` ); infoLines.push( - `Base conda environment path:\n "${condaRootPath}" [${ - condaRootPathExists ? 'exists' : 'not found' + `conda path:\n "${condaPath}" [${ + condaPathExists ? 'exists' : 'not found' }]` ); + infoLines.push(`conda channels:\n "${condaChannels.join(' ')}"`); infoLines.push( `System Python path:\n "${systemPythonPath}" [${ systemPythonPathExists ? 'exists' : 'not found' }]` ); + infoLines.push( + `Python environment install directory:\n "${envsDir}" [${ + envsDirExists ? 'exists' : 'not found' + }]` + ); console.log(infoLines.join('\n')); } @@ -194,11 +259,9 @@ export async function handleEnvListCommand(argv: any) { name => `${name}: ${env.versions[name]}` ); const envPath = envPathForPythonPath(env.path); - const installedByApp = fs.existsSync( - path.join(envPath, '.jupyter', 'env.json') - ); + const installedByApp = isEnvInstalledByDesktopApp(envPath); listLines.push( - ` [${env.name}], path: ${envPath}${ + ` [${env.name}], Python path: ${env.path}${ installedByApp ? ', installed by JupyterLab Desktop' : '' }\n packages: ${versions.join(', ')}` ); @@ -222,7 +285,7 @@ export async function handleEnvListCommand(argv: any) { console.log(listLines.join('\n')); } -function addUserSetEnvironment(envPath: string, isConda: boolean) { +export function addUserSetEnvironment(envPath: string, isConda: boolean) { const pythonPath = pythonPathForEnvPath(envPath, isConda); // this record will get updated with the correct data once app launches @@ -256,11 +319,14 @@ function addUserSetEnvironment(envPath: string, isConda: boolean) { } } -export async function handleEnvInstallCommand(argv: any) { - const installPath = (argv.path as string) || getBundledPythonEnvPath(); +export async function handleInstallCondaPackEnvironment( + condaPackPath: string, + installPath: string, + forceOverwrite: boolean +) { console.log(`Installing to "${installPath}"`); - await installBundledEnvironment(installPath, { + await installCondaPackEnvironment(condaPackPath, installPath, { onInstallStatus: (status, message) => { switch (status) { case EnvironmentInstallStatus.RemovingExistingInstallation: @@ -278,7 +344,7 @@ export async function handleEnvInstallCommand(argv: any) { console.error(`Failed to install.`, message); break; case EnvironmentInstallStatus.Success: - if (argv.path) { + if (installPath !== getBundledPythonEnvPath()) { addUserSetEnvironment(installPath, true); } console.log('Installation succeeded.'); @@ -286,15 +352,55 @@ export async function handleEnvInstallCommand(argv: any) { } }, get forceOverwrite() { - return argv.force; + return forceOverwrite; } }).catch(reason => { // }); } +async function installAdditionalCondaPackagesToEnv( + envPath: string, + packageList: string[], + channelList?: string[], + callbacks?: ICommandRunCallbacks +) { + const baseCondaPath = getCondaPath(); + const baseCondaEnvPath = condaEnvPathForCondaExePath(baseCondaPath); + const condaBaseEnvExists = isBaseCondaEnv(baseCondaEnvPath); + + if (!condaBaseEnvExists) { + throw new Error(`Base conda path not found "${baseCondaEnvPath}".`); + } + + const packages = packageList.join(); + const condaChannels = + channelList?.length > 0 ? channelList : getCondaChannels(); + const channels = condaChannels.map(channel => `-c ${channel}`).join(' '); + // TODO: remove classic solver. since installing additional packages onto conda-lock + // generated environments fails with mamba solver, classic is used here. + const installCommand = `conda install -y ${channels} --solver=classic -p ${envPath} ${packages}`; + console.log(`Installing additional packages: "${packages}"`); + await runCommandInEnvironment(baseCondaEnvPath, installCommand, callbacks); +} + export async function handleEnvActivateCommand(argv: any) { - const envPath = (argv.path as string) || getBundledPythonEnvPath(); + let envPath: string; + if (argv.name) { + envPath = path.join(getPythonEnvsDirectory(), argv.name); + } else if (argv.prefix) { + envPath = path.resolve(argv.prefix); + } else { + envPath = getBundledPythonEnvPath(); + } + + if ( + !(envPath && fs.existsSync(envPath) && fs.statSync(envPath).isDirectory()) + ) { + console.error(`Invalid environment directory "${envPath}"`); + return; + } + console.log(`Activating Python environment "${envPath}"`); await launchCLIinEnvironment(envPath); @@ -307,8 +413,158 @@ export async function handleEnvUpdateRegistryCommand(argv: any) { appData.save(); } +export interface ICreatePythonEnvironmentOptions { + envPath: string; + envType: string; + sourceFilePath?: string; + sourceType?: + | 'registry' + | 'bundle' + | 'conda-pack' + | 'conda-lock-file' + | 'conda-env-file'; + packageList?: string[]; + condaChannels?: string[]; + callbacks?: ICommandRunCallbacks; +} + +export async function createPythonEnvironment( + options: ICreatePythonEnvironmentOptions +) { + const { + envPath, + envType, + packageList, + callbacks, + sourceFilePath, + sourceType + } = options; + const isConda = envType === 'conda'; + const baseCondaPath = getCondaPath(); + const baseCondaEnvPath = condaEnvPathForCondaExePath(baseCondaPath); + const condaBaseEnvExists = isBaseCondaEnv(baseCondaEnvPath); + const packages = packageList ? packageList.join(' ') : ''; + + if (isConda) { + if (!condaBaseEnvExists) { + throw new Error( + 'Failed to create Python environment. Base conda environment not found.' + ); + } + + const condaChannels = + options.condaChannels?.length > 0 + ? options.condaChannels + : getCondaChannels(); + const channels = condaChannels.map(channel => `-c ${channel}`).join(' '); + if (sourceType === 'conda-lock-file') { + const createCommand = `conda-lock install -p ${envPath} ${sourceFilePath}`; + if ( + !(await runCommandInEnvironment( + baseCondaEnvPath, + createCommand, + callbacks + )) + ) { + throw new Error( + `Failed to create environment from pack. Make sure "conda-lock" Python package is installed in then base environment "${baseCondaEnvPath}".` + ); + } + + if (packages) { + // TODO: remove classic solver. since installing additional packages onto conda-lock + // generated environments fails with mamba solver, classic is used here. + const installCommand = `conda install -y ${channels} --solver=classic -p ${envPath} ${packages}`; + console.log(`Installing additional packages: "${packages}"`); + await runCommandInEnvironment( + baseCondaEnvPath, + installCommand, + callbacks + ); + } + } else if (sourceType === 'conda-env-file') { + const createCommand = `conda env create -p ${envPath} -f ${sourceFilePath} -y`; + await runCommandInEnvironment(baseCondaEnvPath, createCommand, callbacks); + + if (packages) { + const installCommand = `conda install -y ${channels} -p ${envPath} ${packages}`; + console.log(`Installing additional packages: "${packages}"`); + await runCommandInEnvironment( + baseCondaEnvPath, + installCommand, + callbacks + ); + } + } else { + const createCommand = `conda create -p ${envPath} ${packages} ${channels} -y`; + await runCommandInEnvironment(baseCondaEnvPath, createCommand, callbacks); + } + } else { + const systemPythonPath = getSystemPythonPath(); + if (condaBaseEnvExists) { + const createCommand = `python -m venv create ${envPath}`; + await runCommandInEnvironment(baseCondaEnvPath, createCommand, callbacks); + } else if (fs.existsSync(systemPythonPath)) { + execFileSync(systemPythonPath, ['-m', 'venv', 'create', envPath]); + } else { + throw new Error( + 'Failed to create Python environment. Python executable not found.' + ); + } + + if (packages) { + const installCommand = `python -m pip install ${packages}`; + console.log('Installing packages...'); + await runCommandInEnvironment(envPath, installCommand, callbacks); + } + } + + markEnvironmentAsJupyterInstalled(envPath, { + type: isConda ? 'conda' : 'venv', + source: 'registry', + appVersion: app.getVersion() + }); + + if (packages.includes('jupyterlab')) { + addUserSetEnvironment(envPath, isConda); + } +} + +function isURL(urlString: string) { + try { + const url = new URL(urlString); + return url && (url.protocol === 'https:' || url.protocol === 'http:'); + } catch (error) { + return false; + } +} + +async function downloadToTempFile( + fetchURL: string, + fileName: string +): Promise { + console.log(`Downloading "${fetchURL}"...`); + const downloadPath = createTempFile(fileName, '', null); + const response = await fetch(fetchURL); + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + fs.writeFileSync(downloadPath, buffer); + console.log(`Finished downloading and saved to temp file "${downloadPath}"`); + + return downloadPath; +} + export async function handleEnvCreateCommand(argv: any) { - const envPath = argv.path as string; + let envPath: string; + let installingToBundledEnvPath = false; + if (argv.name) { + envPath = path.join(getPythonEnvsDirectory(), argv.name); + } else if (argv.prefix) { + envPath = path.resolve(argv.prefix); + } else { + envPath = getBundledPythonEnvPath(); + installingToBundledEnvPath = true; + } if (!envPath) { console.error('Environment path not set.'); @@ -326,75 +582,184 @@ export async function handleEnvCreateCommand(argv: any) { } } else { console.error( - 'Environment path not empty. Use --force flag to overwrite.' + `Environment path ("${envPath}") not empty. Use --force flag to overwrite.` ); return; } } - const excludeJlab = argv.excludeJlab === true; - const envType = argv.envType; - const isConda = envType === 'conda'; - const condaRootExists = isBaseCondaEnv(appData.condaRootPath); + // if no name or prefix path specified (jlab env create), use bundled installer + let source = installingToBundledEnvPath ? 'bundle' : argv.source; + + const { sourceType } = argv; + const isCondaPackSource = source === 'bundle' || sourceType === 'conda-pack'; - const packageList = argv._.slice(1); - if (!excludeJlab) { + const addJupyterlabPackage = argv.addJupyterlabPackage === true; + + const packageList: string[] = argv._.slice(1); + // add jupyterlab package unless source is conda pack + if (!isCondaPackSource && addJupyterlabPackage) { packageList.push('jupyterlab'); } console.log(`Creating Python environment at "${envPath}"...`); - if (isConda && !condaRootExists) { + let sourceIsTempFile = false; + let sourceFilePath = ''; + + if (isCondaPackSource) { + if (source === 'bundle') { + sourceFilePath = getBundledEnvInstallerPath(); + } else if (sourceType === 'conda-pack') { + if (isURL(source)) { + try { + sourceFilePath = await downloadToTempFile(source, 'pack.tar.gz'); + sourceIsTempFile = true; + } catch (error) { + console.error(error); + } + } else { + source = path.resolve(source); + if (fs.existsSync(source) && fs.statSync(source).isFile()) { + sourceFilePath = source; + } else { + console.error(`Source not found at "${source}".`); + } + } + } + + if (sourceFilePath) { + await handleInstallCondaPackEnvironment( + sourceFilePath, + envPath, + argv.force + ); + if (sourceIsTempFile) { + fs.unlinkSync(sourceFilePath); + } + + if (packageList.length > 0) { + await installAdditionalCondaPackagesToEnv( + envPath, + packageList, + argv.channel + ); + } + } + + return; + } + + if (sourceType === 'conda-lock-file' || sourceType === 'conda-env-file') { + if (isURL(source)) { + try { + sourceFilePath = await downloadToTempFile( + source, + sourceType === 'conda-lock-file' ? 'env.lock' : 'env.yml' + ); + sourceIsTempFile = true; + } catch (error) { + console.error(error); + } + } else { + source = path.resolve(source); + if (fs.existsSync(source) && fs.statSync(source).isFile()) { + sourceFilePath = source; + } + } + + if (!sourceFilePath) { + console.error(`Invalid env source "${source}".`); + return; + } + } + + const envType = argv.envType; + const isConda = + envType === 'conda' || + sourceType === 'conda-pack' || + sourceType === 'conda-lock-file' || + sourceType === 'conda-env-file'; + const baseCondaPath = getCondaPath(); + const condaEnvPath = baseCondaPath + ? condaEnvPathForCondaExePath(baseCondaPath) + : ''; + const condaBaseEnvExists = condaEnvPath + ? isBaseCondaEnv(condaEnvPath) + : false; + + if (isConda && !condaBaseEnvExists) { console.error( 'conda base environment not found. You can set using jlab --set-base-conda-env-path command.' ); return; } - const createCondaEnv = isConda || (envType === 'auto' && condaRootExists); + const createCondaEnv = isConda || (envType === 'auto' && condaBaseEnvExists); - if (createCondaEnv) { - const createCommand = `conda create -y -c conda-forge -p ${envPath} ${packageList.join( - ' ' - )}`; - await runCommandInEnvironment(appData.condaRootPath, createCommand); - } else { - if (condaRootExists) { - const createCommand = `python -m venv create ${envPath}`; - await runCommandInEnvironment(appData.condaRootPath, createCommand); - } else if (fs.existsSync(appData.systemPythonPath)) { - execFileSync(appData.systemPythonPath, ['-m', 'venv', 'create', envPath]); - } else { - console.error( - 'Failed to create Python environment. Python executable not found.' - ); - return; - } + try { + await createPythonEnvironment({ + envPath, + envType: createCondaEnv ? 'conda' : 'venv', + sourceFilePath: sourceFilePath, + sourceType: sourceType, + packageList, + condaChannels: argv.channel + }); + } catch (error) { + console.error(error); + } - if (packageList.length > 0) { - const installCommand = `python -m pip install ${packageList.join(' ')}`; - console.log('Installing packages...'); - await runCommandInEnvironment(envPath, installCommand); - } + if (sourceIsTempFile) { + fs.unlinkSync(sourceFilePath); + } +} + +export async function handleEnvSetCondaPathCommand(argv: any) { + const condaPath = argv._.length === 2 ? argv._[1] : undefined; + if (!condaPath) { + console.error('Please set a valid conda path'); + return; + } + + if (!fs.existsSync(condaPath)) { + console.error(`conda path "${condaPath}" does not exist`); + return; + } else if (!(await validateCondaPath(condaPath)).valid) { + console.error(`"${condaPath}" is not a valid conda path`); + return; } - markEnvironmentAsJupyterInstalled(envPath); - addUserSetEnvironment(envPath, createCondaEnv); + console.log(`Setting "${condaPath}" as the conda path`); + userSettings.setValue(SettingType.condaPath, condaPath); + userSettings.save(); +} + +export async function handleEnvSetCondaChannelsCommand(argv: any) { + const channelList = argv._.slice(1); + console.log(`Setting conda channels to "${channelList.join(' ')}"`); + userSettings.setValue(SettingType.condaChannels, channelList); + userSettings.save(); } -export async function handleEnvSetBaseCondaCommand(argv: any) { - const envPath = argv.path as string; - if (!fs.existsSync(envPath)) { - console.error(`Environment path "${envPath}" does not exist`); +export async function handleEnvSetSystemPythonPathCommand(argv: any) { + const systemPythonPath = argv._.length === 2 ? argv._[1] : undefined; + if (!systemPythonPath) { + console.error('Please set a valid Python path'); + return; + } + + if (!fs.existsSync(systemPythonPath)) { + console.error(`Python path "${systemPythonPath}" does not exist`); return; - } else if (!isBaseCondaEnv(envPath)) { - console.error(`"${envPath}" is not a base conda environemnt`); + } else if (!(await validateSystemPythonPath(systemPythonPath)).valid) { + console.error(`"${systemPythonPath}" is not a valid Python path`); return; } - console.log(`Setting "${envPath}" as the base conda environment`); - appData.condaRootPath = envPath; - appData.save(); + console.log(`Setting "${systemPythonPath}" as the system Python path`); + userSettings.setValue(SettingType.systemPythonPath, systemPythonPath); + userSettings.save(); } export async function launchCLIinEnvironment( @@ -404,10 +769,9 @@ export async function launchCLIinEnvironment( const isWin = process.platform === 'win32'; envPath = envPath || getBundledPythonEnvPath(); - const activateCommand = createCommandScriptInEnv( - envPath, - appData.condaRootPath - ); + const baseCondaPath = getCondaPath(); + const baseCondaEnvPath = condaEnvPathForCondaExePath(baseCondaPath); + const activateCommand = createCommandScriptInEnv(envPath, baseCondaEnvPath); const ext = isWin ? 'bat' : 'sh'; const activateFilePath = createTempFile(`activate.${ext}`, activateCommand); @@ -441,51 +805,3 @@ export async function launchCLIinEnvironment( }); }); } - -export async function runCommandInEnvironment( - envPath: string, - command: string -) { - const isWin = process.platform === 'win32'; - const commandScript = createCommandScriptInEnv( - envPath, - appData.condaRootPath, - command, - ' && ' - ); - - // TODO: implement timeout. in case there is network issues - - return new Promise((resolve, reject) => { - const shell = isWin - ? spawn('cmd', ['/c', commandScript], { - env: process.env, - windowsVerbatimArguments: true - }) - : spawn('bash', ['-c', commandScript], { - env: { - ...process.env, - BASH_SILENCE_DEPRECATION_WARNING: '1' - } - }); - - if (shell.stdout) { - shell.stdout.on('data', chunk => { - console.debug('>', Buffer.from(chunk).toString()); - }); - } - if (shell.stderr) { - shell.stderr.on('data', chunk => { - console.error('>', Buffer.from(chunk).toString()); - }); - } - - shell.on('close', code => { - if (code !== 0) { - console.error('Shell exit with code:', code); - resolve(false); - } - resolve(true); - }); - }); -} diff --git a/src/main/config/appdata.ts b/src/main/config/appdata.ts index 97248ec5..cb66b83d 100644 --- a/src/main/config/appdata.ts +++ b/src/main/config/appdata.ts @@ -4,9 +4,8 @@ import * as path from 'path'; import * as fs from 'fs'; import { clearSession, getUserDataDir } from '../utils'; -import { IEnvironmentType, IPythonEnvironment } from '../tokens'; +import { IPythonEnvironment } from '../tokens'; import { SessionConfig } from './sessionconfig'; -import { getOldSettings } from './settings'; import { session as electronSession } from 'electron'; import { ISignal, Signal } from '@lumino/signaling'; @@ -57,15 +56,29 @@ export class ApplicationData { read() { const appDataPath = this._getAppDataPath(); if (!fs.existsSync(appDataPath)) { - // TODO: remove after 07/2023 - this._migrateFromOldSettings(); return; } const data = fs.readFileSync(appDataPath); const jsonData = JSON.parse(data.toString()); + if ('pythonPath' in jsonData) { + this.pythonPath = jsonData.pythonPath; + } + + // TODO: remove after 1/1/2025 if ('condaRootPath' in jsonData) { - this.condaRootPath = jsonData.condaRootPath; + // copied to prevent circular import + const condaExePathForEnvPath = (envPath: string) => { + if (process.platform === 'win32') { + return path.join(envPath, 'Scripts', 'conda.exe'); + } else { + return path.join(envPath, 'bin', 'conda'); + } + }; + this.condaPath = condaExePathForEnvPath(jsonData.condaRootPath); + } + if ('condaPath' in jsonData) { + this.condaPath = jsonData.condaPath; } if ('systemPythonPath' in jsonData) { @@ -158,35 +171,16 @@ export class ApplicationData { } } - private _migrateFromOldSettings() { - const oldSettings = getOldSettings(); - - if (oldSettings.condaRootPath) { - this.condaRootPath = oldSettings.condaRootPath; - } - if (oldSettings.pythonPath) { - this.userSetPythonEnvs.push({ - path: oldSettings.pythonPath, - name: 'env', - type: IEnvironmentType.Path, - versions: {}, - defaultKernel: 'python3' - }); - } - if (oldSettings.remoteURL) { - this.recentRemoteURLs.push({ - url: oldSettings.remoteURL, - date: new Date() - }); - } - } - save() { const appDataPath = this._getAppDataPath(); const appDataJSON: { [key: string]: any } = {}; - if (this.condaRootPath !== '') { - appDataJSON.condaRootPath = this.condaRootPath; + if (this.pythonPath !== '') { + appDataJSON.pythonPath = this.pythonPath; + } + + if (this.condaPath !== '') { + appDataJSON.condaPath = this.condaPath; } if (this.systemPythonPath !== '') { @@ -383,7 +377,17 @@ export class ApplicationData { } newsList: INewsItem[] = []; - condaRootPath: string = ''; + /** + * discovered pythonPath (for JupyterLab server) + */ + pythonPath: string = ''; + /** + * discovered condaPath + */ + condaPath: string = ''; + /** + * discovered Python path + */ systemPythonPath: string = ''; sessions: SessionConfig[] = []; recentRemoteURLs: IRecentRemoteURL[] = []; diff --git a/src/main/config/settings.ts b/src/main/config/settings.ts index 8aed80c7..8045f2ea 100644 --- a/src/main/config/settings.ts +++ b/src/main/config/settings.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import * as fs from 'fs'; -import { getOldUserConfigPath, getUserDataDir, getUserHomeDir } from '../utils'; +import { getUserDataDir, getUserHomeDir } from '../utils'; export const DEFAULT_WIN_WIDTH = 1024; export const DEFAULT_WIN_HEIGHT = 768; @@ -55,7 +55,12 @@ export enum SettingType { ctrlWBehavior = 'ctrlWBehavior', - logLevel = 'logLevel' + logLevel = 'logLevel', + + condaPath = 'condaPath', + systemPythonPath = 'systemPythonPath', + pythonEnvsPath = 'pythonEnvsPath', + condaChannels = 'condaChannels' } export const serverLaunchArgsFixed = [ @@ -141,7 +146,12 @@ export class UserSettings { ctrlWBehavior: new Setting(CtrlWBehavior.CloseTab), - logLevel: new Setting(LogLevel.Warn) + logLevel: new Setting(LogLevel.Warn), + + condaPath: new Setting(''), + systemPythonPath: new Setting(''), + pythonEnvsPath: new Setting(''), + condaChannels: new Setting(['conda-forge']) }; if (readSettings) { @@ -160,8 +170,6 @@ export class UserSettings { read() { const userSettingsPath = this._getUserSettingsPath(); if (!fs.existsSync(userSettingsPath)) { - // TODO: remove after 07/2023 - this._migrateFromOldSettings(); return; } const data = fs.readFileSync(userSettingsPath); @@ -175,23 +183,6 @@ export class UserSettings { } } - private _migrateFromOldSettings() { - const oldSettings = getOldSettings(); - - if (SettingType.checkForUpdatesAutomatically in oldSettings) { - this._settings[SettingType.checkForUpdatesAutomatically].value = - oldSettings[SettingType.checkForUpdatesAutomatically]; - } - if (SettingType.installUpdatesAutomatically in oldSettings) { - this._settings[SettingType.installUpdatesAutomatically].value = - oldSettings[SettingType.installUpdatesAutomatically]; - } - if (SettingType.pythonPath in oldSettings) { - this._settings[SettingType.pythonPath].value = - oldSettings[SettingType.pythonPath]; - } - } - save() { const userSettingsPath = this._getUserSettingsPath(); const userSettings: { [key: string]: any } = {}; @@ -342,22 +333,4 @@ export function resolveWorkingDirectory( return resolved; } -let _oldSettings: any; - -export function getOldSettings() { - if (_oldSettings) { - return _oldSettings; - } - - try { - const oldConfigPath = getOldUserConfigPath(); - const configData = JSON.parse(fs.readFileSync(oldConfigPath).toString()); - _oldSettings = configData['jupyterlab-desktop']['JupyterLabDesktop']; - } catch (error) { - _oldSettings = {}; - } - - return _oldSettings; -} - export const userSettings = new UserSettings(); diff --git a/src/main/env.ts b/src/main/env.ts new file mode 100644 index 00000000..f9f5d10e --- /dev/null +++ b/src/main/env.ts @@ -0,0 +1,584 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import * as fs from 'fs'; +import * as path from 'path'; +import * as semver from 'semver'; +import log from 'electron-log'; +import { SettingType, userSettings } from './config/settings'; +import { appData } from './config/appdata'; +import { + createCommandScriptInEnv, + envPathForPythonPath, + getBundledPythonInstallDir, + isBaseCondaEnv, + pythonPathForEnvPath, + runCommand, + runCommandSync, + versionWithoutSuffix +} from './utils'; +import { + EnvironmentTypeName, + IEnvironmentType, + IPythonEnvironment +} from './tokens'; +import { execFileSync, spawn } from 'child_process'; + +const envInfoPyCode = fs + .readFileSync(path.join(__dirname, 'env_info.py')) + .toString(); + +export interface IJupyterEnvRequirement { + /** + * The display name for the requirement + */ + name: string; + /** + * The actual module name that will be used with the python executable + */ + moduleName: string; + /** + * List of extra commands that will produce a version number for checking + */ + commands: string[]; + /** + * The Range of acceptable version produced by the previous commands field + */ + versionRange: semver.Range; + + /** + * pip install command + */ + pipCommand: string; + /** + * conda install command + */ + condaCommand: string; +} + +const MIN_JLAB_VERSION_REQUIRED = '3.0.0'; + +export const JUPYTER_ENV_REQUIREMENTS = [ + { + name: 'jupyterlab', + moduleName: 'jupyterlab', + commands: ['--version'], + versionRange: new semver.Range(`>=${MIN_JLAB_VERSION_REQUIRED}`), + pipCommand: `"jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}"`, + condaCommand: `"jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}"` + } +]; + +export interface IFormInputValidationResponse { + valid: boolean; + message?: string; +} + +export function getCondaPath() { + let condaPath = userSettings.getValue(SettingType.condaPath); + if (condaPath && fs.existsSync(condaPath)) { + return condaPath; + } + condaPath = appData.condaPath; + if (condaPath && fs.existsSync(condaPath)) { + return condaPath; + } + condaPath = process.env['CONDA_EXE']; + if (condaPath && fs.existsSync(condaPath)) { + return condaPath; + } +} + +export function getCondaChannels(): string[] { + let condaChannels = userSettings.getValue(SettingType.condaChannels); + if (condaChannels && Array.isArray(condaChannels)) { + return condaChannels; + } + + return ['conda-forge']; +} + +export function getSystemPythonPath() { + let pythonPath = userSettings.getValue(SettingType.systemPythonPath); + if (pythonPath && fs.existsSync(pythonPath)) { + return pythonPath; + } + pythonPath = appData.systemPythonPath; + if (pythonPath && fs.existsSync(pythonPath)) { + return pythonPath; + } +} + +export function getPythonEnvsDirectory(): string { + let envsPath = userSettings.getValue(SettingType.pythonEnvsPath); + if (envsPath && fs.existsSync(envsPath)) { + return envsPath; + } + + const userDataDir = getBundledPythonInstallDir(); + + return path.join(userDataDir, 'envs'); +} + +export function getNextPythonEnvName(): string { + const envsDir = getPythonEnvsDirectory(); + const prefix = 'env_'; + const maxTries = 10000; + + let index = 1; + const getNextName = () => { + return `${prefix}${index++}`; + }; + + let name = getNextName(); + + while (fs.existsSync(path.join(envsDir, name))) { + if (index > maxTries) { + return 'invalid_env'; + } + name = getNextName(); + } + + return name; +} + +export function condaExePathForEnvPath(envPath: string) { + if (process.platform === 'win32') { + return path.join(envPath, 'Scripts', 'conda.exe'); + } else { + return path.join(envPath, 'bin', 'conda'); + } +} + +export function condaEnvPathForCondaExePath(condaPath: string) { + return path.resolve(path.dirname(condaPath), '..'); +} + +export interface ICommandRunCallback { + (msg: string): void; +} + +export interface ICommandRunCallbacks { + stdout?: ICommandRunCallback; + stderr?: ICommandRunCallback; +} + +export async function runCommandInEnvironment( + envPath: string, + command: string, + callbacks?: ICommandRunCallbacks +) { + const isWin = process.platform === 'win32'; + const baseCondaPath = getCondaPath(); + const condaEnvPath = condaEnvPathForCondaExePath(baseCondaPath); + const commandScript = createCommandScriptInEnv( + envPath, + condaEnvPath, + command, + ' && ' + ); + + // TODO: implement timeout. in case there is network issues + + return new Promise((resolve, reject) => { + const shell = isWin + ? spawn('cmd', ['/c', commandScript], { + env: process.env, + windowsVerbatimArguments: true + }) + : spawn('bash', ['-c', commandScript], { + env: { + ...process.env, + BASH_SILENCE_DEPRECATION_WARNING: '1' + } + }); + + if (shell.stdout) { + shell.stdout.on('data', chunk => { + const msg = Buffer.from(chunk).toString(); + console.debug('>', msg); + if (callbacks?.stdout) { + callbacks.stdout(msg); + } + }); + } + if (shell.stderr) { + shell.stderr.on('data', chunk => { + const msg = Buffer.from(chunk).toString(); + console.error('>', msg); + if (callbacks?.stdout) { + callbacks.stdout(msg); + } + }); + } + + shell.on('close', code => { + if (code !== 0) { + console.error('Shell exit with code:', code); + resolve(false); + } + resolve(true); + }); + }); +} + +export function validateNewPythonEnvironmentName( + name: string +): IFormInputValidationResponse { + const envsDir = getPythonEnvsDirectory(); + let message = ''; + let valid = false; + + if (name.trim() === '') { + message = 'Name cannot be empty'; + } else if (!name.match(/^[a-zA-Z0-9-_]+$/)) { + message = 'Name can only have letters, numbers, - and _'; + } else if (fs.existsSync(path.join(envsDir, name))) { + message = 'An environment with this name / directory already exists'; + } else { + valid = true; + } + return { + valid, + message + }; +} + +export function validatePythonEnvironmentInstallDirectory( + dirPath: string +): IFormInputValidationResponse { + let message = ''; + let valid = false; + + try { + if (!fs.existsSync(dirPath)) { + message = 'Directory does not exist'; + } else { + const stat = fs.lstatSync(dirPath); + if (!stat || !stat.isDirectory()) { + message = 'Not a directory'; + } else { + valid = true; + } + } + } catch (error) { + message = 'Invalid input. Enter an existing directory path.'; + } + + return { + valid, + message + }; +} + +export async function validatePythonPath( + pythonPath: string +): Promise { + return new Promise((resolve, reject) => { + const returnInvalid = (message: string) => { + resolve({ + valid: false, + message + }); + }; + try { + if (!fs.existsSync(pythonPath)) { + returnInvalid('Python executable does not exist'); + } else { + const stat = fs.lstatSync(pythonPath); + if (!stat || !(stat.isFile() || stat.isSymbolicLink())) { + returnInvalid('Not a valid file'); + } else { + const output = execFileSync(pythonPath, ['--version']); + if (output.toString().trim().startsWith('Python ')) { + resolve({ + valid: true + }); + } + + returnInvalid('Not a valid Python executable'); + } + } + } catch (error) { + returnInvalid('Invalid input. Enter a valid Python executable path.'); + } + }); +} + +/** + * Checks if condaPath is a valid conda executable in a base conda environment + * @param condaPath path to conda executable + * @returns IFormInputValidationResponse with validity and error message if any + */ +export async function validateCondaPath( + condaPath: string +): Promise { + return new Promise((resolve, reject) => { + const returnInvalid = (message: string) => { + resolve({ + valid: false, + message + }); + }; + try { + if (!fs.existsSync(condaPath)) { + returnInvalid('conda executable does not exist'); + } else { + const stat = fs.lstatSync(condaPath); + if (!stat || !stat.isFile()) { + returnInvalid('Not a valid file'); + } else { + const condaEnvPath = condaEnvPathForCondaExePath(condaPath); + if (!isBaseCondaEnv(condaEnvPath)) { + returnInvalid('Executable is not in a base conda environment'); + } else { + try { + let output = ''; + runCommandInEnvironment( + condaEnvPath, + `"${condaPath}" info --json`, + { + stdout: msg => { + output += msg; + } + } + ) + .then(result => { + if (result) { + try { + const jsonOutput = JSON.parse(output); + if ('conda_version' in jsonOutput) { + resolve({ + valid: true + }); + } + } catch (error) { + // + } + } + + returnInvalid('Not a valid conda executable'); + }) + .catch(reason => { + returnInvalid(`Not a valid conda executable. ${reason}`); + }); + } catch (error) { + returnInvalid(`Not a valid conda executable. ${error.message}`); + } + } + } + } + } catch (error) { + returnInvalid('Invalid input. Enter a valid conda executable path.'); + } + }); +} + +export function validateCondaChannels( + condaChannels: string +): IFormInputValidationResponse { + let message = ''; + let valid = false; + + if (condaChannels.trim() === '') { + valid = true; + } else if (!condaChannels.match(/^[a-zA-Z0-9-_ ]+$/)) { + message = 'Channel name can only have letters, numbers, - and _'; + } else { + valid = true; + } + return { + valid, + message + }; +} + +export async function validateSystemPythonPath( + pythonPath: string +): Promise { + return new Promise((resolve, reject) => { + const returnInvalid = (message: string) => { + resolve({ + valid: false, + message + }); + }; + try { + if (!fs.existsSync(pythonPath)) { + returnInvalid('Python executable does not exist'); + } else { + const stat = fs.lstatSync(pythonPath); + if (!stat || !(stat.isFile() || stat.isSymbolicLink())) { + returnInvalid('Not a valid file'); + } else { + const output = execFileSync(pythonPath, ['-c', 'print(":valid:")']); + if (output.toString().trim() === ':valid:') { + resolve({ + valid: true + }); + } + + returnInvalid('Not a valid Python executable'); + } + } + } catch (error) { + returnInvalid('Invalid input. Enter a valid Python executable path.'); + } + }); +} + +export function getAdditionalPathIncludesForPythonPath( + pythonPath: string +): string { + const platform = process.platform; + + let envPath = envPathForPythonPath(pythonPath); + + let pathEnv = ''; + if (platform === 'win32') { + pathEnv = `${envPath};${envPath}\\Library\\mingw-w64\\bin;${envPath}\\Library\\usr\\bin;${envPath}\\Library\\bin;${envPath}\\Scripts;${envPath}\\bin;${process.env['PATH']}`; + } else { + pathEnv = `${envPath}:${envPath}/bin:${process.env['PATH']}`; + } + + return pathEnv; +} + +export async function getEnvironmentInfoFromPythonPath( + pythonPath: string +): Promise { + try { + const envInfoOut = await runCommand(pythonPath, ['-c', envInfoPyCode], { + // TODO: is this still necessary? + env: { PATH: getAdditionalPathIncludesForPythonPath(pythonPath) } + }); + const envInfo = JSON.parse(envInfoOut.trim()); + const envType = + envInfo.type === 'conda-root' + ? IEnvironmentType.CondaRoot + : envInfo.type === 'conda-env' + ? IEnvironmentType.CondaEnv + : IEnvironmentType.VirtualEnv; + const envName = `${EnvironmentTypeName[envType]}: ${envInfo.name}`; + + return { + path: pythonPath, + type: envType, + name: envName, + versions: envInfo.versions, + defaultKernel: envInfo.defaultKernel + }; + } catch (error) { + log.error(`Failed to get environment info at path '${pythonPath}'.`, error); + } +} + +export function getEnvironmentInfoFromPythonPathSync( + pythonPath: string +): IPythonEnvironment { + const envInfoOut = runCommandSync(pythonPath, ['-c', envInfoPyCode], { + env: { PATH: getAdditionalPathIncludesForPythonPath(pythonPath) } + }); + const envInfo = JSON.parse(envInfoOut.trim()); + const envType = + envInfo.type === 'conda-root' + ? IEnvironmentType.CondaRoot + : envInfo.type === 'conda-env' + ? IEnvironmentType.CondaEnv + : IEnvironmentType.VirtualEnv; + const envName = `${EnvironmentTypeName[envType]}: ${envInfo.name}`; + + return { + path: pythonPath, + type: envType, + name: envName, + versions: envInfo.versions, + defaultKernel: envInfo.defaultKernel + }; +} + +export function environmentSatisfiesRequirements( + environment: IPythonEnvironment, + requirements?: IJupyterEnvRequirement[] +): boolean { + if (!requirements) { + requirements = JUPYTER_ENV_REQUIREMENTS; + } + + return requirements.every((req, index, reqSelf) => { + try { + const version = environment.versions[req.name]; + return semver.satisfies(versionWithoutSuffix(version), req.versionRange); + } catch (e) { + return false; + } + }); +} + +export async function updateDiscoveredPythonPaths() { + await updateDiscoveredPathsFromServerPythonPath(); + await updateDiscoveredPathsFromCondaPath(); + await updateDiscoveredPathsFromSystemPythonPath(); +} + +export async function updateDiscoveredPathsFromServerPythonPath() { + const pythonPath = appData.pythonPath; + if (!pythonPath) { + return; + } + + if (!appData.condaPath) { + const envPath = envPathForPythonPath(pythonPath); + const condaPath = condaExePathForEnvPath(envPath); + if ((await validateCondaPath(condaPath)).valid) { + appData.condaPath = condaPath; + } + } + + if (!appData.systemPythonPath) { + appData.systemPythonPath = pythonPath; + } +} + +export async function updateDiscoveredPathsFromCondaPath() { + const condaPath = appData.condaPath; + if (!condaPath) { + return; + } + + const envPath = condaEnvPathForCondaExePath(condaPath); + const pythonPath = pythonPathForEnvPath(envPath); + + if (!appData.pythonPath) { + const env = await getEnvironmentInfoFromPythonPath(pythonPath); + if (env && environmentSatisfiesRequirements(env)) { + appData.pythonPath = env.path; + } + } + + if (!appData.systemPythonPath) { + appData.systemPythonPath = pythonPath; + } +} + +export async function updateDiscoveredPathsFromSystemPythonPath() { + const systemPythonPath = appData.systemPythonPath; + if (!systemPythonPath) { + return; + } + + if (!appData.pythonPath) { + const env = await getEnvironmentInfoFromPythonPath(systemPythonPath); + if (env && environmentSatisfiesRequirements(env)) { + appData.pythonPath = env.path; + } + } + + if (!appData.condaPath) { + const envPath = envPathForPythonPath(systemPythonPath); + const condaPath = condaExePathForEnvPath(envPath); + if ((await validateCondaPath(condaPath)).valid) { + appData.condaPath = condaPath; + } + } +} diff --git a/src/main/eventtypes.ts b/src/main/eventtypes.ts index 5f39aa51..7c1d251d 100644 --- a/src/main/eventtypes.ts +++ b/src/main/eventtypes.ts @@ -19,7 +19,7 @@ export enum EventTypeMain { DeleteRecentSession = 'delete-recent-session', OpenDroppedFiles = 'open-dropped-files', OpenNewsLink = 'open-news-link', - SetPythonPath = 'set-python-path', + SetSessionPythonPath = 'set-session-python-path', ShowEnvSelectPopup = 'show-env-select-popup', HideEnvSelectPopup = 'hide-env-select-popup', SetRemoteServerOptions = 'set-remote-server-options', @@ -58,13 +58,34 @@ export enum EventTypeMain { SetAuthDialogResponse = 'set-auth-dialog-response', InstallPythonEnvRequirements = 'install-python-env-requirements', ShowLogs = 'show-logs', - CopyToClipboard = 'copy-to-clipboard' + CopyToClipboard = 'copy-to-clipboard', + GetNextPythonEnvironmentName = 'get-next-python-environment-name', + CreateNewPythonEnvironment = 'create-new-python-environment', + ShowManagePythonEnvironmentsDialog = 'show-manage-python-environments-dialog', + SelectDirectoryPath = 'select-directory', + SelectFilePath = 'select-file', + ShowPythonEnvironmentContextMenu = 'show-python-environment-context-menu', + DeletePythonEnvironment = 'delete-python-environment', + GetPythonEnvironmentList = 'get-python-environment-list', + GetEnvironmentByPythonPath = 'get-environment-by-python-path', + AddEnvironmentByPythonPath = 'add-environment-by-python-path', + ValidateNewPythonEnvironmentName = 'validate-new-env-name', + ValidatePythonEnvironmentInstallDirectory = 'validate-python-envs-directory', + SetPythonEnvironmentInstallDirectory = 'set-python-envs-directory', + ValidateCondaPath = 'validate-conda-path', + SetCondaPath = 'set-conda-path', + ValidateCondaChannels = 'validate-conda-channels', + SetCondaChannels = 'set-conda-channels', + ValidateSystemPythonPath = 'validate-system-python-path', + SetSystemPythonPath = 'set-system-python-path', + CopySessionInfoToClipboard = 'copy-session-info-to-clipboard', + RestartSession = 'restart-session' } // events sent to Renderer process export enum EventTypeRenderer { WorkingDirectorySelected = 'working-directory-selected', - InstallBundledPythonEnvStatus = 'install-bundled-python-env-status', + InstallPythonEnvStatus = 'install-python-env-status', CustomPythonPathSelected = 'custom-python-path-selected', ShowProgress = 'show-progress', SetCurrentPythonPath = 'set-current-python-path', @@ -76,6 +97,9 @@ export enum EventTypeRenderer { SetRecentSessionList = 'set-recent-session-list', SetNewsList = 'set-news-list', SetNotificationMessage = 'set-notification-message', - DisableLocalServerActions = 'disable-local-server-actions', - SetDefaultWorkingDirectoryResult = 'set-default-working-directory-result' + EnableLocalServerActions = 'enable-local-server-actions', + SetDefaultWorkingDirectoryResult = 'set-default-working-directory-result', + ResetPythonEnvSelectPopup = 'reset-python-env-select-popup', + SetPythonEnvironmentList = 'set-python-environment-list', + SetEnvironmentListUpdateStatus = 'set-environment-list-update-status' } diff --git a/src/main/main.ts b/src/main/main.ts index c1d60c18..da938174 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -8,6 +8,7 @@ import { ICLIArguments } from './tokens'; import { SessionConfig } from './config/sessionconfig'; import { SettingType, userSettings } from './config/settings'; import { parseCLIArgs } from './cli'; +import { getPythonEnvsDirectory } from './env'; let jupyterApp: JupyterApplication; let fileToOpenInMainInstance = ''; @@ -136,6 +137,18 @@ function setupJLabCommand() { } } +function createPythonEnvsDirectory() { + const envsDir = getPythonEnvsDirectory(); + + try { + if (!fs.existsSync(envsDir)) { + fs.mkdirSync(envsDir, { recursive: true }); + } + } catch (error) { + log.error(error); + } +} + function setApplicationMenu() { if (process.platform !== 'darwin') { return; @@ -182,6 +195,7 @@ app.on('ready', async () => { redirectConsoleToLog(); setApplicationMenu(); setupJLabCommand(); + createPythonEnvsDirectory(); argv.cwd = process.cwd(); jupyterApp = new JupyterApplication((argv as unknown) as ICLIArguments); } catch (error) { diff --git a/src/main/progressview/preload.ts b/src/main/progressview/preload.ts index 727b6978..1a261097 100644 --- a/src/main/progressview/preload.ts +++ b/src/main/progressview/preload.ts @@ -50,7 +50,7 @@ ipcRenderer.on( ); ipcRenderer.on( - EventTypeRenderer.InstallBundledPythonEnvStatus, + EventTypeRenderer.InstallPythonEnvStatus, (event, result, message) => { if (onInstallBundledPythonEnvStatusListener) { onInstallBundledPythonEnvStatusListener(result, message); diff --git a/src/main/pythonenvdialog/preload.ts b/src/main/pythonenvdialog/preload.ts new file mode 100644 index 00000000..0546ea5f --- /dev/null +++ b/src/main/pythonenvdialog/preload.ts @@ -0,0 +1,181 @@ +import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; +import { IPythonEnvironment } from '../tokens'; + +const { contextBridge, ipcRenderer } = require('electron'); + +type InstallBundledPythonEnvStatusListener = ( + status: string, + msg: string +) => void; +type CustomPythonPathSelectedListener = (path: string) => void; +type SetPythonEnvironmentListListener = (envs: IPythonEnvironment[]) => void; +type EnvironmentListUpdateStatusListener = ( + status: string, + message: string +) => void; + +let onInstallBundledPythonEnvStatusListener: InstallBundledPythonEnvStatusListener; +let onCustomPythonPathSelectedListener: CustomPythonPathSelectedListener; +let onSetPythonEnvironmentListListener: SetPythonEnvironmentListListener; +let onEnvironmentListUpdateStatusListener: EnvironmentListUpdateStatusListener; + +contextBridge.exposeInMainWorld('electronAPI', { + getAppConfig: () => { + return { + platform: process.platform + }; + }, + isDarkTheme: () => { + return ipcRenderer.invoke(EventTypeMain.IsDarkTheme); + }, + getNextPythonEnvironmentName: () => { + return ipcRenderer.invoke(EventTypeMain.GetNextPythonEnvironmentName); + }, + getPythonEnvironmentList: (cacheOK: boolean) => { + return ipcRenderer.invoke(EventTypeMain.GetPythonEnvironmentList, cacheOK); + }, + createNewPythonEnvironment: ( + envPath: string, + envType: string, + packages: string + ) => { + ipcRenderer.send( + EventTypeMain.CreateNewPythonEnvironment, + envPath, + envType, + packages + ); + }, + selectDirectoryPath: (currentPath?: string) => { + return ipcRenderer.invoke(EventTypeMain.SelectDirectoryPath, currentPath); + }, + selectFilePath: (currentPath?: string) => { + return ipcRenderer.invoke(EventTypeMain.SelectFilePath, currentPath); + }, + showPythonEnvironmentContextMenu: (pythonPath: string) => { + ipcRenderer.send( + EventTypeMain.ShowPythonEnvironmentContextMenu, + pythonPath + ); + }, + browsePythonPath: (currentPath: string) => { + ipcRenderer.send(EventTypeMain.SelectPythonPath, currentPath); + }, + onSetPythonEnvironmentList: (callback: SetPythonEnvironmentListListener) => { + onSetPythonEnvironmentListListener = callback; + }, + onEnvironmentListUpdateStatus: ( + callback: EnvironmentListUpdateStatusListener + ) => { + onEnvironmentListUpdateStatusListener = callback; + }, + installBundledPythonEnv: (envPath: string) => { + ipcRenderer.send(EventTypeMain.InstallBundledPythonEnv, envPath); + }, + updateBundledPythonEnv: () => { + ipcRenderer.send(EventTypeMain.InstallBundledPythonEnv); + }, + onInstallBundledPythonEnvStatus: ( + callback: InstallBundledPythonEnvStatusListener + ) => { + onInstallBundledPythonEnvStatusListener = callback; + }, + selectPythonPath: () => { + ipcRenderer.send(EventTypeMain.SelectPythonPath); + }, + onCustomPythonPathSelected: (callback: CustomPythonPathSelectedListener) => { + onCustomPythonPathSelectedListener = callback; + }, + setDefaultPythonPath: (path: string) => { + ipcRenderer.send(EventTypeMain.SetDefaultPythonPath, path); + }, + validatePythonPath: (path: string) => { + return ipcRenderer.invoke(EventTypeMain.ValidatePythonPath, path); + }, + getEnvironmentByPythonPath: (pythonPath: string) => { + return ipcRenderer.invoke( + EventTypeMain.GetEnvironmentByPythonPath, + pythonPath + ); + }, + addEnvironmentByPythonPath: (pythonPath: string) => { + return ipcRenderer.invoke( + EventTypeMain.AddEnvironmentByPythonPath, + pythonPath + ); + }, + validateNewPythonEnvironmentName: (name: string) => { + return ipcRenderer.invoke( + EventTypeMain.ValidateNewPythonEnvironmentName, + name + ); + }, + validatePythonEnvironmentInstallDirectory: (dirPath: string) => { + return ipcRenderer.invoke( + EventTypeMain.ValidatePythonEnvironmentInstallDirectory, + dirPath + ); + }, + setPythonEnvironmentInstallDirectory: (dirPath: string) => { + return ipcRenderer.send( + EventTypeMain.SetPythonEnvironmentInstallDirectory, + dirPath + ); + }, + validateCondaPath: (condaPath: string) => { + return ipcRenderer.invoke(EventTypeMain.ValidateCondaPath, condaPath); + }, + setCondaPath: (condaPath: string) => { + return ipcRenderer.send(EventTypeMain.SetCondaPath, condaPath); + }, + validateCondaChannels: (condaChannels: string) => { + return ipcRenderer.invoke( + EventTypeMain.ValidateCondaChannels, + condaChannels + ); + }, + setCondaChannels: (condaChannels: string) => { + return ipcRenderer.send(EventTypeMain.SetCondaChannels, condaChannels); + }, + validateSystemPythonPath: (pythonPath: string) => { + return ipcRenderer.invoke( + EventTypeMain.ValidateSystemPythonPath, + pythonPath + ); + }, + setSystemPythonPath: (pythonPath: string) => { + return ipcRenderer.send(EventTypeMain.SetSystemPythonPath, pythonPath); + } +}); + +ipcRenderer.on( + EventTypeRenderer.InstallPythonEnvStatus, + (event, result, msg) => { + if (onInstallBundledPythonEnvStatusListener) { + onInstallBundledPythonEnvStatusListener(result, msg); + } + } +); + +ipcRenderer.on(EventTypeRenderer.CustomPythonPathSelected, (event, path) => { + if (onCustomPythonPathSelectedListener) { + onCustomPythonPathSelectedListener(path); + } +}); + +ipcRenderer.on(EventTypeRenderer.SetPythonEnvironmentList, (event, envs) => { + if (onSetPythonEnvironmentListListener) { + onSetPythonEnvironmentListListener(envs); + } +}); + +ipcRenderer.on( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + (event, status, message) => { + if (onEnvironmentListUpdateStatusListener) { + onEnvironmentListUpdateStatusListener(status, message); + } + } +); + +export {}; diff --git a/src/main/pythonenvdialog/pythonenvdialog.ts b/src/main/pythonenvdialog/pythonenvdialog.ts new file mode 100644 index 00000000..fc58a921 --- /dev/null +++ b/src/main/pythonenvdialog/pythonenvdialog.ts @@ -0,0 +1,1398 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import * as ejs from 'ejs'; +import { + app, + BrowserWindow, + clipboard, + dialog, + Menu, + MenuItemConstructorOptions +} from 'electron'; +import * as path from 'path'; +import * as fs from 'fs'; +import { ThemedWindow } from '../dialog/themedwindow'; +import { IPythonEnvironment } from '../tokens'; +import { + createCommandScriptInEnv, + deletePythonEnvironment, + envPathForPythonPath, + getBundledPythonPath, + isEnvInstalledByDesktopApp, + launchTerminalInDirectory, + openDirectoryInExplorer, + versionWithoutSuffix, + waitForDuration +} from '../utils'; +import { EventManager } from '../eventmanager'; +import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; +import { JupyterApplication } from '../app'; +import { + condaEnvPathForCondaExePath, + getCondaChannels, + getCondaPath, + getNextPythonEnvName, + getPythonEnvsDirectory, + getSystemPythonPath +} from '../env'; + +export class ManagePythonEnvironmentDialog { + constructor(options: ManagePythonEnvironmentDialog.IOptions) { + this._app = options.app; + this._window = new ThemedWindow({ + isDarkTheme: options.isDarkTheme, + title: 'Manage Python environments', + width: 800, + height: 600, + preload: path.join(__dirname, './preload.js') + }); + + let defaultPythonPath = options.defaultPythonPath; + const bundledPythonPath = getBundledPythonPath(); + + if (defaultPythonPath === '') { + defaultPythonPath = bundledPythonPath; + } + let bundledEnvInstallationExists = false; + try { + bundledEnvInstallationExists = fs.existsSync(bundledPythonPath); + } catch (error) { + console.error('Failed to check for bundled Python path', error); + } + + const selectBundledPythonPath = + (defaultPythonPath === '' || defaultPythonPath === bundledPythonPath) && + bundledEnvInstallationExists; + + let bundledEnvInstallationLatest = true; + + if (bundledEnvInstallationExists) { + try { + const bundledEnv = this._app.registry.getEnvironmentByPath( + bundledPythonPath + ); + const jlabVersion = bundledEnv.versions['jupyterlab']; + const appVersion = app.getVersion(); + + if ( + versionWithoutSuffix(jlabVersion) !== versionWithoutSuffix(appVersion) + ) { + bundledEnvInstallationLatest = false; + } + } catch (error) { + console.error('Failed to check bundled environment update', error); + } + } + + const infoIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/info-icon.svg') + ); + const menuIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/ellipsis-vertical.svg') + ); + const checkIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/check-icon.svg') + ); + const xMarkIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/xmark.svg') + ); + const xMarkCircleIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/xmark-circle.svg') + ); + + const pythonEnvName = getNextPythonEnvName(); + const pythonEnvInstallPath = getPythonEnvsDirectory(); + const condaPath = getCondaPath() || ''; + const condaChannels = getCondaChannels().join(' '); + const systemPythonPath = getSystemPythonPath() || ''; + const activateRelPath = + process.platform === 'win32' + ? path.join('Scripts', 'activate.bat') + : path.join('bin', 'activate'); + + this._evm.registerEventHandler( + EventTypeMain.ShowPythonEnvironmentContextMenu, + async (event, pythonPath) => { + const envPath = envPathForPythonPath(pythonPath); + const installedByApp = isEnvInstalledByDesktopApp(envPath); + const deletable = + installedByApp && + !this._app.serverFactory.isEnvironmentInUse(pythonPath); + const openInExplorerLabel = + process.platform === 'darwin' + ? 'Reveal in Finder' + : 'Open in Explorer'; + const envMenuTemplate: MenuItemConstructorOptions[] = [ + { + label: 'Copy Python path', + click: () => { + clipboard.writeText(pythonPath); + } + }, + { + label: 'Copy environment info', + click: () => { + const env = this._app.registry.getEnvironmentByPath(pythonPath); + if (env) { + clipboard.writeText( + JSON.stringify({ + pythonPath: env.path, + name: env.name, + type: env.type, + versions: env.versions, + defaultKernel: env.defaultKernel + }) + ); + } else { + clipboard.writeText('Failed to get environment info!'); + } + } + }, + { + label: 'Launch Terminal', + click: () => { + const condaPath = getCondaPath() || ''; + const condaEnvPath = condaEnvPathForCondaExePath(condaPath); + const activateCommand = createCommandScriptInEnv( + envPath, + condaEnvPath + ); + + launchTerminalInDirectory(envPath, activateCommand); + } + }, + { + label: openInExplorerLabel, + click: () => { + openDirectoryInExplorer(envPath); + } + } + ]; + + const deletableEnvMenuItems: MenuItemConstructorOptions[] = [ + { type: 'separator' }, + { + label: 'Delete', + click: async () => { + const envPath = envPathForPythonPath(pythonPath); + const envName = path.basename(envPath); + + const choice = dialog.showMessageBoxSync({ + type: 'warning', + message: `Delete environment`, + detail: `Are you sure you want to delete "${envName}"?`, + buttons: ['Delete', 'Cancel'], + defaultId: 1, + cancelId: 1 + }); + + // allow dialog to close + if (choice === 0) { + await waitForDuration(200); + } else { + return; + } + + this._window.window.webContents.send( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + 'ENV-DELETE-RUNNING' + ); + try { + await deletePythonEnvironment(envPath); + this._window.window.webContents.send( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + 'ENV-DELETE-RUNNING' + ); + this._app.registry.removeEnvironment(pythonPath); + this._window.window.webContents.send( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + 'ENV-DELETE-FINISHED' + ); + } catch (error) { + this._window.window.webContents.send( + EventTypeRenderer.SetEnvironmentListUpdateStatus, + 'ENV-DELETE-FAILED', + `Failed to delete environment. ${error.message}` + ); + } + } + } + ]; + + const menu = Menu.buildFromTemplate( + deletable + ? [...envMenuTemplate, ...deletableEnvMenuItems] + : envMenuTemplate + ); + menu.popup({ + window: BrowserWindow.fromWebContents(event.sender) + }); + } + ); + + const template = ` + +
+ + Environments + Create new + Settings + + +
+
+
+ Python paths for compatible environments discovered on your system are listed below. You can add other environments by selecting a Python executable path on your system, or create new environments. 'jupyterlab' Python package needs to be installed in an environment to be compatible with JupyterLab Desktop. +
+
+ Add existing + Create new +
+
+ +
+
+
+
${xMarkIconSrc}
+
+
+ +
+
+ + +
+
+
+
+ + +
+
+
+ Create +
+
+ + Copy of the bundled environment + New environment + +
+
+ +
+
+ Name
${infoIconSrc}
+
+
+ +
${checkIconSrc}
${xMarkCircleIconSrc}
+
+
+
+ +
+
+ +
+
+ Environment type +
+
+ + conda + venv + +
+
+ +
+
+ Python packages to install +
+
+ Include jupyterlab (required for use in JupyterLab Desktop) +
+
+ Additional Python packages +
+
+ + +
+
+ +
+
+ Environment create command preview +
+
+ +
+
+ +
+
+ Create +
+
+ Show output + Clear form +
+
+ +
+
+ +
+
+
+
+ + +
+
+
+ Default Python path for JupyterLab Server
${infoIconSrc}
+
+
+
+
+ + Install + Update +
+
+
+ + <%= !bundledEnvInstallationExists ? 'disabled' : '' %> onchange="handleDefaultPythonEnvTypeChange(this);">Use bundled Python environment installation + onchange="handleDefaultPythonEnvTypeChange(this);">Use custom Python environment + + +
+
+ +
${checkIconSrc}
${xMarkCircleIconSrc}
+
+
+
+ Select path +
+
+
+
+
+ +
+
+ New Python environment install directory
${infoIconSrc}
+
+
+
+ +
${checkIconSrc}
${xMarkCircleIconSrc}
+
+
+
+ Select path +
+
+
+ +
+
+ conda path
${infoIconSrc}
+
+
+
+ +
${checkIconSrc}
${xMarkCircleIconSrc}
+
+
+
+ Select path +
+
+
+ +
+
+ conda channels
${infoIconSrc}
+
+
+
+ +
${checkIconSrc}
${xMarkCircleIconSrc}
+
+
+
+
+ +
+
+ Python path to use when creating venv environments
${infoIconSrc}
+
+
+
+ +
${checkIconSrc}
${xMarkCircleIconSrc}
+
+
+
+ Select path +
+
+
+
+
+
+ +
+ + `; + this._pageBody = ejs.render(template, { + envs: options.envs, + defaultPythonPath, + selectBundledPythonPath, + bundledEnvInstallationExists, + bundledEnvInstallationLatest, + pythonEnvName, + pythonEnvInstallPath, + condaPath, + condaChannels, + systemPythonPath + }); + } + + get window(): BrowserWindow { + return this._window.window; + } + + load() { + this._window.loadDialogContent(this._pageBody); + + this._app.registry.environmentListUpdated.connect( + this._onEnvironmentListUpdated, + this + ); + + this._window.window.on('closed', () => { + this._app.registry.environmentListUpdated.disconnect( + this._onEnvironmentListUpdated, + this + ); + this._evm.dispose(); + }); + } + + setPythonEnvironmentList(envs: IPythonEnvironment[]) { + this._window.window.webContents.send( + EventTypeRenderer.SetPythonEnvironmentList, + envs + ); + } + + private async _onEnvironmentListUpdated() { + const envs = await this._app.registry.getEnvironmentList(true); + this.setPythonEnvironmentList(envs); + } + + private _window: ThemedWindow; + private _pageBody: string; + private _evm = new EventManager(); + private _app: JupyterApplication; +} + +export namespace ManagePythonEnvironmentDialog { + export enum Tab { + Environments = 'envs', + Create = 'create', + Settings = 'settings' + } + + export interface IOptions { + isDarkTheme: boolean; + app: JupyterApplication; + envs: IPythonEnvironment[]; + defaultPythonPath: string; + activateTab?: Tab; + } +} diff --git a/src/main/pythonenvselectpopup/preload.ts b/src/main/pythonenvselectpopup/preload.ts index dfd59940..3e1d09b1 100644 --- a/src/main/pythonenvselectpopup/preload.ts +++ b/src/main/pythonenvselectpopup/preload.ts @@ -1,12 +1,20 @@ import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; +import { IPythonEnvironment } from '../tokens'; const { contextBridge, ipcRenderer } = require('electron'); -type CurrentPythonPathSetListener = (path: string) => void; +type CurrentPythonPathSetListener = ( + path: string, + relativePath: string +) => void; +type ResetPythonEnvSelectPopupListener = () => void; type CustomPythonPathSelectedListener = (path: string) => void; +type SetPythonEnvironmentListListener = (envs: IPythonEnvironment[]) => void; let onCustomPythonPathSelectedListener: CustomPythonPathSelectedListener; let onCurrentPythonPathSetListener: CurrentPythonPathSetListener; +let onResetPythonEnvSelectPopupListener: ResetPythonEnvSelectPopupListener; +let onSetPythonEnvironmentListListener: SetPythonEnvironmentListListener; contextBridge.exposeInMainWorld('electronAPI', { getAppConfig: () => { @@ -17,26 +25,52 @@ contextBridge.exposeInMainWorld('electronAPI', { isDarkTheme: () => { return ipcRenderer.invoke(EventTypeMain.IsDarkTheme); }, + showManagePythonEnvsDialog: () => { + ipcRenderer.send(EventTypeMain.ShowManagePythonEnvironmentsDialog); + }, browsePythonPath: (currentPath: string) => { ipcRenderer.send(EventTypeMain.SelectPythonPath, currentPath); }, - setPythonPath: (path: string) => { - ipcRenderer.send(EventTypeMain.SetPythonPath, path); + setSessionPythonPath: (path: string) => { + ipcRenderer.send(EventTypeMain.SetSessionPythonPath, path); }, onCurrentPythonPathSet: (callback: CurrentPythonPathSetListener) => { onCurrentPythonPathSetListener = callback; }, + onResetPythonEnvSelectPopup: ( + callback: ResetPythonEnvSelectPopupListener + ) => { + onResetPythonEnvSelectPopupListener = callback; + }, onCustomPythonPathSelected: (callback: CustomPythonPathSelectedListener) => { onCustomPythonPathSelectedListener = callback; }, hideEnvSelectPopup: () => { ipcRenderer.send(EventTypeMain.HideEnvSelectPopup); + }, + onSetPythonEnvironmentList: (callback: SetPythonEnvironmentListListener) => { + onSetPythonEnvironmentListListener = callback; + }, + restartSession: () => { + ipcRenderer.send(EventTypeMain.RestartSession); + }, + copySessionInfo: () => { + ipcRenderer.send(EventTypeMain.CopySessionInfoToClipboard); } }); -ipcRenderer.on(EventTypeRenderer.SetCurrentPythonPath, (event, path) => { - if (onCurrentPythonPathSetListener) { - onCurrentPythonPathSetListener(path); +ipcRenderer.on( + EventTypeRenderer.SetCurrentPythonPath, + (event, path, relativePath) => { + if (onCurrentPythonPathSetListener) { + onCurrentPythonPathSetListener(path, relativePath); + } + } +); + +ipcRenderer.on(EventTypeRenderer.ResetPythonEnvSelectPopup, event => { + if (onResetPythonEnvSelectPopupListener) { + onResetPythonEnvSelectPopupListener(); } }); @@ -46,4 +80,10 @@ ipcRenderer.on(EventTypeRenderer.CustomPythonPathSelected, (event, path) => { } }); +ipcRenderer.on(EventTypeRenderer.SetPythonEnvironmentList, (event, envs) => { + if (onSetPythonEnvironmentListListener) { + onSetPythonEnvironmentListListener(envs); + } +}); + export {}; diff --git a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts index ca832e50..04c10d42 100644 --- a/src/main/pythonenvselectpopup/pythonenvselectpopup.ts +++ b/src/main/pythonenvselectpopup/pythonenvselectpopup.ts @@ -3,12 +3,16 @@ import * as ejs from 'ejs'; import * as path from 'path'; +import * as fs from 'fs'; import { ThemedView } from '../dialog/themedview'; import { EventTypeRenderer } from '../eventtypes'; import { IPythonEnvironment } from '../tokens'; +import { IApplication } from '../app'; +import { getRelativePathToUserHome } from '../utils'; export class PythonEnvironmentSelectPopup { constructor(options: PythonEnvironmentSelectView.IOptions) { + this._app = options.app; this._view = new ThemedView({ isDarkTheme: options.isDarkTheme, preload: path.join(__dirname, './preload.js') @@ -17,6 +21,21 @@ export class PythonEnvironmentSelectPopup { const { envs, defaultPythonPath, bundledPythonPath } = options; this._envs = options.envs; const currentPythonPath = options.currentPythonPath || ''; + const currentPythonPathRelative = + getRelativePathToUserHome(currentPythonPath) || currentPythonPath; + + const uFuzzyScriptSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/uFuzzy.iife.min.js') + ); + const restartIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/rotate-right-icon.svg') + ); + const copyIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/copy-icon.svg') + ); + const xMarkIconSrc = fs.readFileSync( + path.join(__dirname, '../../../app-assets/xmark.svg') + ); const template = ` +
+ +
+ +
- + - +
- <% if (envs.length > 0) { %> - <% - function getEnvTooltip(env) { - const packages = []; - for (const name in env.versions) { - packages.push(name + ': ' + env.versions[name]); - } - return env.name + '\\n' + env.path + '\\n' + packages.join(', '); - } - %>
- - <% envs.forEach(env => { %> - <%- env.path %> -
<%- env.name %><%- env.path === ${JSON.stringify( - defaultPythonPath - )} ? ' (default)' : env.path === ${JSON.stringify( - bundledPythonPath - )} ? ' (bundled)' : '' %>
-
- <% }); %> -
+
- <% } %>
`; this._pageBody = ejs.render(template, { currentPythonPath, + currentPythonPathRelative, envs }); } @@ -200,31 +418,61 @@ export class PythonEnvironmentSelectPopup { load() { this._view.loadViewContent(this._pageBody); + + this._app.registry.environmentListUpdated.connect( + this._onEnvironmentListUpdated, + this + ); } setCurrentPythonPath(currentPythonPath: string) { + const relativePath = getRelativePathToUserHome(currentPythonPath); this._view.view.webContents.send( EventTypeRenderer.SetCurrentPythonPath, - currentPythonPath + currentPythonPath, + relativePath || currentPythonPath + ); + } + + resetView() { + this._view.view.webContents.send( + EventTypeRenderer.ResetPythonEnvSelectPopup ); } getScrollHeight(): number { const envCount = this._envs.length; return ( + 34 + // header + 30 + // title 40 + // path input (envCount > 0 ? envCount * 40 + 14 : 0) + // env list - 12 - ); // padding + 17 // padding + ); + } + + setPythonEnvironmentList(envs: IPythonEnvironment[]) { + this._envs = envs; + this._view.view.webContents.send( + EventTypeRenderer.SetPythonEnvironmentList, + envs + ); + } + + private async _onEnvironmentListUpdated() { + const envs = await this._app.registry.getEnvironmentList(true); + this.setPythonEnvironmentList(envs); } private _view: ThemedView; private _pageBody: string; private _envs: IPythonEnvironment[]; + private _app: IApplication; } export namespace PythonEnvironmentSelectView { export interface IOptions { + app: IApplication; isDarkTheme: boolean; envs: IPythonEnvironment[]; bundledPythonPath: string; diff --git a/src/main/registry.ts b/src/main/registry.ts index e0dc4b6a..3149e559 100644 --- a/src/main/registry.ts +++ b/src/main/registry.ts @@ -1,18 +1,16 @@ -import { execFile, ExecFileOptions, execFileSync } from 'child_process'; import { basename, join, normalize } from 'path'; import * as path from 'path'; -import * as semver from 'semver'; import * as fs from 'fs'; import log from 'electron-log'; const which = require('which'); const WinRegistry = require('winreg'); import { ISignal, Signal } from '@lumino/signaling'; import { - EnvironmentTypeName, IDisposable, IEnvironmentType, IPythonEnvironment, IPythonEnvResolveError, + IPythonEnvValidateResult, IVersionContainer, PythonEnvResolveErrorType } from './tokens'; @@ -26,28 +24,36 @@ import { isCondaEnv, isPortInUse, pythonPathForEnvPath, - versionWithoutSuffix + runCommand } from './utils'; import { SettingType, userSettings } from './config/settings'; import { appData } from './config/appdata'; - -const envInfoPyCode = fs - .readFileSync(path.join(__dirname, 'env_info.py')) - .toString(); +import { + condaEnvPathForCondaExePath, + condaExePathForEnvPath, + environmentSatisfiesRequirements, + getCondaChannels, + getEnvironmentInfoFromPythonPath, + getEnvironmentInfoFromPythonPathSync, + getPythonEnvsDirectory, + IJupyterEnvRequirement, + JUPYTER_ENV_REQUIREMENTS, + updateDiscoveredPythonPaths, + validatePythonPath +} from './env'; export interface IRegistry { getDefaultEnvironment: () => Promise; getEnvironmentByPath: (pythonPath: string) => IPythonEnvironment; getEnvironmentList: (cacheOK: boolean) => Promise; - condaRootPath: Promise; - setCondaRootPath(rootPath: string): void; addEnvironment: (pythonPath: string) => IPythonEnvironment; - validatePythonEnvironmentAtPath: (pythonPath: string) => boolean; + removeEnvironment: (pythonPath: string) => boolean; + validatePythonEnvironmentAtPath: ( + pythonPath: string + ) => Promise; validateCondaBaseEnvironmentAtPath: (envPath: string) => boolean; - setDefaultPythonPath: (pythonPath: string) => void; + setDefaultPythonPath: (pythonPath: string) => boolean; getCurrentPythonEnvironment: () => IPythonEnvironment; - getAdditionalPathIncludesForPythonPath: (pythonPath: string) => string; - getRequirements: () => Registry.IRequirement[]; getRequirementsInstallCommand: (envPath: string) => string; getEnvironmentInfo(pythonPath: string): Promise; getRunningServerList(): Promise; @@ -57,61 +63,88 @@ export interface IRegistry { } export const SERVER_TOKEN_PREFIX = 'jlab:srvr:'; -const MIN_JLAB_VERSION_REQUIRED = '3.0.0'; export class Registry implements IRegistry, IDisposable { constructor() { - this._requirements = [ - { - name: 'jupyterlab', - moduleName: 'jupyterlab', - commands: ['--version'], - versionRange: new semver.Range(`>=${MIN_JLAB_VERSION_REQUIRED}`), - pipCommand: `"jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}"`, - condaCommand: `"jupyterlab>=${MIN_JLAB_VERSION_REQUIRED}"` - } - ]; + // TODO: refactor into a recallable method // initialize environment list and default + // initialize environment list to cached ones this._environments = [ ...appData.discoveredPythonEnvs, ...appData.userSetPythonEnvs ].filter(env => this._pathExistsSync(env.path)); + // initialize default environment to user set or bundled + // TODO: try to use userSettings.pythonPath and bundled python path separately + // because userSettings.pythonPath might be invalid but bundled one may be valid let pythonPath = userSettings.getValue(SettingType.pythonPath); if (pythonPath === '') { pythonPath = getBundledPythonPath(); } + // TODO: validate appData.condaPath and appData.systemPythonPath. getCondaPath instead of appData.condaPath + // try to set condaPath from CONDA_EXE + try { const defaultEnv = this._resolveEnvironmentSync(pythonPath); if (defaultEnv) { this._defaultEnv = defaultEnv; + // if default env is conda root, then set its conda executable as conda path if ( defaultEnv.type === IEnvironmentType.CondaRoot && - !this._condaRootPath + !appData.condaPath ) { - // this call overrides user set appData.condaRootPath - // which is probably better for compatibility - this.setCondaRootPath(getEnvironmentPath(defaultEnv)); + this.setCondaPath( + condaExePathForEnvPath(getEnvironmentPath(defaultEnv)) + ); } } } catch (error) { // } - if (!this._condaRootPath && appData.condaRootPath) { - if (this.validateCondaBaseEnvironmentAtPath(appData.condaRootPath)) { - this.setCondaRootPath(appData.condaRootPath); + // try to set default env from discovered pythonPath + if (!this._defaultEnv && appData.pythonPath) { + try { + const defaultEnv = this._resolveEnvironmentSync(appData.pythonPath); + + if (defaultEnv) { + this._defaultEnv = defaultEnv; + // if default env is conda root, then set its conda executable as conda path + if ( + defaultEnv.type === IEnvironmentType.CondaRoot && + !appData.condaPath + ) { + this.setCondaPath( + condaExePathForEnvPath(getEnvironmentPath(defaultEnv)) + ); + } + } + } catch (error) { + // + } + } - // set default env from appData.condaRootPath + // try to set default env from condaPath + if (!this._defaultEnv && appData.condaPath) { + if ( + this.validateCondaBaseEnvironmentAtPath( + condaEnvPathForCondaExePath(appData.condaPath) + ) + ) { + // set default env from appData.condPath if (!this._defaultEnv) { - const pythonPath = pythonPathForEnvPath(appData.condaRootPath, true); + const condaEnvPath = condaEnvPathForCondaExePath(appData.condaPath); + const pythonPath = pythonPathForEnvPath(condaEnvPath, true); try { const defaultEnv = this._resolveEnvironmentSync(pythonPath); if (defaultEnv) { this._defaultEnv = defaultEnv; + if (!appData.pythonPath) { + appData.pythonPath = pythonPath; + } } } catch (error) { // @@ -120,21 +153,22 @@ export class Registry implements IRegistry, IDisposable { } } + // try to set systemPythonPath from default env if ( - !this._systemPythonPath && - appData.systemPythonPath && - fs.existsSync(appData.systemPythonPath) + !appData.systemPythonPath && + this._defaultEnv && + fs.existsSync(this._defaultEnv.path) ) { - this.setSystemPythonPath(appData.systemPythonPath); + this.setSystemPythonPath(this._defaultEnv.path); } + // discover environments on system + // TODO: rediscover environments in app's envs directory const pathEnvironments = this._loadPathEnvironments(); const condaEnvironments = this._loadCondaEnvironments(); const allEnvironments = [pathEnvironments, condaEnvironments]; if (process.platform === 'win32') { - let windowRegEnvironments = this._loadWindowsRegistryEnvironments( - this._requirements - ); + let windowRegEnvironments = this._loadWindowsRegistryEnvironments(); allEnvironments.push(windowRegEnvironments); } @@ -143,7 +177,7 @@ export class Registry implements IRegistry, IDisposable { let discoveredEnvs = [].concat(...environments); this._userSetEnvironments = await this._resolveEnvironments( - appData.userSetPythonEnvs, + this._getUserSetPythonEnvs(), true ); @@ -161,8 +195,12 @@ export class Registry implements IRegistry, IDisposable { if (!this._defaultEnv && this._environments.length > 0) { this._defaultEnv = this._environments[0]; + if (!appData.pythonPath) { + appData.pythonPath = this._defaultEnv.path; + } } - return; + + await updateDiscoveredPythonPaths(); }) .catch(reason => { if (reason.fileName || reason.lineNumber) { @@ -196,6 +234,47 @@ export class Registry implements IRegistry, IDisposable { this._environmentListUpdated.emit(); } + // rediscover Python envs directory for user installed environments, + // in case they are not in cache already + private _getUserSetPythonEnvs(): IPythonEnvironment[] { + const envsDir = getPythonEnvsDirectory(); + const envsDirExists = + envsDir && fs.existsSync(envsDir) && fs.statSync(envsDir).isDirectory(); + let userInstalledEnvs: IPythonEnvironment[] = []; + + if (envsDirExists) { + try { + const list = fs.readdirSync(envsDir); + list.forEach(filePath => { + const envPath = envsDir + '/' + filePath; + const stat = fs.lstatSync(envPath); + if (stat && stat.isDirectory()) { + const pythonPath = pythonPathForEnvPath(envPath); + if (fs.existsSync(pythonPath)) { + const found = appData.userSetPythonEnvs.find(env => { + return env.path === pythonPath; + }); + + if (!found) { + userInstalledEnvs.push({ + path: pythonPath, + name: '', + type: IEnvironmentType.Path, + versions: {}, + defaultKernel: 'python3' + }); + } + } + } + }); + } catch (error) { + log.error(`Failed to re-discover /envs directory (${envsDir})`, error); + } + } + + return [...appData.userSetPythonEnvs, ...userInstalledEnvs]; + } + private async _resolveEnvironments( envs: IPythonEnvironment[], sort?: boolean @@ -210,7 +289,7 @@ export class Registry implements IRegistry, IDisposable { filteredEnvs = resolvedEnvs.filter(env => env !== undefined); if (sort) { - this._sortEnvironments(filteredEnvs, this._requirements); + this._sortEnvironments(filteredEnvs, JUPYTER_ENV_REQUIREMENTS); } return filteredEnvs; @@ -225,14 +304,16 @@ export class Registry implements IRegistry, IDisposable { const env = await this.getEnvironmentInfo(pythonPath); - if ( - env && - this._environmentSatisfiesRequirements(env, this._requirements) - ) { + if (env && environmentSatisfiesRequirements(env)) { return env; } } + /** + * Resolve Python environment at pythonPath + * @param pythonPath Python path + * @returns Python environment info or throws exception PythonEnvResolveErrorType + */ private _resolveEnvironmentSync(pythonPath: string): IPythonEnvironment { if (!this._pathExistsSync(pythonPath)) { log.error(`Python path "${pythonPath}" does not exist.`); @@ -255,10 +336,10 @@ export class Registry implements IRegistry, IDisposable { } as IPythonEnvResolveError; } - if (!this._environmentSatisfiesRequirements(env, this._requirements)) { + if (!environmentSatisfiesRequirements(env)) { const envPath = envPathForPythonPath(pythonPath); const versionsFound: string[] = []; - this._requirements.forEach(req => { + JUPYTER_ENV_REQUIREMENTS.forEach(req => { versionsFound.push(`${req.name}: ${env.versions[req.name]}`); }); @@ -334,21 +415,11 @@ export class Registry implements IRegistry, IDisposable { } } - get condaRootPath(): Promise { - return Promise.resolve(this._condaRootPath); - } - - setCondaRootPath(rootPath: string) { - this._condaRootPath = rootPath; - appData.condaRootPath = rootPath; - } - - get systemPythonPath(): string { - return this._systemPythonPath; + setCondaPath(rootPath: string) { + appData.condaPath = rootPath; } setSystemPythonPath(pythonPath: string) { - this._systemPythonPath = pythonPath; appData.systemPythonPath = pythonPath; } @@ -372,18 +443,90 @@ export class Registry implements IRegistry, IDisposable { return inUserSetEnvList; } - const env = this._resolveEnvironmentSync(pythonPath); - if (env) { - this._userSetEnvironments.push(env); + try { + const env = this._resolveEnvironmentSync(pythonPath); + if (env) { + if (!this._defaultEnv) { + this._defaultEnv = env; + } + this._userSetEnvironments.push(env); + this._updateEnvironments(); + this._environmentListUpdated.emit(); + return env; + } + } catch (error) { + // + } + } + + /** + * Remove environment from registry. + * @param pythonPath The location of the python executable to create an environment from + */ + removeEnvironment(pythonPath: string): boolean { + const discoveredEnvironments = this._discoveredEnvironments.filter( + env => pythonPath !== env.path + ); + if (discoveredEnvironments.length < this._discoveredEnvironments.length) { + this._discoveredEnvironments = discoveredEnvironments; this._updateEnvironments(); this._environmentListUpdated.emit(); + return true; } - return env; + const userSetEnvironments = this._userSetEnvironments.filter( + env => pythonPath !== env.path + ); + if (userSetEnvironments.length < this._userSetEnvironments.length) { + this._userSetEnvironments = userSetEnvironments; + this._updateEnvironments(); + this._environmentListUpdated.emit(); + return true; + } + + return false; } - validatePythonEnvironmentAtPath(pythonPath: string): boolean { - return this._resolveEnvironment(pythonPath) !== undefined; + async validatePythonEnvironmentAtPath( + pythonPath: string + ): Promise { + if (!fs.existsSync(pythonPath)) { + return { + valid: false, + error: { + type: PythonEnvResolveErrorType.PathNotFound + } + }; + } + if (!(await validatePythonPath(pythonPath)).valid) { + return { + valid: false, + error: { + type: PythonEnvResolveErrorType.InvalidPythonBinary + } + }; + } + + try { + const env = this._resolveEnvironmentSync(pythonPath); + if (!env) { + return { + valid: false, + error: { + type: PythonEnvResolveErrorType.ResolveError + } + }; + } + } catch (error) { + return { + valid: false, + error + }; + } + + return { + valid: true + }; } validateCondaBaseEnvironmentAtPath(envPath: string): boolean { @@ -395,36 +538,7 @@ export class Registry implements IRegistry, IDisposable { return; } - try { - const envInfoOut = await this._runCommand( - pythonPath, - ['-c', envInfoPyCode], - { - env: { PATH: this.getAdditionalPathIncludesForPythonPath(pythonPath) } - } - ); - const envInfo = JSON.parse(envInfoOut.trim()); - const envType = - envInfo.type === 'conda-root' - ? IEnvironmentType.CondaRoot - : envInfo.type === 'conda-env' - ? IEnvironmentType.CondaEnv - : IEnvironmentType.VirtualEnv; - const envName = `${EnvironmentTypeName[envType]}: ${envInfo.name}`; - - return { - path: pythonPath, - type: envType, - name: envName, - versions: envInfo.versions, - defaultKernel: envInfo.defaultKernel - }; - } catch (error) { - log.error( - `Failed to get environment info at path '${pythonPath}'.`, - error - ); - } + return getEnvironmentInfoFromPythonPath(pythonPath); } getEnvironmentInfoSync(pythonPath: string): IPythonEnvironment { @@ -432,61 +546,48 @@ export class Registry implements IRegistry, IDisposable { return; } - const envInfoOut = this._runCommandSync(pythonPath, ['-c', envInfoPyCode], { - env: { PATH: this.getAdditionalPathIncludesForPythonPath(pythonPath) } - }); - const envInfo = JSON.parse(envInfoOut.trim()); - const envType = - envInfo.type === 'conda-root' - ? IEnvironmentType.CondaRoot - : envInfo.type === 'conda-env' - ? IEnvironmentType.CondaEnv - : IEnvironmentType.VirtualEnv; - const envName = `${EnvironmentTypeName[envType]}: ${envInfo.name}`; - - return { - path: pythonPath, - type: envType, - name: envName, - versions: envInfo.versions, - defaultKernel: envInfo.defaultKernel - }; - } - - setDefaultPythonPath(pythonPath: string): void { - this._defaultEnv = this.getEnvironmentByPath(pythonPath); - } - - getCurrentPythonEnvironment(): IPythonEnvironment { - return this._defaultEnv; + return getEnvironmentInfoFromPythonPathSync(pythonPath); } - getAdditionalPathIncludesForPythonPath(pythonPath: string): string { - const platform = process.platform; + setDefaultPythonPath(pythonPath: string): boolean { + if (pythonPath === '') { + pythonPath = getBundledPythonPath(); + } - let envPath = envPathForPythonPath(pythonPath); + // check if already resolved + let env = this.getEnvironmentByPath(pythonPath); + if (env) { + this._defaultEnv = env; + return true; + } - let pathEnv = ''; - if (platform === 'win32') { - pathEnv = `${envPath};${envPath}\\Library\\mingw-w64\\bin;${envPath}\\Library\\usr\\bin;${envPath}\\Library\\bin;${envPath}\\Scripts;${envPath}\\bin;${process.env['PATH']}`; - } else { - pathEnv = `${envPath}:${envPath}/bin:${process.env['PATH']}`; + // try to resolve it + try { + env = this._resolveEnvironmentSync(pythonPath); + if (env) { + this._defaultEnv = env; + return true; + } + } catch (error) { + // } - return pathEnv; + return false; } - getRequirements(): Registry.IRequirement[] { - return this._requirements; + getCurrentPythonEnvironment(): IPythonEnvironment { + return this._defaultEnv; } getRequirementsInstallCommand(envPath: string): string { const isConda = isCondaEnv(envPath); + const condaChannels = getCondaChannels(); + const channels = condaChannels.map(channel => `-c ${channel}`); const cmdList = [ - isConda ? 'conda install -c conda-forge -y' : 'pip install' + isConda ? `conda install ${channels.join(' ')} -y` : 'pip install' ]; - this._requirements.forEach(req => { + JUPYTER_ENV_REQUIREMENTS.forEach(req => { cmdList.push(isConda ? req.condaCommand : req.pipCommand); }); @@ -578,7 +679,7 @@ export class Registry implements IRegistry, IDisposable { const pythonPaths = await flattenedPythonPaths; - if (!this._systemPythonPath && pythonPaths.length > 0) { + if (!appData.systemPythonPath && pythonPaths.length > 0) { this.setSystemPythonPath(pythonPaths[0]); } @@ -598,8 +699,8 @@ export class Registry implements IRegistry, IDisposable { private async _loadCondaEnvironments(): Promise { const pathCondas = this._getPathCondas(); const commonCondas = Promise.resolve( - Registry.COMMON_CONDA_LOCATIONS.filter(condaPath => - this._pathExistsSync(condaPath) + Registry.COMMON_CONDA_LOCATIONS.filter(condaEnvPath => + this._pathExistsSync(condaEnvPath) ) ); @@ -691,19 +792,19 @@ export class Registry implements IRegistry, IDisposable { ); const uniqueCondaRoots = this._getUniqueObjects(flattenedCondaRoots); - return uniqueCondaRoots.map(condaRootPath => { - const path = pythonPathForEnvPath(condaRootPath, true); + return uniqueCondaRoots.map(condaRootEnvPath => { + const path = pythonPathForEnvPath(condaRootEnvPath, true); const newRootEnvironment: IPythonEnvironment = { - name: basename(condaRootPath), + name: basename(condaRootEnvPath), path: path, type: IEnvironmentType.CondaRoot, versions: {}, defaultKernel: 'python3' }; - if (!this._condaRootPath) { - this.setCondaRootPath(condaRootPath); + if (!appData.condaPath) { + this.setCondaPath(condaExePathForEnvPath(condaRootEnvPath)); } return newRootEnvironment; @@ -716,7 +817,7 @@ export class Registry implements IRegistry, IDisposable { return await Promise.all( condasInPath.map(async condaExecutablePath => { - const condaInfoOutput = this._runCommand(condaExecutablePath, [ + const condaInfoOutput = runCommand(condaExecutablePath, [ 'info', '--json' ]); @@ -741,9 +842,9 @@ export class Registry implements IRegistry, IDisposable { ); } - private async _loadWindowsRegistryEnvironments( - requirements: Registry.IRequirement[] - ): Promise { + private async _loadWindowsRegistryEnvironments(): Promise< + IPythonEnvironment[] + > { const valuePredicate = (value: any) => { return value.name === '(Default)'; }; @@ -827,23 +928,6 @@ export class Registry implements IRegistry, IDisposable { return pathValues.filter(valueFilter).map(v => v.value); } - private _environmentSatisfiesRequirements( - environment: IPythonEnvironment, - requirements: Registry.IRequirement[] - ): boolean { - return requirements.every((req, index, reqSelf) => { - try { - const version = environment.versions[req.name]; - return semver.satisfies( - versionWithoutSuffix(version), - req.versionRange - ); - } catch (e) { - return false; - } - }); - } - private _convertExecutableOutputFromJson( output: Promise ): Promise { @@ -912,7 +996,7 @@ export class Registry implements IRegistry, IDisposable { ): Promise { let totalCommands = ['-m', moduleName].concat(commands); return new Promise((resolve, reject) => { - this._runCommand(pythonPath, totalCommands) + runCommand(pythonPath, totalCommands) .then(output => { let missingModuleReg = new RegExp(`No module named ${moduleName}$`); let commandErrorReg = new RegExp(`Error executing Jupyter command`); @@ -933,82 +1017,9 @@ export class Registry implements IRegistry, IDisposable { }); } - private async _runCommand( - executablePath: string, - commands: string[], - options?: ExecFileOptions - ): Promise { - return new Promise((resolve, reject) => { - let executableRun = execFile(executablePath, commands, options); - let stdoutBufferChunks: Buffer[] = []; - let stdoutLength = 0; - let stderrBufferChunks: Buffer[] = []; - let stderrLength = 0; - - executableRun.stdout.on('data', chunk => { - if (typeof chunk === 'string') { - let newBuffer = Buffer.from(chunk); - stdoutLength += newBuffer.length; - stdoutBufferChunks.push(newBuffer); - } else { - stdoutLength += chunk.length; - stdoutBufferChunks.push(chunk); - } - }); - - executableRun.stderr.on('data', chunk => { - if (typeof chunk === 'string') { - let newBuffer = Buffer.from(chunk); - stderrLength += newBuffer.length; - stderrBufferChunks.push(Buffer.from(newBuffer)); - } else { - stderrLength += chunk.length; - stderrBufferChunks.push(chunk); - } - }); - - executableRun.on('close', () => { - executableRun.removeAllListeners(); - - let stdoutOutput = Buffer.concat( - stdoutBufferChunks, - stdoutLength - ).toString(); - let stderrOutput = Buffer.concat( - stderrBufferChunks, - stderrLength - ).toString(); - - if (stdoutOutput.length === 0) { - if (stderrOutput.length === 0) { - reject( - new Error( - `"${executablePath} ${commands.join( - ' ' - )}" produced no output to stdout or stderr!` - ) - ); - } else { - resolve(stderrOutput); - } - } else { - resolve(stdoutOutput); - } - }); - }); - } - - private _runCommandSync( - executablePath: string, - commands: string[], - options?: ExecFileOptions - ): string { - return execFileSync(executablePath, commands, options).toString(); - } - private _sortEnvironments( environments: IPythonEnvironment[], - requirements: Registry.IRequirement[] + requirements: IJupyterEnvRequirement[] ) { environments.sort((a, b) => { let typeCompareResult = this._compareEnvType(a.type, b.type); @@ -1032,7 +1043,7 @@ export class Registry implements IRegistry, IDisposable { private _compareVersions( a: IVersionContainer, b: IVersionContainer, - requirements: Registry.IRequirement[] + requirements: IJupyterEnvRequirement[] ): number { let versionPairs = requirements.map(req => { return [a[req.name], b[req.name]]; @@ -1113,10 +1124,7 @@ export class Registry implements IRegistry, IDisposable { private _discoveredEnvironments: IPythonEnvironment[] = []; private _userSetEnvironments: IPythonEnvironment[] = []; private _defaultEnv: IPythonEnvironment; - private _condaRootPath: string; - private _systemPythonPath: string; private _registryBuilt: Promise; - private _requirements: Registry.IRequirement[]; private _disposing: boolean = false; private _environmentListUpdated = new Signal(this); } @@ -1127,33 +1135,6 @@ export namespace Registry { * in the registry. Each requirement should correspond to a python module that is also * executable via the '-m ' interface */ - export interface IRequirement { - /** - * The display name for the requirement - */ - name: string; - /** - * The actual module name that will be used with the python executable - */ - moduleName: string; - /** - * List of extra commands that will produce a version number for checking - */ - commands: string[]; - /** - * The Range of acceptable version produced by the previous commands field - */ - versionRange: semver.Range; - - /** - * pip install command - */ - pipCommand: string; - /** - * conda install command - */ - condaCommand: string; - } export const COMMON_CONDA_LOCATIONS = [ join(getUserHomeDir(), 'anaconda3'), diff --git a/src/main/server.ts b/src/main/server.ts index fcfde8fb..6b6f592a 100644 --- a/src/main/server.ts +++ b/src/main/server.ts @@ -1,6 +1,6 @@ import { ChildProcess, execFile } from 'child_process'; import { IRegistry, SERVER_TOKEN_PREFIX } from './registry'; -import { dialog } from 'electron'; +import { dialog, ipcMain } from 'electron'; import { ArrayExt } from '@lumino/algorithm'; import log from 'electron-log'; import * as fs from 'fs'; @@ -25,13 +25,16 @@ import { WorkspaceSettings } from './config/settings'; import { randomBytes } from 'crypto'; +import { condaEnvPathForCondaExePath, getCondaPath } from './env'; +import { EventTypeMain } from './eventtypes'; +import { ManagePythonEnvironmentDialog } from './pythonenvdialog/pythonenvdialog'; const SERVER_LAUNCH_TIMEOUT = 30000; // milliseconds const SERVER_RESTART_LIMIT = 3; // max server restarts function createLaunchScript( serverInfo: JupyterServer.IInfo, - baseCondaPath: string, + baseCondaEnvPath: string, port: number, token: string ): string { @@ -82,7 +85,7 @@ function createLaunchScript( isBaseCondaActivate = false; } else { condaActivatePath = path.join( - baseCondaPath, + baseCondaEnvPath, 'condabin', 'activate.bat' ); @@ -93,9 +96,9 @@ function createLaunchScript( condaActivatePath = envActivatePath; isBaseCondaActivate = false; } else { - condaActivatePath = path.join(baseCondaPath, 'bin', 'activate'); + condaActivatePath = path.join(baseCondaEnvPath, 'bin', 'activate'); condaShellScriptPath = path.join( - baseCondaPath, + baseCondaEnvPath, 'etc', 'profile.d', 'conda.sh' @@ -108,9 +111,15 @@ function createLaunchScript( if (isWin) { if (isConda) { + const parentDir = path.dirname(condaActivatePath); + const activateName = path.basename(condaActivatePath); + // server launch sometimes fails if activate.bat is called directly. + // so, cd into activate directory then back to working directory script = ` - CALL ${condaActivatePath} + CALL cd /d "${parentDir}" + CALL ${activateName} ${isBaseCondaActivate ? `CALL conda activate ${envPath}` : ''} + CALL cd /d "${serverInfo.workingDirectory}" CALL ${launchCmd}`; } else { script = ` @@ -177,13 +186,12 @@ export async function waitUntilServerIsUp(url: URL): Promise { } export class JupyterServer { - constructor(options: JupyterServer.IOptions, registry: IRegistry) { + constructor(options: JupyterServer.IOptions) { this._options = options; this._info.environment = options.environment; const workingDir = this._options.workingDirectory || userSettings.resolvedWorkingDirectory; this._info.workingDirectory = workingDir; - this._registry = registry; const wsSettings = new WorkspaceSettings(workingDir); this._info.serverArgs = wsSettings.getValue(SettingType.serverArgs); @@ -224,58 +232,39 @@ export class JupyterServer { `http://localhost:${this._info.port}/lab?token=${this._info.token}` ); - let baseCondaPath: string = ''; + let baseCondaEnvPath: string = ''; if (this._info.environment.type === IEnvironmentType.CondaRoot) { - baseCondaPath = getEnvironmentPath(this._info.environment); + baseCondaEnvPath = getEnvironmentPath(this._info.environment); } else if (this._info.environment.type === IEnvironmentType.CondaEnv) { - baseCondaPath = await this._registry.condaRootPath; + const condaPath = getCondaPath(); - if (!baseCondaPath) { + if (!condaPath) { const choice = dialog.showMessageBoxSync({ - message: 'Select conda base environment', + message: 'conda not found', detail: - 'Base conda environment not found. Please select a root conda environment to activate the custom environment.', + 'conda executable not found. Please set conda path in settings to use the conda sub environment.', type: 'error', - buttons: ['OK', 'Cancel'], - defaultId: 0, - cancelId: 1 + buttons: ['OK'], + defaultId: 0 }); - if (choice == 1) { - reject('Failed to activate conda environment'); - return; - } - - const filePaths = dialog.showOpenDialogSync({ - properties: [ - 'openDirectory', - 'showHiddenFiles', - 'noResolveAliases' - ], - buttonLabel: 'Use Conda Root' - }); - - if (filePaths && filePaths.length > 0) { - baseCondaPath = filePaths[0]; - if ( - !this._registry.validateCondaBaseEnvironmentAtPath( - baseCondaPath - ) - ) { - reject('Invalid base conda environment'); - return; - } - this._registry.setCondaRootPath(baseCondaPath); - } else { - reject('Failed to activate conda environment'); + if (choice == 0) { + ipcMain.emit( + EventTypeMain.ShowManagePythonEnvironmentsDialog, + undefined /*event*/, + ManagePythonEnvironmentDialog.Tab.Settings + ); + reject(`Error: conda executable not found.`); return; } } + + baseCondaEnvPath = condaEnvPathForCondaExePath(condaPath); } const launchScriptPath = createLaunchScript( this._info, - baseCondaPath, + baseCondaEnvPath, this._info.port, this._info.token ); @@ -527,7 +516,6 @@ export class JupyterServer { serverEnvVars: {}, version: null }; - private _registry: IRegistry; private _stopping: boolean = false; private _restartCount: number = 0; } @@ -604,6 +592,15 @@ export interface IServerFactory { * @return a promise that is fulfilled when all servers are killed. */ killAllServers: () => Promise; + + /** + * Check if any server was launched using the environment. + * + * @param pythonPath Python path for the environment. + * + * @return true if environment at pythonPath is in use. + */ + isEnvironmentInUse(pythonPath: string): boolean; } export namespace IServerFactory { @@ -749,6 +746,14 @@ export class JupyterServerFactory implements IServerFactory, IDisposable { return Promise.all(stopPromises); } + isEnvironmentInUse(pythonPath: string): boolean { + return ( + this._servers.find(server => { + return server.server.info.environment.path === pythonPath; + }) !== undefined + ); + } + dispose(): Promise { if (this._disposePromise) { return this._disposePromise; @@ -770,7 +775,7 @@ export class JupyterServerFactory implements IServerFactory, IDisposable { ): JupyterServerFactory.IFactoryItem { let item: JupyterServerFactory.IFactoryItem = { factoryId: this._nextId++, - server: new JupyterServer(opts, this._registry), + server: new JupyterServer(opts), closing: null, used: false }; diff --git a/src/main/sessionwindow/sessionwindow.ts b/src/main/sessionwindow/sessionwindow.ts index 1341684f..eb6ba62f 100644 --- a/src/main/sessionwindow/sessionwindow.ts +++ b/src/main/sessionwindow/sessionwindow.ts @@ -48,7 +48,7 @@ import { SessionConfig } from '../config/sessionconfig'; import { ISignal, Signal } from '@lumino/signaling'; import { EventTypeMain } from '../eventtypes'; import { EventManager } from '../eventmanager'; -import { runCommandInEnvironment } from '../cli'; +import { runCommandInEnvironment } from '../env'; export enum ContentViewType { Welcome = 'welcome', @@ -71,7 +71,7 @@ export interface IServerInfo { } const titleBarHeight = 29; -const defaultEnvSelectPopupHeight = 300; +const defaultEnvSelectPopupHeight = 330; export class SessionWindow implements IDisposable { constructor(options: SessionWindow.IOptions) { @@ -285,10 +285,6 @@ export class SessionWindow implements IDisposable { this._recentSessionsChangedHandler, this ); - this._registry.environmentListUpdated.disconnect( - this._onEnvironmentListUpdated, - this - ); this._disposeSession().then(() => { this._disposePromise = null; @@ -453,11 +449,6 @@ export class SessionWindow implements IDisposable { this ); - this._registry.environmentListUpdated.connect( - this._onEnvironmentListUpdated, - this - ); - this._window.on('close', async () => { await this.dispose(); }); @@ -540,7 +531,7 @@ export class SessionWindow implements IDisposable { Install / update Python environment using the bundled installer `, false @@ -780,7 +771,7 @@ export class SessionWindow implements IDisposable { }); this._evm.registerEventHandler( - EventTypeMain.SetPythonPath, + EventTypeMain.SetSessionPythonPath, async (event, path) => { if (event.sender !== this._envSelectPopup?.view?.view?.webContents) { return; @@ -868,6 +859,12 @@ export class SessionWindow implements IDisposable { this._app.showSettingsDialog(); } }, + { + label: 'Manage Python environments', + click: () => { + this._app.showManagePythonEnvsDialog(); + } + }, { label: 'Check for updates…', click: () => { @@ -948,6 +945,70 @@ export class SessionWindow implements IDisposable { } } ); + + this._evm.registerEventHandler( + EventTypeMain.RestartSession, + async event => { + if (event.sender !== this._envSelectPopup?.view?.view?.webContents) { + return; + } + + let currentPythonPath = this._wsSettings.getValue( + SettingType.pythonPath + ); + if (!currentPythonPath) { + const defaultEnv = await this.registry.getDefaultEnvironment(); + if (defaultEnv) { + currentPythonPath = defaultEnv.path; + } + } + + if (!currentPythonPath) { + return; + } + + this._hideEnvSelectPopup(); + this._restartServerInPythonEnvironment(currentPythonPath); + } + ); + + this._evm.registerEventHandler( + EventTypeMain.CopySessionInfoToClipboard, + event => { + if (event.sender !== this._envSelectPopup?.view?.view?.webContents) { + return; + } + + const serverInfo = this.getServerInfo(); + + let content = ''; + + if (serverInfo.type === 'local') { + content = [ + 'type: local server', + `url: ${serverInfo.url}`, + `server root: ${serverInfo.workingDirectory}`, + `env name: ${serverInfo.environment.name}`, + `env Python path: ${serverInfo.environment.path}`, + `versions: ${JSON.stringify(serverInfo.environment.versions)}` + ].join('\n'); + } else { + const url = new URL(serverInfo.url); + const isLocalUrl = + url.hostname === 'localhost' || url.hostname === '127.0.0.1'; + + content = [ + `type: ${isLocalUrl ? 'local' : 'remote'} connection`, + `url: ${serverInfo.url}`, + `session data: ${ + serverInfo.persistSessionData ? '' : 'not ' + }persisted` + ].join('\n'); + } + + clipboard.writeText(content); + } + ); } private _restartServerInPythonEnvironment(pythonPath: string) { @@ -979,7 +1040,7 @@ export class SessionWindow implements IDisposable { } } - async getServerInfo(): Promise { + getServerInfo(): IServerInfo { if (this._contentViewType !== ContentViewType.Lab) { return null; } @@ -1116,17 +1177,33 @@ export class SessionWindow implements IDisposable { } private async _createEnvSelectPopup() { - const envs = await this.registry.getEnvironmentList(false); - const defaultEnv = await this._registry.getDefaultEnvironment(); + let defaultEnv: IPythonEnvironment; + try { + defaultEnv = await this._registry.getDefaultEnvironment(); + } catch (error) { + // + } + const defaultPythonPath = defaultEnv ? defaultEnv.path : ''; this._envSelectPopup = new PythonEnvironmentSelectPopup({ + app: this._app, isDarkTheme: this._isDarkTheme, - envs, + envs: [], // start with empty list, populate later bundledPythonPath: getBundledPythonPath(), defaultPythonPath }); + const listPromise = this.registry.getEnvironmentList(false); + + this._envSelectPopup.view.view.webContents.on('did-finish-load', () => { + listPromise.then(envs => { + this._envSelectPopup.setPythonEnvironmentList(envs); + this._resizeEnvSelectPopup(); + this._envSelectPopup.resetView(); + }); + }); + this._envSelectPopup.load(); } @@ -1135,15 +1212,9 @@ export class SessionWindow implements IDisposable { return; } - let currentPythonPath = this._wsSettings.getValue(SettingType.pythonPath); - if (!currentPythonPath) { - const defaultEnv = await this.registry.getDefaultEnvironment(); - if (defaultEnv) { - currentPythonPath = defaultEnv.path; - } - } - - this._envSelectPopup.setCurrentPythonPath(currentPythonPath); + const serverInfo = this.getServerInfo(); + this._envSelectPopup.setCurrentPythonPath(serverInfo?.environment?.path); + this._envSelectPopup.resetView(); this._window.addBrowserView(this._envSelectPopup.view.view); this._envSelectPopupVisible = true; @@ -1510,13 +1581,6 @@ export class SessionWindow implements IDisposable { } } - private _onEnvironmentListUpdated() { - // TODO: add ability to update popup's env list - // recreate env select popup to have newly added env listed - this._hideEnvSelectPopup(); - this._createEnvSelectPopup(); - } - private _wsSettings: WorkspaceSettings; private _isDarkTheme: boolean; private _sessionConfig: SessionConfig | undefined; diff --git a/src/main/settingsdialog/preload.ts b/src/main/settingsdialog/preload.ts index c90b356b..2d3a0571 100644 --- a/src/main/settingsdialog/preload.ts +++ b/src/main/settingsdialog/preload.ts @@ -2,12 +2,8 @@ import { EventTypeMain, EventTypeRenderer } from '../eventtypes'; const { contextBridge, ipcRenderer } = require('electron'); -type InstallBundledPythonEnvStatusListener = (status: string) => void; -type CustomPythonPathSelectedListener = (path: string) => void; type WorkingDirectorySelectedListener = (path: string) => void; -let onInstallBundledPythonEnvStatusListener: InstallBundledPythonEnvStatusListener; -let onCustomPythonPathSelectedListener: CustomPythonPathSelectedListener; let onWorkingDirectorySelectedListener: WorkingDirectorySelectedListener; contextBridge.exposeInMainWorld('electronAPI', { @@ -58,32 +54,6 @@ contextBridge.exposeInMainWorld('electronAPI', { setDefaultWorkingDirectory: (path: string) => { ipcRenderer.send(EventTypeMain.SetDefaultWorkingDirectory, path); }, - installBundledPythonEnv: () => { - ipcRenderer.send(EventTypeMain.InstallBundledPythonEnv); - }, - updateBundledPythonEnv: () => { - ipcRenderer.send(EventTypeMain.InstallBundledPythonEnv); - }, - onInstallBundledPythonEnvStatus: ( - callback: InstallBundledPythonEnvStatusListener - ) => { - onInstallBundledPythonEnvStatusListener = callback; - }, - selectPythonPath: () => { - ipcRenderer.send(EventTypeMain.SelectPythonPath); - }, - onCustomPythonPathSelected: (callback: CustomPythonPathSelectedListener) => { - onCustomPythonPathSelectedListener = callback; - }, - setDefaultPythonPath: (path: string) => { - ipcRenderer.send(EventTypeMain.SetDefaultPythonPath, path); - }, - validatePythonPath: (path: string) => { - return ipcRenderer.invoke(EventTypeMain.ValidatePythonPath, path); - }, - showInvalidPythonPathMessage: (path: string) => { - ipcRenderer.send(EventTypeMain.ShowInvalidPythonPathMessage, path); - }, clearHistory: (options: any) => { return ipcRenderer.invoke(EventTypeMain.ClearHistory, options); }, @@ -114,19 +84,4 @@ ipcRenderer.on(EventTypeRenderer.WorkingDirectorySelected, (event, path) => { } }); -ipcRenderer.on( - EventTypeRenderer.InstallBundledPythonEnvStatus, - (event, result) => { - if (onInstallBundledPythonEnvStatusListener) { - onInstallBundledPythonEnvStatusListener(result); - } - } -); - -ipcRenderer.on(EventTypeRenderer.CustomPythonPathSelected, (event, path) => { - if (onCustomPythonPathSelectedListener) { - onCustomPythonPathSelectedListener(path); - } -}); - export {}; diff --git a/src/main/settingsdialog/settingsdialog.ts b/src/main/settingsdialog/settingsdialog.ts index da57ffda..f535f926 100644 --- a/src/main/settingsdialog/settingsdialog.ts +++ b/src/main/settingsdialog/settingsdialog.ts @@ -2,9 +2,8 @@ // Distributed under the terms of the Modified BSD License. import * as ejs from 'ejs'; -import { app, BrowserWindow } from 'electron'; +import { BrowserWindow } from 'electron'; import * as path from 'path'; -import * as fs from 'fs'; import { ThemedWindow } from '../dialog/themedwindow'; import { CtrlWBehavior, @@ -15,7 +14,6 @@ import { StartupMode, ThemeType } from '../config/settings'; -import { getBundledPythonPath, versionWithoutSuffix } from '../utils'; import { IRegistry } from '../registry'; export class SettingsDialog { @@ -44,40 +42,6 @@ export class SettingsDialog { const installUpdatesAutomaticallyEnabled = process.platform === 'darwin'; const installUpdatesAutomatically = installUpdatesAutomaticallyEnabled && options.installUpdatesAutomatically; - let defaultPythonPath = options.defaultPythonPath; - const bundledPythonPath = getBundledPythonPath(); - - if (defaultPythonPath === '') { - defaultPythonPath = bundledPythonPath; - } - let bundledEnvInstallationExists = false; - try { - bundledEnvInstallationExists = fs.existsSync(bundledPythonPath); - } catch (error) { - console.error('Failed to check for bundled Python path', error); - } - - const selectBundledPythonPath = - (defaultPythonPath === '' || defaultPythonPath === bundledPythonPath) && - bundledEnvInstallationExists; - - let bundledEnvInstallationLatest = true; - - if (bundledEnvInstallationExists) { - try { - const bundledEnv = registry.getEnvironmentByPath(bundledPythonPath); - const jlabVersion = bundledEnv.versions['jupyterlab']; - const appVersion = app.getVersion(); - - if ( - versionWithoutSuffix(jlabVersion) !== versionWithoutSuffix(appVersion) - ) { - bundledEnvInstallationLatest = false; - } - } catch (error) { - console.error('Failed to check bundled environment update', error); - } - } let strServerEnvVars = ''; if (Object.keys(serverEnvVars).length > 0) { @@ -102,47 +66,12 @@ export class SettingsDialog { flex-grow: 1; overflow-y: auto; } - #categories { - width: 200px; - } - #category-content-container { - flex-grow: 1; - } - .category-content { - display: flex; - flex-direction: column; - } #footer { text-align: right; } - #category-jupyterlab jp-divider { - margin: 15px 0; - } - #server-config-section { - display: flex; - flex-direction: column; - align-items: flex-start; - } - jp-tab-panel #tab-updates { - display: flex; - align-items: flex-start; - } #category-tabs { width: 100%; } - #bundled-env-warning { - display: none; - align-items: center; - } - #bundled-env-warning.warning { - color: orange; - } - #install-bundled-env { - display: none; - } - #update-bundled-env { - display: none; - } .row { display: flex; align-items: center; @@ -242,28 +171,6 @@ export class SettingsDialog { -
-
- -
-
-
InstallUpdate
- - <%= !bundledEnvInstallationExists ? 'disabled' : '' %> onchange="handleEnvTypeChange(this);">Bundled Python environment - onchange="handleEnvTypeChange(this);">Custom Python environment - - -
-
- -
-
- Select Python path -
-
-
-
-
Additional JupyterLab Server launch args @@ -285,15 +192,6 @@ export class SettingsDialog { @@ -604,20 +424,6 @@ export class SettingsDialog { const ctrlWBehavior = document.querySelector('jp-radio[name="ctrl-w-behavior"].checked').value; window.electronAPI.setCtrlWBehavior(ctrlWBehavior); - if (defaultPythonEnvChanged) { - if (bundledEnvRadio.checked) { - window.electronAPI.setDefaultPythonPath(''); - } else { - window.electronAPI.validatePythonPath(pythonPathInput.value).then((valid) => { - if (valid) { - window.electronAPI.setDefaultPythonPath(pythonPathInput.value); - } else { - window.electronAPI.showInvalidPythonPathMessage(pythonPathInput.value); - } - }); - } - } - window.electronAPI.restartApp(); } @@ -642,10 +448,6 @@ export class SettingsDialog { installUpdatesAutomaticallyEnabled, installUpdatesAutomatically, defaultWorkingDirectory, - defaultPythonPath, - selectBundledPythonPath, - bundledEnvInstallationExists, - bundledEnvInstallationLatest, logLevel, serverArgs, overrideDefaultServerArgs, @@ -682,7 +484,6 @@ export namespace SettingsDialog { checkForUpdatesAutomatically: boolean; installUpdatesAutomatically: boolean; defaultWorkingDirectory: string; - defaultPythonPath: string; activateTab?: Tab; logLevel: LogLevel; serverArgs: string; diff --git a/src/main/tokens.ts b/src/main/tokens.ts index 7bc37bd0..667010ec 100644 --- a/src/main/tokens.ts +++ b/src/main/tokens.ts @@ -66,6 +66,7 @@ export interface IPythonEnvironment { export enum PythonEnvResolveErrorType { PathNotFound = 'path-not-found', + InvalidPythonBinary = 'invalid-python-binary', ResolveError = 'resolve-error', RequirementsNotSatisfied = 'requirements-not-satisfied' } @@ -75,6 +76,11 @@ export interface IPythonEnvResolveError { message?: string; } +export interface IPythonEnvValidateResult { + valid: boolean; + error?: IPythonEnvResolveError; +} + export interface IDisposable { dispose(): Promise; } diff --git a/src/main/utils.ts b/src/main/utils.ts index 9cb52cff..70518578 100644 --- a/src/main/utils.ts +++ b/src/main/utils.ts @@ -10,7 +10,7 @@ import log from 'electron-log'; import { AddressInfo, createServer, Socket } from 'net'; import { app, nativeTheme } from 'electron'; import { IPythonEnvironment } from './tokens'; -import { exec } from 'child_process'; +import { exec, execFile, ExecFileOptions, execFileSync } from 'child_process'; export const DarkThemeBGColor = '#212121'; export const LightThemeBGColor = '#ffffff'; @@ -55,10 +55,22 @@ export function getSchemasDir(): string { return path.normalize(path.join(getAppDir(), './build/schemas')); } +export function getRelativePathToUserHome(absolutePath: string): string { + const home = getUserHomeDir(); + if (absolutePath.startsWith(home)) { + return `~${path.sep}${path.relative(home, absolutePath)}`; + } +} + export function getEnvironmentPath(environment: IPythonEnvironment): string { return envPathForPythonPath(environment.path); } +export function getBundledEnvInstallerPath(): string { + const appDir = getAppDir(); + return path.join(appDir, 'env_installer', 'jlab_server.tar.gz'); +} + export function getBundledPythonInstallDir(): string { // this directory path cannot have any spaces since // conda environments cannot be installed to such paths @@ -78,11 +90,6 @@ export function getBundledPythonInstallDir(): string { return installDir; } -// user data dir for<= 3.5.1-1 -export function getOldUserConfigPath() { - return path.join(getBundledPythonInstallDir(), 'jupyterlab-desktop-data'); -} - export function getBundledPythonEnvPath(): string { const userDataDir = getBundledPythonInstallDir(); @@ -225,13 +232,22 @@ export function versionWithoutSuffix(version: string) { export enum EnvironmentInstallStatus { Started = 'STARTED', + Running = 'RUNNING', Failure = 'FAILURE', Cancelled = 'CANCELLED', Success = 'SUCCESS', RemovingExistingInstallation = 'REMOVING_EXISTING_INSTALLATION' } -export interface IBundledEnvironmentInstallListener { +export enum EnvironmentDeleteStatus { + Started = 'STARTED', + Running = 'RUNNING', + Failure = 'FAILURE', + Cancelled = 'CANCELLED', + Success = 'SUCCESS' +} + +export interface ICondaPackEnvironmentInstallListener { onInstallStatus: (status: EnvironmentInstallStatus, message?: string) => void; forceOverwrite?: boolean; confirmOverwrite?: () => Promise; @@ -239,18 +255,23 @@ export interface IBundledEnvironmentInstallListener { export async function installBundledEnvironment( installPath: string, - listener?: IBundledEnvironmentInstallListener + listener?: ICondaPackEnvironmentInstallListener ): Promise { + const condaPackPath = getBundledEnvInstallerPath(); + + return installCondaPackEnvironment(condaPackPath, installPath, listener); +} + +export async function installCondaPackEnvironment( + condaPackPath: string, + installPath: string, + listener?: ICondaPackEnvironmentInstallListener +): Promise { + const isBundledInstaller = condaPackPath === getBundledEnvInstallerPath(); // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { const platform = process.platform; const isWin = platform === 'win32'; - const appDir = getAppDir(); - const installerPath = path.join( - appDir, - 'env_installer', - 'jlab_server.tar.gz' - ); installPath = installPath || getBundledPythonEnvPath(); if (fs.existsSync(installPath)) { @@ -279,7 +300,7 @@ export async function installBundledEnvironment( try { fs.mkdirSync(installPath, { recursive: true }); - await tar.x({ C: installPath, file: installerPath }); + await tar.x({ C: installPath, file: condaPackPath }); } catch (error) { listener?.onInstallStatus( EnvironmentInstallStatus.Failure, @@ -290,13 +311,18 @@ export async function installBundledEnvironment( return; } - markEnvironmentAsJupyterInstalled(installPath); + markEnvironmentAsJupyterInstalled(installPath, { + type: 'conda', + source: 'bundled-installer', + appVersion: app.getVersion() + }); let unpackCommand = isWin ? `${installPath}\\Scripts\\activate.bat && conda-unpack` : `source "${installPath}/bin/activate" && conda-unpack`; - if (platform === 'darwin') { + // only unsign when installing from bundled installer + if (platform === 'darwin' && isBundledInstaller) { unpackCommand = `${createUnsignScriptInEnv( installPath )}\n${unpackCommand}`; @@ -351,6 +377,38 @@ export function markEnvironmentAsJupyterInstalled( } } +export interface IEnvironmentDeleteListener { + onDeleteStatus: (status: EnvironmentDeleteStatus, message?: string) => void; +} + +export async function deletePythonEnvironment( + envPath: string, + listener?: IEnvironmentDeleteListener +): Promise { + return new Promise((resolve, reject) => { + if (!isEnvInstalledByDesktopApp(envPath)) { + listener?.onDeleteStatus( + EnvironmentDeleteStatus.Failure, + 'Environment cannot be deleted since it was not installed by JupyterLab Desktop.' + ); + reject(); + } + + try { + listener?.onDeleteStatus( + EnvironmentDeleteStatus.Running, + 'Deleting environment files...' + ); + fs.rmSync(envPath, { recursive: true, force: true }); + listener?.onDeleteStatus(EnvironmentDeleteStatus.Success); + resolve(true); + } catch (error) { + listener?.onDeleteStatus(EnvironmentDeleteStatus.Failure, error.message); + reject(); + } + }); +} + export function createTempFile( fileName = 'temp', data = '', @@ -404,10 +462,15 @@ export function jupyterEnvInstallInfoPathForEnvPath(envPath: string) { return path.join(envPath, '.jupyter', 'env.json'); } +export function isEnvInstalledByDesktopApp(envPath: string) { + return fs.existsSync(jupyterEnvInstallInfoPathForEnvPath(envPath)); +} + export function isCondaEnv(envPath: string): boolean { return fs.existsSync(path.join(envPath, 'conda-meta')); } +// TODO: sync with condaExePathForEnvPath export function isBaseCondaEnv(envPath: string): boolean { const isWin = process.platform === 'win32'; const condaBinPath = path.join( @@ -420,7 +483,7 @@ export function isBaseCondaEnv(envPath: string): boolean { export function createCommandScriptInEnv( envPath: string, - baseCondaPath: string, + baseCondaEnvPath: string, command?: string, joinStr?: string ): string { @@ -454,9 +517,9 @@ export function createCommandScriptInEnv( // conda-packed environments let isBaseCondaActivate = false; if (!hasActivate && isConda) { - if (fs.existsSync(baseCondaPath)) { - activatePath = activatePathForEnvPath(baseCondaPath); - condaSourcePath = condaSourcePathForEnvPath(baseCondaPath); + if (fs.existsSync(baseCondaEnvPath)) { + activatePath = activatePathForEnvPath(baseCondaEnvPath); + condaSourcePath = condaSourcePathForEnvPath(baseCondaEnvPath); hasActivate = fs.existsSync(activatePath); isBaseCondaActivate = true; } @@ -492,6 +555,45 @@ export function createCommandScriptInEnv( return scriptLines.join(joinStr); } +export function getBinarySignList(envPath: string) { + const { isBinary } = require('istextorbinary'); + const envBinDir = path.join(envPath, 'bin'); + + const needsSigning = (filePath: string) => { + // conly consider bin directory, and .so, .dylib files in other directories + if ( + filePath.startsWith(envBinDir) || + filePath.endsWith('.so') || + filePath.endsWith('.dylib') + ) { + // check for binary content + return isBinary(null, fs.readFileSync(filePath)); + } + + return false; + }; + + const findBinariesInDirectory = (dirPath: string): string[] => { + let results: string[] = []; + const list = fs.readdirSync(dirPath); + list.forEach(filePath => { + filePath = dirPath + '/' + filePath; + const stat = fs.lstatSync(filePath); + if (stat && stat.isDirectory()) { + results = results.concat(findBinariesInDirectory(filePath)); + } else { + if (!stat.isSymbolicLink() && needsSigning(filePath)) { + results.push(path.relative(envPath, filePath)); + } + } + }); + + return results; + }; + + return findBinariesInDirectory(envPath); +} + /* signed tarball contents need to be unsigned except for python binary, otherwise server runs into issues at runtime. python binary comes originally @@ -536,3 +638,105 @@ export function getLogFilePath(processType: 'main' | 'renderer' = 'main') { ); } } + +export async function runCommand( + executablePath: string, + commands: string[], + options?: ExecFileOptions +): Promise { + return new Promise((resolve, reject) => { + execFile(executablePath, commands, options, (error, stdout, stderr) => { + if (error) { + reject(error); + } + if (stdout) { + resolve(stdout); + } else if (stderr) { + resolve(stderr); + } else { + reject( + new Error( + `"${executablePath} ${commands.join( + ' ' + )}" produced no output to stdout or stderr!` + ) + ); + } + }); + }); +} + +export function runCommandSync( + executablePath: string, + commands: string[], + options?: ExecFileOptions +): string { + return execFileSync(executablePath, commands, options).toString(); +} + +export function openDirectoryInExplorer(dirPath: string): boolean { + if (!(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory())) { + return false; + } + + const { platform } = process; + const openCommand = + platform === 'darwin' + ? 'open' + : platform === 'win32' + ? 'explorer' + : 'xdg-open'; + + exec(`${openCommand} "${dirPath}"`); + + return true; +} + +export function launchTerminalInDirectory( + dirPath: string, + commands?: string +): boolean { + if (!(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory())) { + return false; + } + + const { platform } = process; + if (platform === 'darwin') { + let callCommands = ''; + if (commands) { + // replace " with ' + commands = commands.split('"').join("'"); + callCommands = `&& ${commands}`; + } + + exec( + `osascript -e 'tell application "Terminal" to do script "cd '${dirPath}' ${callCommands}"' -e 'tell application "Terminal" to activate'` + ); + ``; + } else if (platform === 'win32') { + if (commands) { + const activateFilePath = createTempFile( + `activate.bat`, + `cd /D "${dirPath}"\n${commands}` + ); + + exec(`start cmd.exe /K ${activateFilePath}`); + + setTimeout(() => { + try { + fs.unlinkSync(activateFilePath); + } catch (error) { + console.error('Failed to delete the temp file'); + } + }, 2000); + } else { + exec(`start cmd.exe /K cd /D "${dirPath}"`); + } + } else { + let callCommands = ''; + if (commands) { + callCommands = ` -- bash -c "${commands}; exec bash"`; + } + exec(`gnome-terminal --working-directory="${dirPath}"${callCommands}`); + } +} diff --git a/src/main/welcomeview/preload.ts b/src/main/welcomeview/preload.ts index 04be86e6..2e84a736 100644 --- a/src/main/welcomeview/preload.ts +++ b/src/main/welcomeview/preload.ts @@ -11,7 +11,7 @@ type SetNotificationMessageListener = ( message: string, closable: boolean ) => void; -type DisableLocalServerActionsListener = () => void; +type EnableLocalServerActionsListener = (enable: boolean) => void; type InstallBundledPythonEnvStatusListener = ( status: string, message: string @@ -20,7 +20,7 @@ type InstallBundledPythonEnvStatusListener = ( let onSetRecentSessionListListener: SetRecentSessionListListener; let onSetNewsListListener: SetNewsListListener; let onSetNotificationMessageListener: SetNotificationMessageListener; -let onDisableLocalServerActionsListener: DisableLocalServerActionsListener; +let onEnableLocalServerActionsListener: EnableLocalServerActionsListener; let onInstallBundledPythonEnvStatusListener: InstallBundledPythonEnvStatusListener; contextBridge.exposeInMainWorld('electronAPI', { @@ -71,10 +71,8 @@ contextBridge.exposeInMainWorld('electronAPI', { onSetNotificationMessage: (callback: SetNotificationMessageListener) => { onSetNotificationMessageListener = callback; }, - onDisableLocalServerActions: ( - callback: DisableLocalServerActionsListener - ) => { - onDisableLocalServerActionsListener = callback; + onEnableLocalServerActions: (callback: EnableLocalServerActionsListener) => { + onEnableLocalServerActionsListener = callback; }, onInstallBundledPythonEnvStatus: ( callback: InstallBundledPythonEnvStatusListener @@ -107,14 +105,14 @@ ipcRenderer.on( } ); -ipcRenderer.on(EventTypeRenderer.DisableLocalServerActions, event => { - if (onDisableLocalServerActionsListener) { - onDisableLocalServerActionsListener(); +ipcRenderer.on(EventTypeRenderer.EnableLocalServerActions, (event, enable) => { + if (onEnableLocalServerActionsListener) { + onEnableLocalServerActionsListener(enable); } }); ipcRenderer.on( - EventTypeRenderer.InstallBundledPythonEnvStatus, + EventTypeRenderer.InstallPythonEnvStatus, (event, result, message) => { if (onInstallBundledPythonEnvStatusListener) { onInstallBundledPythonEnvStatusListener(result, message); diff --git a/src/main/welcomeview/welcomeview.ts b/src/main/welcomeview/welcomeview.ts index 4e066927..177d3eb5 100644 --- a/src/main/welcomeview/welcomeview.ts +++ b/src/main/welcomeview/welcomeview.ts @@ -583,25 +583,33 @@ export class WelcomeView { function showNotificationPanel(message, closable) { notificationPanelMessage.innerHTML = message; - notificationPanel.style.display = "flex"; notificationPanelCloseButton.style.display = closable ? 'block' : 'none'; + notificationPanel.style.display = message === "" ? "none" : "flex"; } function closeNotificationPanel() { notificationPanel.style.display = "none"; } - function disableLocalServerActions() { + function enableLocalServerActions(enable) { const serverActionIds = ["new-notebook-link", "new-session-link", "open-file-or-folder-link", "open-file-link", "open-folder-link"]; serverActionIds.forEach(id => { const link = document.getElementById(id); if (link) { - link.classList.add("disabled"); + if (enable) { + link.classList.remove("disabled"); + } else { + link.classList.add("disabled"); + } } }); document.querySelectorAll('div.recent-item-local').forEach(link => { - link.classList.add("disabled"); + if (enable) { + link.classList.remove("disabled"); + } else { + link.classList.add("disabled"); + } }); } @@ -609,8 +617,8 @@ export class WelcomeView { showNotificationPanel(message, closable); }); - window.electronAPI.onDisableLocalServerActions(() => { - disableLocalServerActions(); + window.electronAPI.onEnableLocalServerActions((enable) => { + enableLocalServerActions(enable); }); window.electronAPI.onInstallBundledPythonEnvStatus((status, detail) => { @@ -620,7 +628,7 @@ export class WelcomeView { 'Installation cancelled!' : status === 'FAILURE' ? 'Failed to install!' : - status === 'SUCCESS' ? 'Installation succeeded. Restarting now...' : ''; + status === 'SUCCESS' ? 'Installation succeeded.' : ''; if (detail) { message += \`[\$\{detail\}]\`; } @@ -629,7 +637,7 @@ export class WelcomeView { if (status === 'SUCCESS') { setTimeout(() => { - sendMessageToMain('restart-app'); + showNotificationPanel('', true); }, 2000); } }); @@ -660,25 +668,26 @@ export class WelcomeView { this._updateNewsList(); } - this._registry.getDefaultEnvironment().catch(() => { - this.disableLocalServerActions(); - this.showNotification( - ` -
- - - -
- Python environment not found. Install using the bundled installer or Change the default Python environment - `, - true + this._registry.environmentListUpdated.connect( + this._onEnvironmentListUpdated, + this + ); + + this._view.webContents.on('destroyed', () => { + this._registry.environmentListUpdated.disconnect( + this._onEnvironmentListUpdated, + this ); }); + this._onEnvironmentListUpdated(); } - disableLocalServerActions() { + enableLocalServerActions(enable: boolean) { this._viewReady.then(() => { - this._view.webContents.send(EventTypeRenderer.DisableLocalServerActions); + this._view.webContents.send( + EventTypeRenderer.EnableLocalServerActions, + enable + ); }); } @@ -692,6 +701,29 @@ export class WelcomeView { }); } + private async _onEnvironmentListUpdated() { + this._registry + .getDefaultEnvironment() + .then(() => { + this.enableLocalServerActions(true); + this.showNotification('', false); + }) + .catch(() => { + this.enableLocalServerActions(false); + this.showNotification( + ` +
+ + + +
+ Python environment not found. Install using the bundled installer or Change the default Python environment + `, + true + ); + }); + } + private _updateNewsList() { if (WelcomeView._newsListFetched) { return; diff --git a/webpack.preload.js b/webpack.preload.js index 689cc376..c0a60f7c 100644 --- a/webpack.preload.js +++ b/webpack.preload.js @@ -14,6 +14,7 @@ const preloadFiles = [ 'labview/preload.js', 'settingsdialog/preload.js', 'progressview/preload.js', + 'pythonenvdialog/preload.js', 'pythonenvselectpopup/preload.js', 'remoteserverselectdialog/preload.js', 'titlebarview/preload.js', diff --git a/yarn.lock b/yarn.lock index c2f7d2e3..b20ba0c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -200,6 +200,11 @@ "@microsoft/fast-foundation" "^2.37.1" "@microsoft/fast-web-utilities" "^5.1.0" +"@leeoniya/ufuzzy@1.0.14": + version "1.0.14" + resolved "https://registry.yarnpkg.com/@leeoniya/ufuzzy/-/ufuzzy-1.0.14.tgz#01572c0de9cfa1420cf6ecac76dd59db5ebd1337" + integrity sha512-/xF4baYuCQMo+L/fMSUrZnibcu0BquEGnbxfVPiZhs/NbJeKj4c/UmFpQzW9Us0w45ui/yYW3vyaqawhNYsTzA== + "@lumino/algorithm@^1.9.2": version "1.9.2" resolved "https://registry.yarnpkg.com/@lumino/algorithm/-/algorithm-1.9.2.tgz#b95e6419aed58ff6b863a51bfb4add0f795141d3" @@ -2067,6 +2072,11 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + function.prototype.name@^1.1.5: version "1.1.5" resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" @@ -2150,7 +2160,7 @@ glob-to-regexp@^0.4.1: resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== -glob@^7.1.1, glob@^7.1.3, glob@^7.1.6: +glob@^7.0.0, glob@^7.1.1, glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -2301,6 +2311,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hasown@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.0.tgz#f4c513d454a57b7c7e1650778de226b11700546c" + integrity sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA== + dependencies: + function-bind "^1.1.2" + hosted-git-info@^2.1.4: version "2.8.9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" @@ -2436,6 +2453,11 @@ internal-slot@^1.0.3, internal-slot@^1.0.4: has "^1.0.3" side-channel "^1.0.4" +interpret@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" + integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== + interpret@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" @@ -2482,6 +2504,13 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + is-core-module@^2.5.0, is-core-module@^2.9.0: version "2.11.0" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" @@ -2971,7 +3000,7 @@ minimist-options@4.1.0, minimist-options@^4.0.2: is-plain-obj "^1.1.0" kind-of "^6.0.3" -minimist@^1.2.6: +minimist@^1.2.3, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -3520,6 +3549,13 @@ readdir-scoped-modules@^1.0.0: graceful-fs "^4.1.2" once "^1.3.0" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + rechoir@^0.7.0: version "0.7.1" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" @@ -3589,6 +3625,15 @@ resolve-from@^5.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== +resolve@^1.1.6: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + resolve@^1.10.0, resolve@^1.9.0: version "1.22.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" @@ -3768,6 +3813,23 @@ shell-path@^2.1.0: dependencies: shell-env "^0.3.0" +shelljs@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shx@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" + integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== + dependencies: + minimist "^1.2.3" + shelljs "^0.8.5" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"