WolframLanguageForJupyter/configure-jupyter.wls
2019-05-28 16:46:47 -04:00

464 lines
16 KiB
Plaintext
Executable File

#!/usr/bin/env wolframscript
Begin["WolframLanguageForJupyter`Private`"];
notfound = "configure-jupyter.wls: Jupyter installation on Environment[\"PATH\"] not found.";
isdir = "configure-jupyter.wls: Provided Jupyter binary path is a directory. Please provide the path to the Jupyter binary."
nobin = "configure-jupyter.wls: Provided Jupyter binary path does not exist.";
isdirMath = "configure-jupyter.wls: Provided Wolfram Engine binary path is a directory. Please provide the path to the Wolfram Engine binary."
nobinMath = "configure-jupyter.wls: Provided Wolfram Engine binary path does not exist.";
notadded = "configure-jupyter.wls: An error has occurred. The desired Wolfram Engine is not in \"jupyter kernelspec list.\"";
notremoved = "configure-jupyter.wls: An error has occurred: Wolfram Engine(s) still in \"jupyter kernelspec list.\"";
addconflict = "configure-jupyter.wls: An error has occurred. A Wolfram Engine with the same $VersionNumber of the target Wolfram Engine is in \"jupyter kernelspec list.\" Attempting to overwrite ...";
(* removeconflict = "configure-jupyter.wls: An error has occurred. The Wolfram Engine(s) to be removed is/are not in \"jupyter kernelspec list.\""; *)
removeconflict = "";
nopaclet = "configure-jupyter.wls: WolframLanguageForJupyter paclet source not detected. Are you running the script in the root project directory?";
nolink = "configure-jupyter.wls: Communication with provided Wolfram Engine binary could not be established.";
(*
Dictionary:
mathBin/mathBinSession = WolframKernel binary
kernelspec = Kernel Specification; term used by Jupyter
notProvidedQ = was a Wolfram Engine Binary explicitly specified?
*)
(* START: Helper symbols *)
projectHome = If[StringQ[$InputFileName] && $InputFileName != "", DirectoryName[$InputFileName], Directory[]];
(* establishes link with Wolfram Engine at mathBin and evaluates $Version/$VersionNumber *)
(* returns string form *)
getVersionFromKernel[mathBin_String] :=
Module[{link, res},
link =
LinkLaunch[
StringJoin[
{
"\"",
mathBin,
"\" -wstp"
}
]
];
If[FailureQ[link],
Return[$Failed];
];
(* bleed link *)
While[LinkReadyQ[link, 0.5], LinkRead[link];];
LinkWrite[link, Unevaluated[$VersionNumber]];
res = StringTrim[ToString[LinkRead[link]], "ReturnPacket[" | "]"];
LinkClose[link];
If[!StringContainsQ[res, "[" | "]"],
Return[res];,
Return[$Failed];
];
];
(* determine display name for Jupyter installation from Wolfram Engine $Version/$VersionNumber *)
(* returns {Kernel ID, Display Name} *)
getNames[mathBin_String, notProvidedQ_?BooleanQ] :=
Module[{version, installDir, (* names, hashedKernelUUID *) versionStr},
(* if Wolfram Engine binary not provided, just evaluate $Version in the current session *)
(* otherwise, use MathLink to obtain $Version *)
If[
notProvidedQ,
version = ToString[$VersionNumber];
installDir = $InstallationDirectory;
,
version = Quiet[getVersionFromKernel[mathBin]];
If[
FailureQ[version],
Return[$Failed];
];
installDir = mathBin;
];
(*
hashedKernelUUID = StringJoin["wl-script-", Hash[installDir, "SHA", "HexString"]];
names = StringCases[version, name___ ~~ " for " ~~ ("Mac" | "Microsoft" | "Windows" | "Linux") -> name];
Return[
If[Length[names] > 0,
{
ToLowerCase[StringJoin[
"WolframLanguage-script-",
StringReplace[First[names], Whitespace -> "-"]
]],
StringJoin[
"Wolfram Language (",
Capitalize[
First[names],
"AllWords"
],
") | Script Install"
]
}
,
{hashedKernelUUID, "Wolfram Language | Script Install"}
]
];
*)
versionStr = StringTrim[version, "."];
Return[
{
(* Kernel ID *)
StringJoin["wolframlanguage", versionStr],
(* Display Name *)
StringJoin["Wolfram Language ", versionStr]
}
];
];
(* determine symbols related to finding Wolfram Engine and Jupyter installations *)
(* mathBinSession: WolframKernel location for the current session *)
(* fileExt: file extension for executables *)
(* pathSeperator: delimiter for directories on PATH *)
defineGlobalVars[] :=
Switch[
$OperatingSystem,
"Windows",
mathBinSession = FileNameJoin[{$InstallationDirectory, "wolfram.exe"}];
fileExt = ".exe";
pathSeperator = ";";,
"MacOSX",
mathBinSession = FileNameJoin[{$InstallationDirectory, "MacOS", "WolframKernel"}];
fileExt = "";
pathSeperator = ":";,
"Unix",
mathBinSession = FileNameJoin[{$InstallationDirectory, "Executables", "WolframKernel"}];
fileExt = "";
pathSeperator = ":";
];
mathBinSession := (defineGlobalVars[]; mathBinSession);
fileExt := (defineGlobalVars[]; fileExt);
pathSeperator := (defineGlobalVars[]; pathSeperator);
(* a list of directories in PATH *)
splitPath := StringSplit[Environment["PATH"], pathSeperator];
(* restore PATH, if due to a bug, it becomes essentially empty; this is relevant to finding the Jupyter installation *)
(* returns above *)
attemptPathRegeneration[] := If[
$OperatingSystem === "MacOSX" && FileType["~/.profile"] === File,
Print["install.wls: Warning: Regenerating PATH ..."];
SetEnvironment[
"PATH" -> StringTrim[
RunProcess[
$SystemShell,
"StandardOutput",
StringJoin[Import["~/.profile", "String"], "\necho $PATH"],
ProcessEnvironment -> {}
],
"\n"
]
];
];
(* find Jupyter installation path *)
(* returns kernel IDs in Jupyter *)
findJupyerPath[] :=
SelectFirst[
splitPath,
(* check every directory in PATH to see if a Jupyter binary is a member *)
(FileType[FileNameJoin[{#1, StringJoin["jupyter", fileExt]}]] === File)&
];
(* get information about installed kernels in Jupyter *)
(* returns kernel IDs in Jupyter *)
getKernels[jupyterPath_String, processEnvironment_] :=
Module[{json, kernelspecAssoc},
(* obtain information about "jupyter kernelspec list" in JSON *)
json = Quiet[ImportString[RunProcess[{jupyterPath, "kernelspec", "list", "--json"}, "StandardOutput", ProcessEnvironment -> processEnvironment], "JSON"]];
(* transform that JSON information into an Association *)
kernelspecAssoc =
If[
FailureQ[json],
Association[],
Replace[
json,
part_List /; AllTrue[part, Head[#1] === Rule &] -> Association @ part,
{0, Infinity}
]
];
Return[
(* if the above process worked, just return the kernel IDs of all the kernelspecs *)
(* otherwise, return an empty list *)
If[
KeyExistsQ[kernelspecAssoc, "kernelspecs"],
Keys[kernelspecAssoc["kernelspecs"]],
{}
]
];
];
(* END: Helper symbols *)
(* main install command *)
(* specs: options \"WolframEngineBinary\" and \"JupyterInstallation\" in an Association, when provided *)
(* removeQ: remove a Jupyter installation or not *)
(* removeAllQ: clear all Jupyter installations or not *)
(* removeQ first, removeAllQ second: "add" is False, False; "remove" is True, False, and "clear" is True, True *)
configureJupyter[specs_Association, removeQ_?BooleanQ, removeAllQ_?BooleanQ] :=
Module[
{
kernelScript,
retrievedNames, kernelID, displayName,
notProvidedQ,
jupyterPath, mathBin,
fileType,
processEnvironment,
baseDir, tempDir,
wlKernels, (* wlKernelsL(owerCase) *) wlKernelsL,
commandArgs,
exitInfo, kernelspecAssoc, kernelspecs,
conflictMessage, failureMessage
},
kernelScript = FileNameJoin[{projectHome, "WolframLanguageForJupyter", "Resources", "KernelForWolframLanguageForJupyter.wl"}];
(* just check that the REPL script is there *)
If[
!(FileType[kernelScript] === File),
Print[nopaclet];
Return[$Failed];
];
jupyterPath = specs["JupyterInstallation"];
(* if no Jupyter installation path provided, determine it from PATH *)
If[
MissingQ[jupyterPath],
jupyterPath = findJupyerPath[];
(* if Jupyter not on PATH, message *)
If[MissingQ[jupyterPath],
Print[notfound];
Return[$Failed];
];
jupyterPath = FileNameJoin[{jupyterPath, StringJoin["jupyter", fileExt]}];
];
mathBin =
Lookup[
specs,
"WolframEngineBinary",
(* if no "WolframEngineBinary" provided, use the session Wolfram Kernel location and set notProvidedQ to True *)
(notProvidedQ = True; mathBinSession)
];
(* check that the Jupyter installation path is a file *)
If[
!((fileType = FileType[jupyterPath]) === File),
Switch[
fileType,
Directory,
Print[isdir];,
None,
Print[nobin];
];
Return[$Failed];
];
{kernelID, displayName} = {"", ""};
(* if not clearing, check that the Wolfram Engine installation path is a file, and message appropriately *)
If[
!(removeQ && removeAllQ),
If[
(fileType = FileType[mathBin]) === File,
(* get the "Kernel ID" and "Display Name" for the new Jupyter kernel *)
retrievedNames = getNames[mathBin, TrueQ[notProvidedQ]];
If[FailureQ[retrievedNames], Print[nolink]; Return[$Failed]];
{kernelID, displayName} = retrievedNames;,
Switch[
fileType,
Directory,
Print[isdirMath];,
None,
Print[nobinMath];
];
Return[$Failed];
];
];
(* as an association for 11.3 compatibility *)
processEnvironment = Association[GetEnvironment[]];
processEnvironment["PATH"] = StringJoin[Environment["PATH"], pathSeperator, DirectoryName[jupyterPath]];
(* list of kernels in Jupyter to perform an action on *)
wlKernels = {kernelID};
tempDir = "";
(* if adding, ...*)
(* otherwise, when removing or clearing, ...*)
If[
!removeQ,
failureMessage = notadded;
conflictMessage = addconflict;
(* create staging directory for files needed to register a kernel with Jupyter *)
tempDir = CreateDirectory[
FileNameJoin[{
projectHome,
CreateUUID[],
(* removing this would cause every evalution of addKernelToJupyter adds a new kernel with a different uuid *)
kernelID
}], CreateIntermediateDirectories -> True
];
(* export a JSON file to the staging directory that contains all the relevant information on how to run the kernel *)
Export[
FileNameJoin[{tempDir, "kernel.json"}],
Association[
"argv" -> {mathBin, "-script", kernelScript, "{connection_file}", "ScriptInstall" (* , "-noprompt" *)},
"display_name" -> displayName,
"language" -> "Wolfram Language"
]
];
(* create a list of arguments that directs Jupyter to install from the staging directory *)
commandArgs = {jupyterPath, "kernelspec", "install", "--user", tempDir};,
failureMessage = notremoved;
conflictMessage = removeconflict;
(* create a list of arguments that directs Jupyter to remove ... *)
commandArgs = {jupyterPath, "kernelspec", "remove", "-f",
If[
!removeAllQ,
(* just the specified kernel *)
kernelID,
(* all Wolfram Language Jupyter kernels *)
(* select from all kernel IDs in Jupyter those that match the form used by this install *)
Sequence @@ (wlKernels = Select[getKernels[jupyterPath, processEnvironment], StringMatchQ[#1, (* ("WolframLanguage-" | "wl-") *) "WolframLanguage" ~~ ___, IgnoreCase -> True] &])
]
}
];
(* if no kernels to act on, quit *)
If[Length[wlKernels] == 0, Return[];];
wlKernelsL = ToLowerCase /@ wlKernels;
(* for error detection, get a snapshot of kernels before the action is performed *)
kernelspecs = getKernels[jupyterPath, processEnvironment];
(* when adding, if there is a kernel with the same id already in Jupyter, it will be replaced; thus, message, but continue *)
If[Xor[removeQ, SubsetQ[kernelspecs, wlKernelsL]], Print[conflictMessage];];
(* perform the action *)
exitInfo = RunProcess[commandArgs, All, ProcessEnvironment -> processEnvironment];
(* remove temporary directory if it was created *)
If[StringLength[tempDir] > 0, DeleteDirectory[DirectoryName[tempDir], DeleteContents -> True]];
(* get list of kernels after the action was performed *)
kernelspecs = getKernels[jupyterPath, processEnvironment];
(* message about success with respect to the action that was performed *)
If[
!Xor[removeQ, SubsetQ[kernelspecs, wlKernelsL]],
Print[failureMessage];
Print["configure-jupyter.wls: See below for the message that Jupyter returned when attempting to add the Wolfram Engine."];
Print[StringTrim[exitInfo["StandardError"], Whitespace]];
Return[$Failed];
];
];
(* checking RunProcess ..., and messaging appropriately *)
If[
FailureQ[RunProcess[$SystemShell, All, ""]],
(* maybe remove *)
If[
MemberQ[$CommandLine, "-script"],
Print["configure-jupyter.wls: Please use -file instead of -script in WolframScript."];
Quit[];
,
Print["configure-jupyter.wls: An unknown error has occurred."];
attemptPathRegeneration[];
If[FailureQ[RunProcess[$SystemShell, All, ""]], Quit[]];
];
];
defineGlobalVars[];
(* maybe remove *)
(* checking PATH ..., and messaging appropriately *)
If[
Length[splitPath] == 1,
Print["configure-jupyter.wls: Warning: This script has encountered a very small PATH environment variable."];
Print["configure-jupyter.wls: Warning: This can occur due to a possible WolframScript bug."];
attemptPathRegeneration[];
];
(* START: Building usage message *)
templateJupyterPath = StringJoin["\"", FileNameJoin[{"path", "to", "Jupyter-binary"}], "\""];
templateWLPath = StringJoin["\"", FileNameJoin[{"", "absolute", "path", "to", "Wolfram-Engine-binary--not-wolframscript"}], "\""];
(* helpMessage = StringJoin[
"configure-jupyter.wls add [", templateJupyterPath, "]\n",
"configure-jupyter.wls adds a Wolfram Engine to a Jupyter binary on PATH, or optional provided Jupyter binary path\n",
"configure-jupyter.wls add ", templateJupyterPath, " ", templateWLPath, "\n",
"\tadds the provided absolute Wolfram Engine binary path to the provided Jupyter binary path\n",
"configure-jupyter.wls remove [", templateJupyterPath ,"]\n",
"\tremoves any Wolfram Engines found on a Jupyter binary on PATH, or optional provided Jupyter binary path"
]; *)
helpMessage = StringJoin[
"configure-jupyter.wls add [", templateWLPath, "]\n",
"\tadds a Wolfram Engine, either attached to the current invocation, or at the provided absolute Wolfram Engine binary path, to a Jupyter binary on PATH\n",
"configure-jupyter.wls add ", templateWLPath, " ", templateJupyterPath, "\n",
"\tadds the provided absolute Wolfram Engine binary path to the provided Jupyter binary path\n",
"configure-jupyter.wls remove [", templateWLPath ,"]\n",
"\tremoves the Wolfram Engine, either attached to the current invocation, or at the provided absolute Wolfram Engine binary path, from a Jupyter binary on PATH\n",
"configure-jupyter.wls remove ", templateWLPath, " ", templateJupyterPath, "\n",
"\tremoves the provided absolute Wolfram Engine binary path from the provided Jupyter binary path\n",
"configure-jupyter.wls clear [", templateJupyterPath ,"]\n",
"\tremoves all Wolfram Engines found on a Jupyter binary on PATH, or optional provided Jupyter binary path\n",
"configure-jupyter.wls build\n",
"\tbuilds the WolframLanguageForJupyter paclet in the project directory"
];
(* END: Building usage message *)
(* based off of the script invocation, use configureJupyter or PackPaclet; or display help message *)
If[
Length[$ScriptCommandLine] < 2 ||
Length[$ScriptCommandLine] > 4 ||
$ScriptCommandLine[[2]] === "help",
Print[helpMessage];
,
Switch[
$ScriptCommandLine[[2]],
"add" | "Add",
command = {False, False};,
"remove" | "Remove",
command = {True, False};,
"clear" | "Clear",
command = {True, True};,
"build",
PackPaclet["WolframLanguageForJupyter"];
Quit[];
,
_,
Print[helpMessage];
];
configureJupyter[
Switch[
Length[$ScriptCommandLine],
4,
Association[
"WolframEngineBinary" -> $ScriptCommandLine[[3]],
"JupyterInstallation" -> $ScriptCommandLine[[4]]
],
3,
If[command === {True, True},
Association["JupyterInstallation" -> $ScriptCommandLine[[3]]],
Association["WolframEngineBinary" -> $ScriptCommandLine[[3]]]
],
2,
Association[]
],
Sequence @@ command
];
];
End[];