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