Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create specialized kernels w/ JDK, JVM args, and environment variables #287

Merged
merged 14 commits into from
Jul 3, 2021
10 changes: 10 additions & 0 deletions distrib/kotlin_kernel/__main__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
from kotlin_kernel.install_user import install_user
from kotlin_kernel.add_kernel import add_kernel

import sys

if __name__ == "__main__":
if len(sys.argv) == 2 and sys.argv[1] == "fix-kernelspec-location":
install_user()
elif len(sys.argv) >= 2 and sys.argv[1] == "add-kernel":
add_kernel()
else:
if len(sys.argv) < 2:
print("Must specify a command", file=sys.stderr)
else:
print("Unknown command " + sys.argv[1] + ", known commands are fix-kernelspec-location and add-kernel.",
file=sys.stderr)
exit(1)
109 changes: 109 additions & 0 deletions distrib/kotlin_kernel/add_kernel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import argparse
import json
import os.path
import platform
import shutil
import subprocess
import sys

from kotlin_kernel import env_names
from kotlin_kernel.install_user import get_user_jupyter_path
from kotlin_kernel.install_user import install_base_kernel


def add_kernel():
parser = argparse.ArgumentParser(
prog="add-kernel",
description="Add a kernel with specified JDK, JVM args, and environment",
fromfile_prefix_chars='@')
parser.add_argument("--name",
help="The kernel's sub-name. The kernel will be named \"Kotlin ($name)\". "
"Will be autodetected if JDK is specified, otherwise required. "
"Must be file system compatible.")
parser.add_argument("--jdk",
help="The home directory of the JDK to use")
parser.add_argument("--jvm-arg", action='append', default=[],
help="Add a JVM argument")
parser.add_argument("--env", action='append', nargs=2, default=[],
help="Add an environment variable")
parser.add_argument("--set-jvm-args", action="store_true", default=False,
help="Set JVM args instead of adding them.")
parser.add_argument("--force", action="store_true", default=False,
help="Overwrite an existing kernel with the same name.")

if len(sys.argv) == 2:
parser.print_usage()
exit(0)

args = parser.parse_args(sys.argv[2:])

jdk = args.jdk
if jdk is not None:
jdk = os.path.abspath(os.path.expanduser(jdk))

name = args.name
env = {e[0]: e[1] for e in args.env}

for arg in [env_names.JAVA_HOME, env_names.KERNEL_JAVA_HOME, env_names.JAVA_OPTS,
env_names.KERNEL_EXTRA_JAVA_OPTS, env_names.KERNEL_INTERNAL_ADDED_JAVA_OPTS]:
if arg in env:
print(
"Specified environment variable " + arg + ", will be ignored. "
"Use the corresponding arguments instead.", file=sys.stderr)
del env[arg]

if args.set_jvm_args:
env[env_names.KERNEL_JAVA_OPTS] = " ".join(args.jvm_arg)
else:
env[env_names.KERNEL_INTERNAL_ADDED_JAVA_OPTS] = " ".join(args.jvm_arg)

if jdk is not None:
env[env_names.KERNEL_JAVA_HOME] = jdk
if platform.system() == 'Windows':
java = os.path.join(jdk, "bin/java.exe")
else:
java = os.path.join(jdk, "bin/java")

if not os.path.exists(java):
print("JDK " + jdk + " has no bin/" + os.path.basename(java), file=sys.stderr)
exit(1)

if name is None:
version_spec = subprocess.check_output([java, "--version"], text=True).splitlines()[0].split(" ")
dist = version_spec[0]
version = version_spec[1]
name = "JDK " + dist + " " + version

if name is None:
print("name is required when JDK not specified.", file=sys.stderr)
exit(1)

kernel_name = "kotlin_" + name.replace(" ", "_")
kernel_location = os.path.join(get_user_jupyter_path(), "kernels", kernel_name)

print("Installing kernel to", kernel_location)

if os.path.exists(kernel_location):
if args.force:
print("Overwriting existing kernel at " + kernel_location, file=sys.stderr)
shutil.rmtree(kernel_location)
else:
print("There is already a kernel with name " + kernel_name + ", specify a different name "
"or use --force to overwrite it",
file=sys.stderr)
exit(1)

install_base_kernel(kernel_name)

with open(os.path.join(kernel_location, "kernel.json")) as kernel_file:
kernelspec = json.load(kernel_file)

kernelspec["display_name"] = "Kotlin (" + name + ")"

if "env" in kernelspec:
kernelspec["env"].update(env)
else:
kernelspec["env"] = env

