diff --git a/cli/args/flags.rs b/cli/args/flags.rs index ce9085f204..ba1096bfff 100644 --- a/cli/args/flags.rs +++ b/cli/args/flags.rs @@ -278,8 +278,11 @@ pub struct JSONReferenceFlags { #[derive(Clone, Debug, Eq, PartialEq)] pub struct JupyterFlags { pub install: bool, + pub name: Option, + pub display: Option, pub kernel: bool, pub conn_file: Option, + pub force: bool, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -2723,14 +2726,37 @@ fn json_reference_subcommand() -> Command { } fn jupyter_subcommand() -> Command { - command("jupyter", "Deno kernel for Jupyter notebooks", UnstableArgsConfig::ResolutionAndRuntime) + command("jupyter", "Deno kernel for Jupyter notebooks", UnstableArgsConfig::None) .arg( Arg::new("install") .long("install") - .help("Installs kernelspec, requires 'jupyter' command to be available.") + .help("Install a kernelspec") .conflicts_with("kernel") .action(ArgAction::SetTrue) ) + .arg( + Arg::new("name") + .long("name") + .short('n') + .help(cstr!("Set a name for the kernel (defaults to 'deno'). Useful when maintaing multiple Deno kernels.")) + .value_parser(value_parser!(String)) + .conflicts_with("kernel") + ) + .arg( + Arg::new("display") + .long("display") + .short('d') + .help(cstr!("Set a display name for the kernel (defaults to 'Deno'). Useful when maintaing multiple Deno kernels.")) + .value_parser(value_parser!(String)) + .requires("install") + ) + .arg( + Arg::new("force") + .long("force") + .help("Force installation of a kernel, overwriting previously existing kernelspec") + .requires("install") + .action(ArgAction::SetTrue) + ) .arg( Arg::new("kernel") .long("kernel") @@ -5163,11 +5189,17 @@ fn jupyter_parse(flags: &mut Flags, matches: &mut ArgMatches) { let conn_file = matches.remove_one::("conn"); let kernel = matches.get_flag("kernel"); let install = matches.get_flag("install"); + let display = matches.remove_one::("display"); + let name = matches.remove_one::("name"); + let force = matches.get_flag("force"); flags.subcommand = DenoSubcommand::Jupyter(JupyterFlags { install, kernel, conn_file, + name, + display, + force, }); } @@ -11340,6 +11372,9 @@ mod tests { install: false, kernel: false, conn_file: None, + name: None, + display: None, + force: false, }), ..Flags::default() } @@ -11353,6 +11388,65 @@ mod tests { install: true, kernel: false, conn_file: None, + name: None, + display: None, + force: false, + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!["deno", "jupyter", "--install", "--force"]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Jupyter(JupyterFlags { + install: true, + kernel: false, + conn_file: None, + name: None, + display: None, + force: true, + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec![ + "deno", + "jupyter", + "--install", + "--name", + "debugdeno", + "--display", + "Deno (debug)" + ]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Jupyter(JupyterFlags { + install: true, + kernel: false, + conn_file: None, + name: Some("debugdeno".to_string()), + display: Some("Deno (debug)".to_string()), + force: false, + }), + ..Flags::default() + } + ); + + let r = flags_from_vec(svec!["deno", "jupyter", "-n", "debugdeno",]); + assert_eq!( + r.unwrap(), + Flags { + subcommand: DenoSubcommand::Jupyter(JupyterFlags { + install: false, + kernel: false, + conn_file: None, + name: Some("debugdeno".to_string()), + display: None, + force: false, }), ..Flags::default() } @@ -11372,6 +11466,9 @@ mod tests { install: false, kernel: true, conn_file: Some(String::from("path/to/conn/file")), + name: None, + display: None, + force: false, }), ..Flags::default() } @@ -11389,6 +11486,12 @@ mod tests { r.unwrap_err(); let r = flags_from_vec(svec!["deno", "jupyter", "--install", "--kernel",]); r.unwrap_err(); + let r = flags_from_vec(svec!["deno", "jupyter", "--display", "deno"]); + r.unwrap_err(); + let r = flags_from_vec(svec!["deno", "jupyter", "--kernel", "--display"]); + r.unwrap_err(); + let r = flags_from_vec(svec!["deno", "jupyter", "--force"]); + r.unwrap_err(); } #[test] @@ -11733,6 +11836,9 @@ mod tests { install: false, kernel: false, conn_file: None, + name: None, + display: None, + force: false, }), unstable_config: UnstableConfig { bare_node_builtins: true, diff --git a/cli/tools/jupyter/install.rs b/cli/tools/jupyter/install.rs index d76c59343b..777ad42d9e 100644 --- a/cli/tools/jupyter/install.rs +++ b/cli/tools/jupyter/install.rs @@ -3,27 +3,51 @@ use std::env::current_exe; use std::io::Write; use std::path::Path; +use std::path::PathBuf; +use deno_core::anyhow::bail; +use deno_core::anyhow::Context; use deno_core::error::AnyError; use deno_core::serde_json; use deno_core::serde_json::json; -use jupyter_runtime::dirs::user_data_dir; + +static TEST_ENV_VAR_NAME: &str = "DENO_TEST_JUPYTER_PATH"; const DENO_ICON_32: &[u8] = include_bytes!("./resources/deno-logo-32x32.png"); const DENO_ICON_64: &[u8] = include_bytes!("./resources/deno-logo-64x64.png"); const DENO_ICON_SVG: &[u8] = include_bytes!("./resources/deno-logo-svg.svg"); -pub fn status() -> Result<(), AnyError> { - let user_data_dir = user_data_dir()?; +fn get_user_data_dir() -> Result { + Ok(if let Some(env_var) = std::env::var_os(TEST_ENV_VAR_NAME) { + PathBuf::from(env_var) + } else { + jupyter_runtime::dirs::user_data_dir()? + }) +} - let kernel_spec_dir_path = user_data_dir.join("kernels").join("deno"); +pub fn status(maybe_name: Option<&str>) -> Result<(), AnyError> { + let user_data_dir = get_user_data_dir()?; + + let kernel_name = maybe_name.unwrap_or("deno"); + let kernel_spec_dir_path = user_data_dir.join("kernels").join(kernel_name); let kernel_spec_path = kernel_spec_dir_path.join("kernel.json"); if kernel_spec_path.exists() { - log::info!("✅ Deno kernel already installed"); + log::info!( + "✅ Deno kernel already installed at {}", + kernel_spec_dir_path.display() + ); Ok(()) } else { - log::warn!("ℹ️ Deno kernel is not yet installed, run `deno jupyter --install` to set it up"); + let mut install_cmd = "deno jupyter --install".to_string(); + if let Some(name) = maybe_name { + install_cmd.push_str(" --name "); + install_cmd.push_str(name); + } + log::warn!( + "ℹ️ Deno kernel is not yet installed, run `{}` to set it up", + install_cmd + ); Ok(()) } } @@ -39,29 +63,69 @@ fn install_icon( Ok(()) } -pub fn install() -> Result<(), AnyError> { - let user_data_dir = user_data_dir()?; - let kernel_dir = user_data_dir.join("kernels").join("deno"); +pub fn install( + maybe_name: Option<&str>, + maybe_display_name: Option<&str>, + force: bool, +) -> Result<(), AnyError> { + let user_data_dir = get_user_data_dir()?; - std::fs::create_dir_all(&kernel_dir)?; + let kernel_name = maybe_name.unwrap_or("deno"); + let kernel_spec_dir_path = user_data_dir.join("kernels").join(kernel_name); + let kernel_spec_path = kernel_spec_dir_path.join("kernel.json"); - let kernel_json_path = kernel_dir.join("kernel.json"); + std::fs::create_dir_all(&kernel_spec_dir_path).with_context(|| { + format!( + "Failed to create kernel directory at {}", + kernel_spec_dir_path.display() + ) + })?; + + if kernel_spec_path.exists() && !force { + bail!( + "Deno kernel already exists at {}, run again with `--force` to overwrite it", + kernel_spec_dir_path.display() + ); + } + + let display_name = maybe_display_name.unwrap_or("Deno"); + let current_exe_path = current_exe() + .context("Failed to get current executable path")? + .to_string_lossy() + .to_string(); // TODO(bartlomieju): add remaining fields as per // https://jupyter-client.readthedocs.io/en/stable/kernels.html#kernel-specs - // FIXME(bartlomieju): replace `current_exe` before landing? let json_data = json!({ - "argv": [current_exe().unwrap().to_string_lossy(), "jupyter", "--kernel", "--conn", "{connection_file}"], - "display_name": "Deno", + "argv": [current_exe_path, "jupyter", "--kernel", "--conn", "{connection_file}"], + "display_name": display_name, "language": "typescript", }); - let f = std::fs::File::create(kernel_json_path)?; - serde_json::to_writer_pretty(f, &json_data)?; - install_icon(&kernel_dir, "logo-32x32.png", DENO_ICON_32)?; - install_icon(&kernel_dir, "logo-64x64.png", DENO_ICON_64)?; - install_icon(&kernel_dir, "logo-svg.svg", DENO_ICON_SVG)?; + let f = std::fs::File::create(&kernel_spec_path).with_context(|| { + format!( + "Failed to create kernelspec file at {}", + kernel_spec_path.display() + ) + })?; + serde_json::to_writer_pretty(f, &json_data).with_context(|| { + format!( + "Failed to write kernelspec file at {}", + kernel_spec_path.display() + ) + })?; + let failed_icon_fn = + || format!("Failed to copy icon to {}", kernel_spec_dir_path.display()); + install_icon(&kernel_spec_dir_path, "logo-32x32.png", DENO_ICON_32) + .with_context(failed_icon_fn)?; + install_icon(&kernel_spec_dir_path, "logo-64x64.png", DENO_ICON_64) + .with_context(failed_icon_fn)?; + install_icon(&kernel_spec_dir_path, "logo-svg.svg", DENO_ICON_SVG) + .with_context(failed_icon_fn)?; - log::info!("✅ Deno kernelspec installed successfully."); + log::info!( + "✅ Deno kernelspec installed successfully at {}.", + kernel_spec_dir_path.display() + ); Ok(()) } diff --git a/cli/tools/jupyter/mod.rs b/cli/tools/jupyter/mod.rs index 7fa8c65516..8a3b1d53d0 100644 --- a/cli/tools/jupyter/mod.rs +++ b/cli/tools/jupyter/mod.rs @@ -49,12 +49,16 @@ pub async fn kernel( ); if !jupyter_flags.install && !jupyter_flags.kernel { - install::status()?; + install::status(jupyter_flags.name.as_deref())?; return Ok(()); } if jupyter_flags.install { - install::install()?; + install::install( + jupyter_flags.name.as_deref(), + jupyter_flags.display.as_deref(), + jupyter_flags.force, + )?; return Ok(()); } diff --git a/tests/specs/jupyter/install_command/__test__.jsonc b/tests/specs/jupyter/install_command/__test__.jsonc index df60c3b867..e223cc31e4 100644 --- a/tests/specs/jupyter/install_command/__test__.jsonc +++ b/tests/specs/jupyter/install_command/__test__.jsonc @@ -1,8 +1,24 @@ { - "args": "jupyter --install", - "output": "install_command.out", + "tempDir": true, "envs": { - "PATH": "" + "PATH": "", + "DENO_TEST_JUPYTER_PATH": "$PWD" }, - "exitCode": 0 + "steps": [{ + "args": "jupyter --install", + "output": "install_command.out", + "exitCode": 0 + }, { + "args": "jupyter --install", + "output": "already_installed.out", + "exitCode": 1 + }, { + "args": "jupyter --install --name devdeno", + "output": "install_name.out", + "exitCode": 0 + }, { + "args": "jupyter --install --force", + "output": "install_command.out", + "exitCode": 0 + }] } diff --git a/tests/specs/jupyter/install_command/already_installed.out b/tests/specs/jupyter/install_command/already_installed.out new file mode 100644 index 0000000000..11df7e0f5b --- /dev/null +++ b/tests/specs/jupyter/install_command/already_installed.out @@ -0,0 +1,2 @@ +Warning "deno jupyter" is unstable and might change in the future. +error: Deno kernel already exists at [WILDLINE]deno, run again with `--force` to overwrite it diff --git a/tests/specs/jupyter/install_command/install_command.out b/tests/specs/jupyter/install_command/install_command.out index 62875d9cfc..d71b7e4b17 100644 --- a/tests/specs/jupyter/install_command/install_command.out +++ b/tests/specs/jupyter/install_command/install_command.out @@ -1,2 +1,2 @@ Warning "deno jupyter" is unstable and might change in the future. -✅ Deno kernelspec installed successfully. +✅ Deno kernelspec installed successfully at [WILDLINE]deno. diff --git a/tests/specs/jupyter/install_command/install_name.out b/tests/specs/jupyter/install_command/install_name.out new file mode 100644 index 0000000000..a8cb8b9023 --- /dev/null +++ b/tests/specs/jupyter/install_command/install_name.out @@ -0,0 +1,2 @@ +Warning "deno jupyter" is unstable and might change in the future. +✅ Deno kernelspec installed successfully at [WILDLINE]devdeno.