From 0bd8d0689e70a355ef80ec5350ca899547339e13 Mon Sep 17 00:00:00 2001 From: Kristoffer Carlsson Date: Wed, 5 Nov 2025 11:21:38 +0100 Subject: [PATCH 1/2] change from using a build script to auto installing kernels and providing a jupyter path update function The Pkg build functionality is not good. It gets run whenever a package is *downloaded* (which is a weird thing to trigger on where packages are considered immutable and multiple environments share the same package downloads), it requires the package to recompile when build options are changed etc. This instead auto installs the kernel if needed and also provides a separate function to update the jupyter path. It also moves to using Scratch.jl system over using `DEPOT_PATH[1]/prefs` (but reads the old file as a fallback) It also changes some things that had to be done at build time (like setting the debug mode) to now just be a runtime thing --- .gitignore | 4 - Project.toml | 2 + README.md | 33 ++++---- {deps => assets}/ijulialogo.png | Bin {deps => assets}/logo-32x32.png | Bin {deps => assets}/logo-64x64.png | Bin {deps => assets}/logo-svg.svg | 0 deps/build.jl | 45 ----------- docs/src/_changelog.md | 28 +++++++ docs/src/library/public.md | 1 + docs/src/manual/installation.md | 83 ++++++++++--------- docs/src/manual/troubleshooting.md | 31 ++++--- src/IJulia.jl | 21 +++-- src/config.jl | 126 +++++++++++++++++++++++++++++ src/jupyter.jl | 44 +++++++--- {deps => src}/kspec.jl | 16 +++- {deps => vendor}/jsonx.jl | 0 17 files changed, 292 insertions(+), 142 deletions(-) rename {deps => assets}/ijulialogo.png (100%) rename {deps => assets}/logo-32x32.png (100%) rename {deps => assets}/logo-64x64.png (100%) rename {deps => assets}/logo-svg.svg (100%) delete mode 100644 deps/build.jl create mode 100644 src/config.jl rename {deps => src}/kspec.jl (93%) rename {deps => vendor}/jsonx.jl (100%) 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 73d175c6..1ec616f9 100644 --- a/Project.toml +++ b/Project.toml @@ -15,6 +15,7 @@ Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" +Scratch = "6c6a2e73-6563-6170-7368-637461726353" Sockets = "6462fe0b-24de-5631-8697-dd941f90decc" UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" ZMQ = "c2297ded-f4af-51ae-bb23-16f91089e4e1" @@ -42,6 +43,7 @@ REPL = "1" Random = "1" Revise = "3" SHA = "0.7, 1" +Scratch = "1.3.0" Sockets = "1" UUIDs = "1" ZMQ = "1.4" diff --git a/README.md b/README.md index 35df1678..4a584055 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,26 @@ 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/JuliaPy/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. A Julia kernel can be +installed with `IJulia.installkernel()`. + +**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 b982f47e..a1a32bdc 100644 --- a/docs/src/_changelog.md +++ b/docs/src/_changelog.md @@ -7,6 +7,32 @@ CurrentModule = IJulia This documents notable changes in IJulia.jl. The format is based on [Keep a Changelog](https://keepachangelog.com). +## Unreleased + +### Added +- Added [`IJulia.update_jupyter_path()`](@ref) function to explicitly update the saved + Jupyter executable path preference. +- Added a zero-argument [`installkernel()`](@ref) convenience method that installs + the default Julia kernel with `--project=@.`. + +### 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 [Scratch.jl](https://github.com/JuliaPackaging/Scratch.jl) to + store configuration preferences instead of instead of writing to + `DEPOT_PATH/prefs/`. +- 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. + ## [v1.33.0] - 2025-11-22 ### Added @@ -53,6 +79,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](manual/usage.md#Python-integration) diff --git a/docs/src/library/public.md b/docs/src/library/public.md index 7169d5fe..1b62f447 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..ae3ad6e6 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()`](@ref) or [`IJulia.jupyterlab()`](@ref), or you can + install it manually by running [`IJulia.installkernel()`](@ref). 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,17 +84,13 @@ 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 executable versions and/or environment settings. You can install one or more custom Julia kernels by using the -[`IJulia.installkernel`](@ref) function. For example, if you want to run Julia +[`IJulia.installkernel()`](@ref) function. For example, if you want to run Julia with all deprecation warnings disabled, you can create a custom IJulia kernel: ```julia @@ -99,7 +102,7 @@ will be installed (will show up in your main Jupyter kernel menu) that lets you open notebooks with this flag. Note that the default kernel that IJulia installs passes the `--project=@.` option to Julia, if you want to preserve this behaviour for custom kernels make sure to pass it -explicitly to `IJulia.installkernel`: +explicitly to [`IJulia.installkernel`](@ref): ```julia installkernel("Julia nodeps", "--depwarn=no", "--project=@.") ``` @@ -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()`](@ref) will not +auto-install the default kernel. You can still manually install the default +kernel by calling [`IJulia.installkernel()`](@ref) 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,10 @@ 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()`](@ref) or +[`IJulia.jupyterlab()`](@ref). 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()`](@ref). diff --git a/docs/src/manual/troubleshooting.md b/docs/src/manual/troubleshooting.md index bcddfde1..44241b9e 100644 --- a/docs/src/manual/troubleshooting.md +++ b/docs/src/manual/troubleshooting.md @@ -16,8 +16,7 @@ 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. +[`IJulia.installkernel()`](@ref) in the Julia REPL. The `kernel.json` file for the `IJulia` kernel should look something like this: ```json @@ -72,7 +71,7 @@ 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 +simply create a new one, using [`IJulia.installkernel()`](@ref) from the Julia REPL. For further insight into kernel connection issues, look at the error messages @@ -86,14 +85,14 @@ cf. [Debugging IJulia problems](@ref), below. ## 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. -* 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 +* If you ran into a problem with the above steps, after fixing the problem you + can run [`IJulia.installkernel()`](@ref) 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 [`IJulia.installkernel()`](@ref) 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 @@ -118,7 +117,7 @@ cf. [Debugging IJulia problems](@ref), below. 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 +* 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`). @@ -127,17 +126,15 @@ cf. [Debugging IJulia problems](@ref), below. 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 739dd055..c155fc28 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 shutting_down::Threads.Atomic{Bool} = Threads.Atomic{Bool}(false) @@ -111,7 +120,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 @@ -312,7 +321,7 @@ function set_current_module(m::Module; kernel=_default_kernel) end ####################################################################### -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..aee2e17c --- /dev/null +++ b/src/config.jl @@ -0,0 +1,126 @@ +using Scratch: @get_scratch! + +# 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 from the Scratch space. +# Returns the stored Jupyter path, or empty string if not set. +function load_jupyter_preference() + # Check new Scratch.jl location first + prefsfile = preference_path("jupyter") + if isfile(prefsfile) + return readchomp(prefsfile) + end + + # Backwards compat with .julia/prefs + old_prefsfile = joinpath(first(DEPOT_PATH), "prefs", "IJulia") + if isfile(old_prefsfile) + return readchomp(old_prefsfile) + end + + return "" +end + +# Save the users preference to the Scratch space. +preference_path(name::String) = joinpath(@get_scratch!("prefs"), name) + +function save_preference(name::String, value::String) + prefsfile = preference_path(name) + mkpath(dirname(prefsfile)) + write(prefsfile, value) + return value +end + +delete_preference(name::String) = rm(preference_path(name); force=true) + +# 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" : "" + 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 + 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 Scratch.jl) +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_preference("jupyter") + jupyter = determine_jupyter_path() + end + # else: ENV set to specific path - use it + + save_preference("jupyter", jupyter) + global JUPYTER = jupyter + @info "Jupyter path updated: $jupyter" + return jupyter +end diff --git a/src/jupyter.jl b/src/jupyter.jl index 01674443..04021596 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") """ @@ -20,6 +9,10 @@ isyes(s) = isempty(s) || lowercase(strip(s)) in ("y", "yes") Return a `Cmd` for the program `subcommand`. """ function find_jupyter_subcommand(subcommand::AbstractString, port::Union{Nothing,Int}=nothing) + global JUPYTER + if isempty(JUPYTER) + JUPYTER = determine_jupyter_path() + end jupyter = JUPYTER scriptdir = get_Conda() do Conda Conda.SCRIPTDIR @@ -52,6 +45,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) @@ -91,6 +112,7 @@ end function run_subcommand(name, package_name, port, args...) inited && error("IJulia is already running") + maybe_install_default_kernel() subcmd = find_jupyter_subcommand(name, port) jupyter = first(subcmd) 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 aacc4b4e..4c91ca63 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, '{') @@ -115,6 +115,14 @@ function julia_cmd(bindir=Sys.BINDIR) `$(joinpath(bindir, exe("julia")))` 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, @@ -193,9 +201,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 From 9eb22e3c40a4a8ec08e4ff9c2fd2d863a54747d8 Mon Sep 17 00:00:00 2001 From: KristofferC Date: Wed, 5 Nov 2025 14:46:31 +0100 Subject: [PATCH 2/2] run jupyterlab and notebook in verbose when ijulia debug is set --- src/jupyter.jl | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/jupyter.jl b/src/jupyter.jl index 04021596..a872fff9 100644 --- a/src/jupyter.jl +++ b/src/jupyter.jl @@ -133,7 +133,7 @@ function run_subcommand(name, package_name, port, args...) end """ - notebook(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=false) + notebook(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=ijulia_debug()) The `notebook()` function launches the Jupyter notebook, and is equivalent to running `jupyter notebook` at the operating-system @@ -160,33 +160,34 @@ 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()) run_subcommand("notebook", "jupyter", port, 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()) run_subcommand("lab", "jupyterlab", port, args, dir, detached, verbose) end """ - nbclassic(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=false) + nbclassic(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=ijulia_debug()) Similar to [`IJulia.notebook()`](@ref) but launches the v6 [nbclassic](https://nbclassic.readthedocs.io) notebook instead of the v7 notebook. """ -function nbclassic(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=false) +function nbclassic(args=``; dir=homedir(), detached=false, port::Union{Nothing,Int}=nothing, verbose=ijulia_debug()) run_subcommand("nbclassic", "nbclassic", port, args, dir, detached, verbose) end