with open(os.path.join(kernel_location, "kernel.json"), "w") as kernel_file:
json.dump(kernelspec, kernel_file, indent=4)
17 changes: 17 additions & 0 deletions distrib/kotlin_kernel/env_names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# standard JVM options environment variable
JAVA_OPTS = "JAVA_OPTS"

# specific JVM options environment variable
KERNEL_JAVA_OPTS = "KOTLIN_JUPYTER_JAVA_OPTS"

# additional JVM options to add to either JAVA_OPTS or KOTLIN_JUPYTER_JAVA_OPTS
KERNEL_EXTRA_JAVA_OPTS = "KOTLIN_JUPYTER_JAVA_OPTS_EXTRA"

# used internally to add JVM options without overwriting KOTLIN_JUPYTER_JAVA_OPTS_EXTRA
KERNEL_INTERNAL_ADDED_JAVA_OPTS = "KOTLIN_JUPYTER_KERNEL_EXTRA_JVM_OPTS"

# standard JDK location environment variable
JAVA_HOME = "JAVA_HOME"

# specific JDK location environment variable
KERNEL_JAVA_HOME = "KOTLIN_JUPYTER_JAVA_HOME"
31 changes: 21 additions & 10 deletions distrib/kotlin_kernel/install_user.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
import os.path
import platform
import shutil
import site
import sys
from os import path, environ


def install_user():
data_relative_path = 'share/jupyter/kernels/kotlin'
user_location = path.join(site.getuserbase(), data_relative_path)
sys_location = path.join(sys.prefix, data_relative_path)
src_paths = [user_location, sys_location]

def get_user_jupyter_path() -> str:
platform_name = platform.system()

if platform_name == 'Linux':
user_jupyter_path = '~/.local/share/jupyter'
jupyter_path = '~/.local/share/jupyter'
elif platform_name == 'Darwin':
user_jupyter_path = '~/Library/Jupyter'
jupyter_path = '~/Library/Jupyter'
elif platform_name == 'Windows':
user_jupyter_path = path.join(environ['APPDATA'], 'jupyter')
jupyter_path = path.join(environ['APPDATA'], 'jupyter')
else:
raise OSError("Unknown platform: " + platform_name)

dst = path.join(user_jupyter_path, 'kernels/kotlin')
return os.path.abspath(os.path.expanduser(jupyter_path))


def install_base_kernel(kernel_name: str):
data_relative_path = 'share/jupyter/kernels/kotlin'
user_location = path.join(site.getuserbase(), data_relative_path)
sys_location = path.join(sys.prefix, data_relative_path)
src_paths = [user_location, sys_location]

user_jupyter_path = get_user_jupyter_path()

dst = path.join(user_jupyter_path, 'kernels/' + kernel_name)
for src in src_paths:
if not path.exists(src):
continue
shutil.copytree(src, dst, dirs_exist_ok=True)


def install_user():
install_base_kernel('kotlin')
23 changes: 22 additions & 1 deletion distrib/run_kotlin_kernel/run_kernel.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import json
import os
import shlex
import subprocess
import sys
from typing import List

from kotlin_kernel import env_names


