diff --git a/.gitignore b/.gitignore index f037700b..22437b93 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,5 @@ *.pyc *.un~ /build/ -/deps/deps.jl -/deps/build.log -/deps/JUPYTER -/deps/julia-* *.jl.*.cov Manifest*.toml diff --git a/Project.toml b/Project.toml index 409337c9..57e818c0 100644 --- a/Project.toml +++ b/Project.toml @@ -11,6 +11,7 @@ Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +Preferences = "21216c6a-2e73-6563-6e65-726566657250" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" @@ -34,6 +35,7 @@ Logging = "1" Markdown = "1" Pkg = "1" PrecompileTools = "1.2.1" +Preferences = "1" Printf = "1" PythonCall = "0.9" REPL = "1" diff --git a/README.md b/README.md index 35df1678..fa9e5289 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -
IJulia logo
+
IJulia logo
[![](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaLang.github.io/IJulia.jl/stable) [![](https://img.shields.io/badge/docs-latest-blue.svg)](https://JuliaLang.github.io/IJulia.jl/dev) @@ -31,29 +31,20 @@ Install IJulia from the Julia REPL by pressing `]` to enter pkg mode and enterin add IJulia ``` -If you already have Python/Jupyter installed on your machine, this process will also install a -[kernel specification](https://jupyter-client.readthedocs.io/en/latest/kernels.html#kernelspecs) -that tells Jupyter how to launch Julia. You can then launch the notebook server the usual -way by running `jupyter notebook` in the terminal. - -Note that `IJulia` should generally be installed in Julia's global package environment, unless you -install a custom kernel that specifies a particular environment. - -Alternatively, you can have IJulia create and manage its own Python/Jupyter installation. -To do this, type the following in Julia, at the `julia>` prompt: +To launch the Jupyter notebook, type the following in Julia at the `julia>` prompt: ```julia using IJulia notebook() ``` -to launch the IJulia notebook in your browser. -The first time you run `notebook()`, it will prompt you -for whether it should install Jupyter. Hit enter to -have it use the [Conda.jl](https://github.com/Luthaf/Conda.jl) -package to install a minimal Python+Jupyter distribution (via -[Miniconda](https://www.anaconda.com/docs/getting-started/miniconda/install#quickstart-install-instructions)) that is -private to Julia (not in your `PATH`). +The first time you run `notebook()`, it will: +- Prompt you to install Jupyter if you don't already have it (hit enter to install via [Conda.jl](https://github.com/Luthaf/Conda.jl), which creates a minimal Python+Jupyter distribution private to Julia) +- Automatically install the Julia kernel for your current Julia version + +If you already have Jupyter installed and prefer to use it, you can launch it from the terminal with `jupyter notebook` instead. The Julia kernel will be automatically installed the first time you run `IJulia.notebook()` or `IJulia.jupyterlab()`. + +**Note:** IJulia should generally be installed in Julia's global package environment, unless you install a custom kernel that specifies a particular environment. For more advanced installation options, such as specifying a specific Jupyter installation to use, see the [documentation](https://JuliaLang.github.io/IJulia.jl/stable). diff --git a/deps/ijulialogo.png b/assets/ijulialogo.png similarity index 100% rename from deps/ijulialogo.png rename to assets/ijulialogo.png diff --git a/deps/logo-32x32.png b/assets/logo-32x32.png similarity index 100% rename from deps/logo-32x32.png rename to assets/logo-32x32.png diff --git a/deps/logo-64x64.png b/assets/logo-64x64.png similarity index 100% rename from deps/logo-64x64.png rename to assets/logo-64x64.png diff --git a/deps/logo-svg.svg b/assets/logo-svg.svg similarity index 100% rename from deps/logo-svg.svg rename to assets/logo-svg.svg diff --git a/deps/build.jl b/deps/build.jl deleted file mode 100644 index 59dd63af..00000000 --- a/deps/build.jl +++ /dev/null @@ -1,45 +0,0 @@ -using Conda - -include("kspec.jl") -if !haskey(ENV, "IJULIA_NODEFAULTKERNEL") - # Install Jupyter kernel-spec file. - kernelpath = installkernel("Julia", "--project=@.") -end - -# make it easier to get more debugging output by setting JULIA_DEBUG=1 -# when building. -IJULIA_DEBUG = lowercase(get(ENV, "IJULIA_DEBUG", "0")) -IJULIA_DEBUG = IJULIA_DEBUG in ("1", "true", "yes") - -# remember the user's Jupyter preference, if any; empty == Conda -prefsfile = joinpath(first(DEPOT_PATH), "prefs", "IJulia") -mkpath(dirname(prefsfile)) -jupyter = get(ENV, "JUPYTER", isfile(prefsfile) ? readchomp(prefsfile) : Sys.isunix() && !Sys.isapple() ? "jupyter" : "") -condajupyter = normpath(Conda.SCRIPTDIR, exe("jupyter")) -if isempty(jupyter) || dirname(jupyter) == abspath(Conda.SCRIPTDIR) - jupyter = condajupyter # will be installed if needed -elseif isabspath(jupyter) - if !Sys.isexecutable(jupyter) - @warn("ignoring non-executable JUPYTER=$jupyter") - jupyter = condajupyter - end -elseif jupyter != basename(jupyter) # relative path - @warn("ignoring relative path JUPYTER=$jupyter") - jupyter = condajupyter -elseif Sys.which(jupyter) === nothing - @warn("JUPYTER=$jupyter not found in PATH") -end - -function write_if_changed(filename, contents) - if !isfile(filename) || read(filename, String) != contents - write(filename, contents) - end -end - -# Install the deps.jl file: -deps = """ - const IJULIA_DEBUG = $(IJULIA_DEBUG) - const JUPYTER = $(repr(jupyter)) -""" -write_if_changed("deps.jl", deps) -write_if_changed(prefsfile, jupyter) diff --git a/docs/src/_changelog.md b/docs/src/_changelog.md index 61dc419e..edbe7429 100644 --- a/docs/src/_changelog.md +++ b/docs/src/_changelog.md @@ -7,6 +7,31 @@ CurrentModule = IJulia This documents notable changes in IJulia.jl. The format is based on [Keep a Changelog](https://keepachangelog.com). +## [Unreleased] + +### Changed +- Removed `Pkg.build("IJulia")` support. IJulia no longer uses a + build step for kernel installation. The build-time configuration system has + been replaced with runtime functions. Use [`installkernel()`](@ref) to install + or update kernels, and [`update_jupyter_path()`](@ref) to configure the + Jupyter executable path. +- IJulia now uses [Preferences.jl](https://github.com/JuliaPackaging/Preferences.jl) to + store configuration preferences (in `LocalPreferences.toml`) instead of Scratch.jl. +- Kernel auto-installation (when calling `notebook()` or `jupyterlab()`) now + checks if the **default** kernel for the current Julia version exists. + If not, it automatically installs it. This auto-installation can be disabled + by setting the `IJULIA_NODEFAULTKERNEL` environment variable. Note that + explicit calls to `installkernel()` always install/update a kernel for the + current Julia version, regardless of the environment variable. +- `ENV["IJULIA_DEBUG"]` now automatically enables verbose output for `notebook()` and + `jupyterlab()`, making it easier to debug Jupyter launch issues. + +### Added +- Added [`IJulia.update_jupyter_path()`](@ref) function to explicitly update the saved + Jupyter executable path preference. +- Added zero-argument [`installkernel()`](@ref) convenience method that installs + the default Julia kernel with `--project=@.`. + ## [v1.32.0] - 2025-11-04 ### Added @@ -18,6 +43,8 @@ Changelog](https://keepachangelog.com). patch release of Julia, but it does mean that IJulia will only create kernels for each Julia minor release instead of each patch release. + + ### Fixed - Fixed the display of `UnionAll` types such as `Pair.body` ([#1203]). - Fixed a bug in the PythonCall extension that would break opening comms from diff --git a/docs/src/library/public.md b/docs/src/library/public.md index be1fb021..170db84b 100644 --- a/docs/src/library/public.md +++ b/docs/src/library/public.md @@ -7,6 +7,7 @@ IJulia.IJulia IJulia.inited IJulia.installkernel +IJulia.update_jupyter_path ``` diff --git a/docs/src/manual/installation.md b/docs/src/manual/installation.md index 9707c447..21215318 100644 --- a/docs/src/manual/installation.md +++ b/docs/src/manual/installation.md @@ -15,17 +15,16 @@ ``` !!! info - This process installs a - [kernel specification](https://jupyter-client.readthedocs.io/en/latest/kernels.html#kernelspecs) - for IJulia. The IJulia kernelspec, shorthand for kernel specification, - contains the instructions for launching a Julia kernel that a notebook - frontend (Jupyter, JupyterLab, nteract) can use. The kernelspec does not - install the notebook frontend. + The Julia kernel will be automatically installed the first time you run + `IJulia.notebook()` or `IJulia.jupyterlab()`, or you can install it manually + by running `IJulia.installkernel()`. The kernel specification contains the + instructions for launching a Julia kernel that a notebook frontend + (Jupyter, JupyterLab, nteract) can use. IJulia respects the standard [`JUPYTER_DATA_DIR`](https://docs.jupyter.org/en/stable/use/jupyter-directories.html#data-files) - environment variable, so you can set that before installation if you want - the kernel to be installed in a specific location. + environment variable, so you can set that before installing the kernel if you want + it to be installed in a specific location. !!! warning The command, `Pkg.add("IJulia")`, does not install Jupyter @@ -34,19 +33,26 @@ You can install Jupyter Notebook by following the Notebook's installation instructions if you want. Conveniently, Jupyter Notebook can also be installed automatically when you run - `IJulia.notebook()`. + `IJulia.notebook()`. See [Running the Julia notebook](running.md#Running-the-IJulia-Notebook). - + You can direct `IJulia.notebook()` to use a specific Jupyter - installation by setting `ENV["JUPYTER"]` to the path of the - `jupyter` program executable. This environment variable should - be set before `Pkg.add` or before running `Pkg.build("IJulia")`, - and it will remember your preference for subsequent updates. + installation by passing the path directly to `IJulia.update_jupyter_path()`, + or by setting `ENV["JUPYTER"]` before calling it. This preference will be + remembered for subsequent updates. For example: + ```julia + # Option 1: Pass path directly + IJulia.update_jupyter_path("/usr/local/bin/jupyter") + + # Option 2: Set environment variable + ENV["JUPYTER"] = "/usr/local/bin/jupyter" + IJulia.update_jupyter_path() + ``` ## Updating Julia and IJulia Julia is improving rapidly, so it won't be long before you want to -update your packages or Julia to a more recent version. +update your packages or Julia to a more recent version. ### Update packages @@ -66,7 +72,8 @@ for the most recent Julia). If you're using juliaup to manage Julia, then for every Julia *minor release* (1.11, 1.12, etc) you will need to explicitly update the IJulia installation to tell Jupyter where to find the new Julia version: ```julia -Pkg.build("IJulia") +using IJulia +IJulia.installkernel() ``` This is because IJulia creates default kernels for every minor version if it @@ -77,10 +84,6 @@ installation every time you install a new Julia binary (or do anything that *changes the location of Julia* on your computer). -!!! important - `Pkg.build("IJulia")` **must** be run at the Julia command line. - It will error and fail if run within IJulia. - ## Installing and customizing kernels You may find it helpful to run multiple Julia kernels to support different Julia @@ -116,23 +119,27 @@ installkernel("Julia (4 threads)", env=Dict("JULIA_NUM_THREADS"=>"4")) The `env` keyword should be a `Dict` which maps environment variables to values. -To *prevent* IJulia from installing a default kernel when the package is built, -define the `IJULIA_NODEFAULTKERNEL` environment variable before adding or -building IJulia. +If you want to disable automatic installation of the default kernel (for example, if +you only want custom kernels), set the `IJULIA_NODEFAULTKERNEL` environment variable: -## Low-level IPython Installations +```julia +using IJulia -We recommend using IPython 7.15 or later as well as Python 3. +# Disable auto-installation of the default kernel +ENV["IJULIA_NODEFAULTKERNEL"] = "true" -### Using legacy IPython 2.x version +# Install custom kernels +IJulia.installkernel("Julia O3", "-O3") +IJulia.installkernel("Julia (4 threads)", env=Dict("JULIA_NUM_THREADS"=>"4")) +``` -We recognize that some users may need to use legacy IPython 2.x. You -can do this by checkout out the `ipython2` branch of the IJulia package: +With `IJULIA_NODEFAULTKERNEL` set, `IJulia.notebook()` will not auto-install the +default kernel. You can still manually install the default kernel by calling +`IJulia.installkernel()` without arguments. -```julia -Pkg.checkout("IJulia", "ipython2") -Pkg.build("IJulia") -``` +## Low-level IPython Installations + +We recommend using IPython 7.15 or later as well as Python 3. ### Manual installation of IPython @@ -165,8 +172,8 @@ Once IPython 3.0+ and Julia 0.7+ are installed, you can install IJulia from a Ju Pkg.add("IJulia") ``` -This will download IJulia and a few other prerequisites, and will set up a -Julia kernel for IPython. +This will download IJulia and a few other prerequisites. The Julia kernel will be +automatically installed the first time you run `IJulia.notebook()` or `IJulia.jupyterlab()`. If the command above returns an error, you may need to run `Pkg.update()`, then -retry it, or possibly run `Pkg.build("IJulia")` to force a rebuild. +retry it. If you need to reinstall the kernel, run `IJulia.installkernel()`. diff --git a/docs/src/manual/troubleshooting.md b/docs/src/manual/troubleshooting.md index f709a39b..0c77dd99 100644 --- a/docs/src/manual/troubleshooting.md +++ b/docs/src/manual/troubleshooting.md @@ -8,7 +8,7 @@ jupyter kernelspec list Or, explore the data directory relevant to your system, e.g., `~/.local/share/jupyter/kernels`. -Make sure that you can find a Julia kernel. If you can't, run [`IJulia.installkernel`](@ref), e.g., as `import IJulia; IJulia.installkernel("Julia", "--project=@.")` in the Julia REPL. +Make sure that you can find a Julia kernel. If you can't, run `IJulia.installkernel()` to install the default kernel. The `kernel.json` file for the `IJulia` kernel should look something like this: @@ -41,7 +41,7 @@ Fundamentally, if the `IJulia` kernel fails to connect, it is most likely due to * The `julia` executable no longer exists (maybe you updated your installed Julia versions). * The environment that the `julia` executable runs in does not have the `IJulia` package installed. This is a common error. In general, the `IJulia` package should be installed in the base environment of your Julia installation (what you get when you type `] activate` into the REPL without any further options, or when you simply start the Julia REPL without any options). Note that the `--project=@.` option in the above `kernel.json` falls back to the base environment, so it should generally be safe. If you like to use [shared environments](https://pkgdocs.julialang.org/v1/environments/#Shared-environments), you may want to have a `--project` flag that references that shared environment, and make sure that `IJulia` is installed in that environment. Also make sure that the environment is instantiated. -You can edit the `kernel.json` file to fix any issues. Or, delete the entire folder containing the `kernel.json` file to start from scratch. This is entirely safe to do, or you could also use `jupyter kernelspec uninstall ` from the command line, see `jupyter kernelspec --help`. After deleting an old kernel, simply create a new one, using [`IJulia.installkernel`](@ref) from the Julia REPL. +You can edit the `kernel.json` file to fix any issues. Or, delete the entire folder containing the `kernel.json` file to start from scratch. This is entirely safe to do, or you could also use `jupyter kernelspec uninstall ` from the command line, see `jupyter kernelspec --help`. After deleting an old kernel, simply create a new one by running `IJulia.installkernel()` for the default kernel, or [`IJulia.installkernel`](@ref) with arguments for a custom kernel. For further insight into kernel connection issues, look at the error messages emitted by Jupyter. If you started `jupyter lab` / `jupyter notebook` in the terminal, messages will be printed there, not in the web interface that you access via the browser. For more details, you can pass the `--debug` command line flag to `jupyter`. If you started Jupyter via `IJulia.jupyterlab()` / `IJulia.notebook()`, you must also pass `verbose=true` to see any of the output emitted by `jupyter`, including error messages about connection failures; cf. [Debugging IJulia problems](@ref), below. @@ -50,32 +50,30 @@ For further insight into kernel connection issues, look at the error messages em ## General troubleshooting tips * If you ran into a problem with the above steps, after fixing the - problem you can type `Pkg.build()` to try to rerun the install scripts. + problem you can run `IJulia.installkernel()` to try to reinstall the kernel. * If you tried it a while ago, try running `Pkg.update()` and try again: this will fetch the latest versions of the Julia packages in case - the problem you saw was fixed. Run `Pkg.build("IJulia")` if your Julia version may have changed. If this doesn't work, you could try just deleting the whole `.julia/conda` directory in your home directory (on Windows, it is called `Users\USERNAME\.julia\conda` in your home directory) via `rm(abspath(first(DEPOT_PATH), "conda"),recursive=true)` in Julia and re-adding the packages. + the problem you saw was fixed. Run `IJulia.installkernel()` if your Julia version may have changed. If this doesn't work, you could try just deleting the whole `.julia/conda` directory in your home directory (on Windows, it is called `Users\USERNAME\.julia\conda` in your home directory) via `rm(abspath(first(DEPOT_PATH), "conda"),recursive=true)` in Julia and re-adding the packages. * On MacOS, you currently need MacOS 10.7 or later; [MacOS 10.6 doesn't work](https://github.com/JuliaLang/julia/issues/4215) (unless you compile Julia yourself, from source code). * Internet Explorer 8 (the default in Windows 7) or 9 don't work with the notebook; use Firefox (6 or later) or Chrome (13 or later). Internet Explorer 10 in Windows 8 works (albeit with a few rendering glitches), but Chrome or Firefox is better. * If the notebook opens up, but doesn't respond (the input label is `In[*]` indefinitely), try creating a new Python notebook (not Julia) from the `New` button in the Jupyter dashboard, to see if `1+1` works in Python. If it is the same problem, then probably you have a [firewall running](https://github.com/ipython/ipython/issues/2499) on your machine (this is common on Windows) and you need to disable the firewall or at least to allow the IP address 127.0.0.1. (For the [Sophos](https://en.wikipedia.org/wiki/Sophos) endpoint security software, go to "Configure Anti-Virus and HIPS", select "Authorization" and then "Websites", and add 127.0.0.1 to "Authorized websites"; finally, restart your computer.) If the Python test works, then IJulia may not be installed in the global or default environment and you may need to install a custom Julia kernel that uses your required `Project.toml` (see [Julia projects](@ref)). * Try running `jupyter --version` and make sure that it prints `3.0.0` or larger; earlier versions of IPython are no longer supported by IJulia. -* You can try setting `ENV["JUPYTER"]=""; Pkg.build("IJulia")` to force IJulia to go back to its own Conda-based Jupyter version (if you previously tried a different `jupyter`). +* You can try running `IJulia.update_jupyter_path("")` to force IJulia to go back to its own Conda-based Jupyter version (if you previously tried a different `jupyter`). ## Debugging IJulia problems If IJulia is crashing (e.g. it gives you a "kernel appears to have died" message), you can modify it to print more descriptive error -messages to the terminal by doing: +messages to the terminal by setting the `IJULIA_DEBUG` environment variable: ```julia ENV["IJULIA_DEBUG"]=true -Pkg.build("IJulia") ``` -Restart the notebook and look for the error message when IJulia dies. +Restart the notebook, and look for the error message when IJulia dies. (This changes IJulia to default to `verbose = true` mode, and sets `capture_stderr = false`, hopefully sending a bunch of debugging to the terminal where you launched `jupyter`). -When you are done, set `ENV["IJULIA_DEBUG"]=false` and re-run -`Pkg.build("IJulia")` to turn off the debugging output. +When you are done, set `ENV["IJULIA_DEBUG"]=false` to turn off the debugging output. diff --git a/src/IJulia.jl b/src/IJulia.jl index 233b7935..1fd1a647 100644 --- a/src/IJulia.jl +++ b/src/IJulia.jl @@ -48,9 +48,18 @@ import Logging # and this import makes it possible to load InteractiveUtils from the IJulia namespace import InteractiveUtils -const depfile = joinpath(dirname(@__FILE__), "..", "deps", "deps.jl") -isfile(depfile) || error("IJulia not properly installed. Please run Pkg.build(\"IJulia\")") -include(depfile) # generated by Pkg.build("IJulia") +# Conda is a rather heavy dependency so we go to some effort to load it lazily +const Conda_pkgid = Base.PkgId(Base.UUID("8f4d0f93-b110-5947-807f-2305c1781a2d"), "Conda") + +function get_Conda(f::Function) + if !haskey(Base.loaded_modules, Conda_pkgid) + @eval import Conda + end + @invokelatest f(Base.loaded_modules[Conda_pkgid]) +end + +# Load configuration system +include("config.jl") # use our own random seed for msg_id so that we # don't alter the user-visible random state (issue #336) @@ -98,7 +107,7 @@ end REPL.REPLDisplay(repl::MiniREPL) = repl.display @kwdef mutable struct Kernel - verbose::Bool = IJULIA_DEBUG + verbose::Bool = ijulia_debug() inited::Bool = false current_module::Module = Main @@ -110,7 +119,7 @@ REPL.REPLDisplay(repl::MiniREPL) = repl.display n::Int = 0 capture_stdout::Bool = true - capture_stderr::Bool = !IJULIA_DEBUG + capture_stderr::Bool = !ijulia_debug() capture_stdin::Bool = true minirepl::Union{MiniREPL, Nothing} = nothing @@ -309,7 +318,7 @@ end _shutting_down::Threads.Atomic{Bool} = Threads.Atomic{Bool}(false) ####################################################################### -include(joinpath("..", "deps", "kspec.jl")) +include("kspec.jl") include("jupyter.jl") ####################################################################### diff --git a/src/config.jl b/src/config.jl new file mode 100644 index 00000000..1855fb7c --- /dev/null +++ b/src/config.jl @@ -0,0 +1,119 @@ +using Preferences: @load_preference, @set_preferences!, @delete_preferences! + +# Initialized to empty string; will be lazily set on first use (e.g., notebook()) +# or explicitly via update_jupyter_path() +JUPYTER::String = "" + +# Get the IJULIA_DEBUG setting from environment variable. +function ijulia_debug() + debug_val = lowercase(get(ENV, "IJULIA_DEBUG", "0")) + return debug_val in ("1", "true", "yes") +end + +# Load the user's Jupyter preference. +# Returns the stored Jupyter path, or empty string if not set. +function load_jupyter_preference() + # Check Preferences.jl location first + pref = @load_preference("jupyter", nothing) + if pref !== nothing + return pref + end + + # Backwards compat: check old .julia/prefs location + old_prefsfile = joinpath(first(DEPOT_PATH), "prefs", "IJulia") + if isfile(old_prefsfile) + jupyter = readchomp(old_prefsfile) + # Migrate to Preferences.jl + @set_preferences!("jupyter" => jupyter) + return jupyter + end + + return "" +end + +# Determine the Jupyter executable path based on environment variables and preferences. +function determine_jupyter_path() + condajupyter = get_Conda() do Conda + normpath(Conda.SCRIPTDIR, exe("jupyter")) + end + + # Get user preference from environment or stored preference + jupyter = get(load_jupyter_preference, ENV, "JUPYTER") + + # Default to "jupyter" on Unix (non-Apple) if nothing is set + if isempty(jupyter) + jupyter = Sys.isunix() && !Sys.isapple() ? "jupyter" : condajupyter + end + + # Validate the jupyter path + if !isempty(condajupyter) && (isempty(jupyter) || dirname(jupyter) == abspath(dirname(condajupyter))) + jupyter = condajupyter # will be installed if needed + elseif isabspath(jupyter) + if !Sys.isexecutable(jupyter) + @warn("ignoring non-executable JUPYTER=$jupyter") + jupyter = condajupyter + end + elseif jupyter != basename(jupyter) # relative path + @warn("ignoring relative path JUPYTER=$jupyter") + jupyter = condajupyter + elseif Sys.which(jupyter) === nothing + @warn("JUPYTER=$jupyter not found in PATH") + end + + return jupyter +end + +""" + update_jupyter_path() + +Set or determine the Jupyter executable path preference and save it. + +The function checks `ENV["JUPYTER"]` first: +- If not set: uses existing preference, or searches if no preference exists +- If set to a path: uses that path +- If set to empty string `""`: deletes existing preference and forces a fresh search + +The search checks (in order): +1. `ENV["JUPYTER"]` environment variable +2. Previously saved preference (from LocalPreferences.toml) +3. System default (Conda-based Jupyter or system `jupyter`) + +The saved preference will be used by `IJulia.notebook()` and `IJulia.jupyterlab()`. + +Returns the Jupyter path that was saved. + +## Examples +```julia +using IJulia + +# Use existing preference (or search if none exists) +IJulia.update_jupyter_path() + +# Explicitly set Jupyter path via ENV +ENV["JUPYTER"] = "/usr/local/bin/jupyter" +IJulia.update_jupyter_path() + +# Force re-detection, ignoring saved preference +ENV["JUPYTER"] = "" +IJulia.update_jupyter_path() +``` +""" +function update_jupyter_path() + # Check ENV first + jupyter = get(ENV, "JUPYTER", nothing) + + if jupyter === nothing + # No ENV set - use existing preference or search if none exists + jupyter = determine_jupyter_path() + elseif isempty(jupyter) + # ENV set to "" - delete preference and force fresh search + @delete_preferences!("jupyter") + jupyter = determine_jupyter_path() + end + # else: ENV set to specific path - use it + + @set_preferences!("jupyter" => jupyter) + global JUPYTER = jupyter + @info "Jupyter path updated: $jupyter" + return jupyter +end diff --git a/src/jupyter.jl b/src/jupyter.jl index 5dec4ac8..868a7277 100644 --- a/src/jupyter.jl +++ b/src/jupyter.jl @@ -1,17 +1,6 @@ # Code to launch and interact with Jupyter, not via messaging protocol ################################################################## -# Conda is a rather heavy dependency so we go to some effort to load it lazily -const Conda_pkgid = Base.PkgId(Base.UUID("8f4d0f93-b110-5947-807f-2305c1781a2d"), "Conda") - -function get_Conda(f::Function) - if !haskey(Base.loaded_modules, Conda_pkgid) - @eval import Conda - end - - @invokelatest f(Base.loaded_modules[Conda_pkgid]) -end - isyes(s) = isempty(s) || lowercase(strip(s)) in ("y", "yes") """ @@ -21,6 +10,9 @@ Return a `Cmd` for the program `subcommand`. If the program is `jupyter` or `jupyterlab` it may prompt the user to install it. """ function find_jupyter_subcommand(subcommand::AbstractString, port::Union{Nothing,Int}=nothing) + if isempty(JUPYTER) + global JUPYTER = determine_jupyter_path() + end jupyter = JUPYTER scriptdir = get_Conda() do Conda Conda.SCRIPTDIR @@ -63,6 +55,34 @@ end ################################################################## +# Check if the default Julia kernel is installed for the current Julia version. +# If not (and IJULIA_NODEFAULTKERNEL is not set), automatically install it. +function maybe_install_default_kernel() + if haskey(ENV, "IJULIA_NODEFAULTKERNEL") + return + end + + # Check if the default kernel for the current version exists + specname = kernelspec_name("Julia") + kernel_path = joinpath(kerneldir(), specname) + + if !isdir(kernel_path) + # No default kernel found, install it + debugdesc = ccall(:jl_is_debugbuild,Cint,())==1 ? "-debug" : "" + @info """ + No default Julia kernel found for Julia $(VERSION.major).$(VERSION.minor)$(debugdesc). + Installing kernel automatically. You can reinstall or update the kernel + anytime by running: IJulia.installkernel() + + To disable this auto-installation, set the environment variable: + ENV["IJULIA_NODEFAULTKERNEL"] = "true" + """ + installkernel() + end +end + +################################################################## + """ launch(cmd, dir, detached, verbose) @@ -128,25 +148,28 @@ When the optional keyword `port` is not `nothing`, open the notebook on the given port number. If `verbose=true` then the stdout/stderr from Jupyter will be echoed to the -terminal. Try enabling this if you're having problems connecting to a kernel to +terminal. By default, this is enabled when `ENV["IJULIA_DEBUG"]` is set. +Try enabling this if you're having problems connecting to a kernel to see if there's any useful error messages from Jupyter. For launching a JupyterLab instance, see [`IJulia.jupyterlab()`](@ref). """ -function notebook(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=false) +function notebook(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=ijulia_debug()) inited && error("IJulia is already running") + maybe_install_default_kernel() notebook = find_jupyter_subcommand("notebook", port) return launch(notebook, args, dir, detached, verbose) end """ - jupyterlab(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=false) + jupyterlab(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=ijulia_debug()) Similar to [`IJulia.notebook()`](@ref) but launches JupyterLab instead of the Jupyter notebook. """ -function jupyterlab(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=false) +function jupyterlab(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=ijulia_debug()) inited && error("IJulia is already running") + maybe_install_default_kernel() lab = find_jupyter_subcommand("lab", port) jupyter = first(lab) scriptdir = get_Conda() do Conda diff --git a/deps/kspec.jl b/src/kspec.jl similarity index 93% rename from deps/kspec.jl rename to src/kspec.jl index fbdcee94..70387551 100644 --- a/deps/kspec.jl +++ b/src/kspec.jl @@ -1,4 +1,4 @@ -include("jsonx.jl") +include("../vendor/jsonx.jl") function print_json_dict(io::IO, dict::AbstractDict) print(io, '{') @@ -99,6 +99,14 @@ function julia_cmd(bindir=Sys.BINDIR) end end +""" + installkernel(; kwargs...) + +Convenience method that installs the default Julia kernel with `--project=@.`. +Equivalent to `installkernel("Julia", "--project=@."; kwargs...)`. +""" +installkernel(; kwargs...) = installkernel("Julia", "--project=@."; kwargs...) + """ installkernel(name::AbstractString, options::AbstractString...; julia::Cmd, @@ -177,9 +185,9 @@ function installkernel(name::AbstractString, julia_options::AbstractString...; print_json_dict(f, ks) end - copy_config(joinpath(ijulia_dir,"deps","logo-32x32.png"), juliakspec) - copy_config(joinpath(ijulia_dir,"deps","logo-64x64.png"), juliakspec) - copy_config(joinpath(ijulia_dir,"deps","logo-svg.svg"), juliakspec) + copy_config(joinpath(ijulia_dir,"assets","logo-32x32.png"), juliakspec) + copy_config(joinpath(ijulia_dir,"assets","logo-64x64.png"), juliakspec) + copy_config(joinpath(ijulia_dir,"assets","logo-svg.svg"), juliakspec) return juliakspec catch diff --git a/deps/jsonx.jl b/vendor/jsonx.jl similarity index 100% rename from deps/jsonx.jl rename to vendor/jsonx.jl