diff --git a/.gitignore b/.gitignore index 288dc539dbc7cbe867c32d4c5a1afe648d44a760..3b0c2dbbde577e98c2718bce879111ebccb95a35 100644 --- a/.gitignore +++ b/.gitignore @@ -2,9 +2,6 @@ .gradle/ /build/ -# Generated -/kernelspec/prob2/kernel.json - # Python virtual environment /env/ diff --git a/build.gradle b/build.gradle index 53650b7e87f60aecbbb0a4a5144c90cf519a708e..77543cf46b51a5a4c46cc205538616fe50d82d17 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,3 @@ -import java.nio.charset.StandardCharsets -import java.nio.file.Files import java.nio.file.Paths import java.util.concurrent.TimeUnit @@ -53,37 +51,31 @@ tasks.withType(JavaCompile) { mainClassName = "de.prob2.jupyter.Main" -final KERNEL_SPEC_PATH = project.projectDir.toPath().resolve(Paths.get("kernelspec", "prob2")) -final KERNEL_JSON_PATH = KERNEL_SPEC_PATH.resolve("kernel.json") -task createKernelSpec { - dependsOn = [shadowJar] - ext.probHome = project.hasProperty("probHome") ? project.probHome : null - inputs.property("probHome", probHome).optional(true) - outputs.files(KERNEL_JSON_PATH.toFile()) - - doFirst { - final jarPath = shadowJar.archivePath - final probHomeDef = probHome != null ? "\n\t\t\"-Dprob.home=$probHome\"," : "" - final jsonText = """{ -\t"argv": [ -\t\t"java",$probHomeDef -\t\t"-jar", -\t\t"$jarPath", -\t\t"{connection_file}" -\t], -\t"display_name": "ProB 2", -\t"language": "prob" +final KERNEL_SPEC_FILES_PATH = sourceSets.main.resources.sourceDirectories.singleFile.toPath().resolve(Paths.get("de", "prob2", "jupyter", "kernelspecfiles")) +final KERNEL_SPEC_OUTPUT_PATH = project.buildDir.toPath().resolve(Paths.get("kernelspec")) + +task cleanKernelSpec(type: Delete) { + delete = [KERNEL_SPEC_OUTPUT_PATH.toFile()] } -""" - Files.write(KERNEL_JSON_PATH, jsonText.getBytes(StandardCharsets.UTF_8)) +clean.dependsOn << cleanKernelSpec + +task createKernelSpec(type: JavaExec) { + dependsOn = [cleanKernelSpec, shadowJar] + main = project.mainClassName + args = ["createKernelSpec", KERNEL_SPEC_OUTPUT_PATH.toString()] + classpath(shadowJar.archivePath) + if (project.hasProperty("probHome")) { + systemProperty("prob.home", project.probHome) + } + inputs.dir(KERNEL_SPEC_FILES_PATH.toFile()) + outputs.dir(KERNEL_SPEC_OUTPUT_PATH.toFile()) + doFirst { + mkdir(KERNEL_SPEC_OUTPUT_PATH) } } task installKernelSpec(type: Exec) { dependsOn = [createKernelSpec] - executable = "python3" - args = ["-m", "jupyter", "kernelspec", "install", "--sys-prefix", KERNEL_SPEC_PATH.toString()] -} -if (hasProperty("pythonInterpreter")) { - installKernelSpec.executable = pythonInterpreter + executable = project.hasProperty("pythonInterpreter") ? project.pythonInterpreter : "python3" + args = ["-m", "jupyter", "kernelspec", "install", "--sys-prefix", "--name=prob2", KERNEL_SPEC_OUTPUT_PATH.toString()] } diff --git a/src/main/java/de/prob2/jupyter/Main.java b/src/main/java/de/prob2/jupyter/Main.java index 8652c342b060a1f06c86a2b8274ccc9d12d981a1..c0aa3115ebeec6b2640f184620bf04b3dea39a70 100644 --- a/src/main/java/de/prob2/jupyter/Main.java +++ b/src/main/java/de/prob2/jupyter/Main.java @@ -1,12 +1,22 @@ package de.prob2.jupyter; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UncheckedIOException; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.security.CodeSource; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; import com.google.inject.Guice; import com.google.inject.Injector; @@ -15,6 +25,8 @@ import com.google.inject.Stage; import io.github.spencerpark.jupyter.channels.JupyterConnection; import io.github.spencerpark.jupyter.kernel.KernelConnectionProperties; +import org.jetbrains.annotations.Nullable; + public final class Main { private Main() { super(); @@ -22,18 +34,161 @@ public final class Main { throw new AssertionError(); } - public static void main(final String[] args) throws IOException, InvalidKeyException, NoSuchAlgorithmException { - if (args.length != 1) { - System.err.printf("Expected exactly one argument, not %d%n", args.length); - System.exit(2); + private static AssertionError die(final int status, final @Nullable Throwable cause) { + System.exit(status); + return new AssertionError("Unreachable", cause); + } + + private static AssertionError die(final int status) { + return die(status, null); + } + + private static Path getJarPath() { + try { + final CodeSource cs = Main.class.getProtectionDomain().getCodeSource(); + if (cs == null) { + System.err.println("Unable to determine location of kernel jar file (CodeSource is null)"); + throw die(1); + } + return Paths.get(cs.getLocation().toURI()); + } catch (final RuntimeException | URISyntaxException e) { + System.err.println("Unable to determine location of kernel jar file"); + e.printStackTrace(); + throw die(1, e); } + } + + private static Path getDestPath(final Path jarPath) { + return Paths.get(de.prob.Main.getProBDirectory(), "jupyter", jarPath.getFileName().toString()); + } + + private static void copyJar(final Path jarPath, final Path destPath) { + System.out.println("Installing kernel jar file..."); + System.out.println("Path to kernel jar file: " + jarPath); + System.out.println("Kernel jar will be copied to: " + destPath); + try { + Files.createDirectories(destPath.getParent()); + } catch (final IOException e) { + System.err.println("Failed to create destination directory"); + e.printStackTrace(); + throw die(1, e); + } + try { + Files.copy(jarPath, destPath, StandardCopyOption.REPLACE_EXISTING); + } catch (final IOException e) { + System.err.println("Failed to copy kernel jar file"); + e.printStackTrace(); + throw die(1, e); + } + System.out.println("Kernel jar file installed"); + } + + private static void createKernelSpec(final Path jarPath, final Path kernelSpecDir) { + System.out.println("Creating kernel spec..."); + System.out.println("Path to kernel jar file: " + jarPath); + System.out.println("Kernel spec directory: " + kernelSpecDir); + Stream.of("kernel.js", "logo-32x32.png", "logo-64x64.png").forEach(name -> { + System.out.println("Extracting: " + name); + try (final InputStream is = Main.class.getResourceAsStream("kernelspecfiles/" + name)) { + Files.copy(is, kernelSpecDir.resolve(name)); + } catch (final IOException e) { + System.err.println("Failed to extract kernel spec file: " + name); + e.printStackTrace(); + throw die(1, e); + } + }); - System.setProperty("prob.stdlib", Paths.get(de.prob.Main.getProBDirectory(), "stdlib").toString()); + final String probHome = System.getProperty("prob.home"); + final String probHomeDef; + if (probHome != null) { + System.out.println("prob.home is set, adding a corresponding prob.home defintion to kernel.json: " + probHome); + probHomeDef = String.format("\n\t\t\"-Dprob.home=%s\",", probHome); + } else { + System.out.println("prob.home is not set, not adding a prob.home definition to kernel.json"); + probHomeDef = ""; + } - final Path connectionFile = Paths.get(args[0]); + System.out.println("Creating kernel.json"); + try ( + final InputStream is = Main.class.getResourceAsStream("kernelspecfiles/kernel.json.template"); + final InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); + final BufferedReader br = new BufferedReader(isr); + ) { + final String kernelJsonText = String.format(br.lines().collect(Collectors.joining("\n")), probHomeDef, jarPath); + Files.write(kernelSpecDir.resolve("kernel.json"), Arrays.asList(kernelJsonText.split("\n"))); + } catch (final IOException e) { + System.err.println("Failed to create kernel.json"); + e.printStackTrace(); + throw die(1, e); + } + System.out.println("Kernel spec created"); + } + + private static void installKernelSpec(final Path pythonInterpreter, final Path kernelSpecDir) { + System.out.println("Installing kernel spec..."); + System.out.println("Python interpreter: " + pythonInterpreter); + System.out.println("Kernel spec directory: " + kernelSpecDir); + final ProcessBuilder pb = new ProcessBuilder(pythonInterpreter.toString(), "-m", "jupyter", "kernelspec", "install", "--sys-prefix", "--name=prob2", kernelSpecDir.toString()); + pb.redirectInput(ProcessBuilder.Redirect.INHERIT); + pb.redirectOutput(ProcessBuilder.Redirect.INHERIT); + pb.redirectError(ProcessBuilder.Redirect.INHERIT); + final Process pythonProcess; + try { + pythonProcess = pb.start(); + } catch (final IOException e) { + System.err.println("Failed to install kernel spec"); + e.printStackTrace(); + throw die(1, e); + } + final int statusCode; + try { + statusCode = pythonProcess.waitFor(); + } catch (final InterruptedException e) { + System.err.println("Interrupted"); + e.printStackTrace(); + Thread.currentThread().interrupt(); + throw die(1, e); + } + if (statusCode != 0) { + System.err.println("Python exited with status " + statusCode); + throw die(1); + } + System.out.println("Kernel spec installed"); + } + + private static void install(final Path pythonInterpreter) { + final Path jarPath = getJarPath(); + final Path destPath = getDestPath(jarPath); + copyJar(jarPath, destPath); + try { + final Path kernelSpecDir = Files.createTempDirectory("prob2kernelspec"); + createKernelSpec(destPath, kernelSpecDir); + installKernelSpec(pythonInterpreter, kernelSpecDir); + try (final Stream<Path> contents = Files.list(kernelSpecDir)) { + contents.forEach(path -> { + try { + Files.delete(path); + } catch (final IOException e) { + throw new UncheckedIOException(e); + } + }); + } + Files.delete(kernelSpecDir); + } catch (final IOException | UncheckedIOException e) { + System.err.println("Failed to create kernel spec"); + e.printStackTrace(); + throw die(1, e); + } + System.out.println("The ProB 2 Jupyter kernel has been installed."); + System.out.println("To use it, start Jupyter Notebook and select \"ProB 2\" when creating a new notebook."); + System.out.println("This jar file can be safely deleted after installation."); + } + + private static void startKernel(final Path connectionFile) throws IOException, NoSuchAlgorithmException, InvalidKeyException { final String contents = String.join("\n", Files.readAllLines(connectionFile, StandardCharsets.UTF_8)); final JupyterConnection conn = new JupyterConnection(KernelConnectionProperties.parse(contents)); + System.setProperty("prob.stdlib", Paths.get(de.prob.Main.getProBDirectory(), "stdlib").toString()); final Injector injector = Guice.createInjector(Stage.PRODUCTION, new ProBKernelModule()); final ProBKernel kernel = injector.getInstance(ProBKernel.class); kernel.becomeHandlerForConnection(conn); @@ -41,4 +196,51 @@ public final class Main { conn.connect(); conn.waitUntilClose(); } + + public static void main(final String[] args) throws IOException, InvalidKeyException, NoSuchAlgorithmException { + if (args.length < 1 || args.length == 1 && "--help".equals(args[0])) { + System.err.println("Usage: java -jar prob2-jupyter-kernel-all.jar [--help | install PYTHONINTERPRETER | createKernelSpec KERNELSPECDIR | run CONNECTIONFILE]"); + System.err.println("--help: Prints this information."); + System.err.println("install: Copies the kernel into the ProB home directory, and installs the kernel in the given Python interpreter."); + System.err.println("\tIf you're not sure which Python interpreter is used by your Jupyter installation, open a Python notebook and run \"import sys; print(sys.executable)\"."); + System.err.println("createKernelSpec: Creates a Jupyter kernel spec for this jar file at the given location."); + System.err.println("\tThis option is for advanced users or developers, who don't want the jar file to be copied, or who want to install the kernel spec manually."); + System.err.println("run: Runs the kernel using the given connection file."); + System.err.println("\tThis option is not meant to be used manually, it is used internally when Jupyter starts the kernel."); + throw die(2); + } + switch (args[0]) { + case "install": + if (args.length != 2) { + System.err.println("install expects exactly one argument, not " + (args.length-1)); + System.err.println("Use --help for more info."); + throw die(2); + } + install(Paths.get(args[1])); + break; + + case "createKernelSpec": + if (args.length != 2) { + System.err.println("createKernelSpec expects exactly one argument, not " + (args.length-1)); + System.err.println("Use --help for more info."); + throw die(2); + } + createKernelSpec(getJarPath(), Paths.get(args[1])); + break; + + case "run": + if (args.length != 2) { + System.err.println("run expects exactly one argument, not " + (args.length-1)); + System.err.println("Use --help for more info."); + throw die(2); + } + startKernel(Paths.get(args[1])); + break; + + default: + System.err.println("Unknown subcommand: " + args[0]); + System.err.println("Use --help for more info."); + throw die(2); + } + } } diff --git a/kernelspec/prob2/kernel.js b/src/main/resources/de/prob2/jupyter/kernelspecfiles/kernel.js similarity index 100% rename from kernelspec/prob2/kernel.js rename to src/main/resources/de/prob2/jupyter/kernelspecfiles/kernel.js diff --git a/src/main/resources/de/prob2/jupyter/kernelspecfiles/kernel.json.template b/src/main/resources/de/prob2/jupyter/kernelspecfiles/kernel.json.template new file mode 100644 index 0000000000000000000000000000000000000000..6af4f8e3cce8eff4648ea063dcc7221aea281ce9 --- /dev/null +++ b/src/main/resources/de/prob2/jupyter/kernelspecfiles/kernel.json.template @@ -0,0 +1,11 @@ +{ + "argv": [ + "java",%s + "-jar", + "%s", + "run", + "{connection_file}" + ], + "display_name": "ProB 2", + "language": "prob" +} diff --git a/kernelspec/prob2/logo-32x32.png b/src/main/resources/de/prob2/jupyter/kernelspecfiles/logo-32x32.png similarity index 100% rename from kernelspec/prob2/logo-32x32.png rename to src/main/resources/de/prob2/jupyter/kernelspecfiles/logo-32x32.png diff --git a/kernelspec/prob2/logo-64x64.png b/src/main/resources/de/prob2/jupyter/kernelspecfiles/logo-64x64.png similarity index 100% rename from kernelspec/prob2/logo-64x64.png rename to src/main/resources/de/prob2/jupyter/kernelspecfiles/logo-64x64.png