def run_kernel(*args) -> None:
try:
Expand Down Expand Up @@ -39,7 +42,25 @@ def run_kernel_impl(connection_file: str, jar_args_file: str = None, executables
class_path_arg = os.pathsep.join([os.path.join(jars_dir, jar_name) for jar_name in cp])
main_jar_path = os.path.join(jars_dir, main_jar)

subprocess.call(['java', '-jar'] + debug_list +
java_home = os.getenv(env_names.KERNEL_JAVA_HOME) or os.getenv(env_names.JAVA_HOME)

if java_home is None:
java = "java"
else:
java = os.path.join(java_home, "bin", "java")

jvm_arg_str = os.getenv(env_names.KERNEL_JAVA_OPTS) or os.getenv(env_names.JAVA_OPTS) or ""
extra_args = os.getenv(env_names.KERNEL_EXTRA_JAVA_OPTS)
if extra_args is not None:
jvm_arg_str += " " + extra_args

kernel_args = os.getenv(env_names.KERNEL_INTERNAL_ADDED_JAVA_OPTS)
if kernel_args is not None:
jvm_arg_str += " " + kernel_args

jvm_args = shlex.split(jvm_arg_str)

subprocess.call([java] + jvm_args + ['-jar'] + debug_list +
[main_jar_path,
'-classpath=' + class_path_arg,
connection_file,
Expand Down
33 changes: 33 additions & 0 deletions docs/README-STUB.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,39 @@ Don't forget to re-run this script on the kernel update.

To start using `kotlin` kernel inside Jupyter Notebook or JupyterLab create a new notebook with `kotlin` kernel.

The default kernel will use the JDK pointed to by the environment variable `KOTLIN_JUPYTER_JAVA_HOME`,
or `JAVA_HOME` if the first is not set.

JVM arguments will be set from the environment variable `KOTLIN_JUPYTER_JAVA_OPTS` or `JAVA_OPTS` if the first is not set.
Additionally, arguments from `KOTLIN_JUPYTER_JAVA_OPTS_EXTRA` will be added.
Arguments are parsed using [`shlex.split`](https://docs.python.org/3/library/shlex.html).

### Creating Kernels

To create a kernel for a specific JDK, JVM arguments, and environment variables, you can use the `add-kernel` script:
```bash
python -m kotlin_kernel add-kernel [--name name] [--jdk jdk_home_dir] [--set-jvm-args] [--jvm-arg arg]* [--env KEY VALUE]* [--force]
```
The command uses `argparse`, so `--help`, `@argfile` (you will need to escape the `@` in powershell), and `--opt=value` are all supported. `--jvm-arg=arg` in particular
is needed when passing JVM arguments that start with `-`.

If `jdk` not specified, `name` is required. If `name` is not specified but `jdk` is the name will be
`JDK $vendor $version` detected from the JDK. Regardless, the actual name of the kernel will be `Kotlin ($name)`,
and the directory will be `kotlin_$name` with the spaces in `name` replaced by underscores
(so make sure it's compatible with your file system).

JVM arguments are joined with a `' '`, so multiple JVM arguments in the same argument are supported.
The arguments will be added to existing ones (see above section) unless `--set-jvm-args` is present, in which case they
will be set to `KOTLIN_JUPYTER_JAVA_OPTS`. Note that both adding and setting work fine alongside `KOTLIN_JUPYTER_JAVA_OPTS_EXTRA`.

While jupyter kernel environment variable substitutions are supported in `env`, note that if the used environment
variable doesn't exist, nothing will be replaced.

An example:
```bash
python -m kotlin_kernel add-kernel --name "JDK 15 Big 2 GPU" --jdk ~/.jdks/openjdk-15.0.2 --jvm-arg=-Xmx8G --env CUDA_VISIBLE_DEVICES 0,1
```

## Supported functionality

### REPL commands
Expand Down
33 changes: 33 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,39 @@ Don't forget to re-run this script on the kernel update.

To start using `kotlin` kernel inside Jupyter Notebook or JupyterLab create a new notebook with `kotlin` kernel.

The default kernel will use the JDK pointed to by the environment variable `KOTLIN_JUPYTER_JAVA_HOME`,
or `JAVA_HOME` if the first is not set.

JVM arguments will be set from the environment variable `KOTLIN_JUPYTER_JAVA_OPTS` or `JAVA_OPTS` if the first is not set.
Additionally, arguments from `KOTLIN_JUPYTER_JAVA_OPTS_EXTRA` will be added.
Arguments are parsed using [`shlex.split`](https://docs.python.org/3/library/shlex.html).

### Creating Kernels

To create a kernel for a specific JDK, JVM arguments, and environment variables, you can use the `add-kernel` script:
```bash
python -m kotlin_kernel add-kernel [--name name] [--jdk jdk_home_dir] [--set-jvm-args] [--jvm-arg arg]* [--env KEY VALUE]* [--force]
```
The command uses `argparse`, so `--help`, `@argfile` (you will need to escape the `@` in powershell), and `--opt=value` are all supported. `--jvm-arg=arg` in particular
is needed when passing JVM arguments that start with `-`.

If `jdk` not specified, `name` is required. If `name` is not specified but `jdk` is the name will be
`JDK $vendor $version` detected from the JDK. Regardless, the actual name of the kernel will be `Kotlin ($name)`,
and the directory will be `kotlin_$name` with the spaces in `name` replaced by underscores
(so make sure it's compatible with your file system).

JVM arguments are joined with a `' '`, so multiple JVM arguments in the same argument are supported.
The arguments will be added to existing ones (see above section) unless `--set-jvm-args` is present, in which case they
will be set to `KOTLIN_JUPYTER_JAVA_OPTS`. Note that both adding and setting work fine alongside `KOTLIN_JUPYTER_JAVA_OPTS_EXTRA`.

While jupyter kernel environment variable substitutions are supported in `env`, note that if the used environment
variable doesn't exist, nothing will be replaced.

An example:
```bash
python -m kotlin_kernel add-kernel --name "JDK 15 Big 2 GPU" --jdk ~/.jdks/openjdk-15.0.2 --jvm-arg=-Xmx8G --env CUDA_VISIBLE_DEVICES 0,1
```

## Supported functionality

### REPL commands
Expand Down