-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathconfigure-jupyter.wls
executable file
·485 lines (434 loc) · 17 KB
/
configure-jupyter.wls
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
#!/usr/bin/env wolframscript
(*
Copyright 2018 Wolfram Research Inc. Modified by Jonas Musall.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*)
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 *)
findJupyterPath[] :=
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 = findJupyterPath[];
(* 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};, *)
commandArgs = {jupyterPath, "kernelspec", "install", 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[];