@damurka/jovian 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -11
- package/docs/api/README.md +10 -1
- package/docs/api/types.md +2 -2
- package/docs/development.md +2 -2
- package/docs/getting-started.md +2 -2
- package/docs/releasing.md +32 -10
- package/docs/troubleshooting.md +9 -4
- package/lib/execution/execution-queue.js +1 -1
- package/lib/session/native-paths.d.ts +1 -1
- package/lib/session/native-paths.js +1 -1
- package/lib/session/r-setup.d.ts +47 -0
- package/lib/session/r-setup.js +238 -0
- package/lib/session/runtimes.d.ts +29 -0
- package/lib/session/runtimes.js +80 -0
- package/lib/session/session-manager.d.ts +17 -5
- package/lib/session/session-manager.js +34 -8
- package/lib/session/supervisor-client.d.ts +14 -3
- package/lib/session/supervisor-client.js +39 -6
- package/lib/types/engine.d.ts +62 -37
- package/lib/utils/logger.d.ts +14 -3
- package/lib/utils/logger.js +38 -30
- package/package.json +11 -7
- package/packages/hera/DESCRIPTION +1 -1
package/README.md
CHANGED
|
@@ -9,11 +9,11 @@ import { SessionManager } from '@damurka/jovian';
|
|
|
9
9
|
|
|
10
10
|
const manager = new SessionManager();
|
|
11
11
|
|
|
12
|
-
const r = await manager.createSession({ kernelType: 'r',
|
|
12
|
+
const r = await manager.createSession({ kernelType: 'r', workingDirectory: '/projects/analysis' });
|
|
13
13
|
const result = await r.execute('x <- 1:10; mean(x)');
|
|
14
14
|
console.log(result.success, result.output);
|
|
15
15
|
|
|
16
|
-
const py = await manager.createSession({ kernelType: 'python'
|
|
16
|
+
const py = await manager.createSession({ kernelType: 'python' });
|
|
17
17
|
console.log((await py.execute('sum(range(1, 11))')).success);
|
|
18
18
|
|
|
19
19
|
await manager.stopAll();
|
|
@@ -36,17 +36,13 @@ The pieces are named after moons of Jupiter:
|
|
|
36
36
|
npm install @damurka/jovian
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
The package ships **prebuilt** `themisto`, `elara` and `carpo` binaries — no compiler, CMake or vcpkg — for **Windows x64**
|
|
39
|
+
The package ships **prebuilt** `themisto`, `elara` and `carpo` binaries — no compiler, CMake or vcpkg — for **Windows x64**, **Linux x64 and arm64**, and **macOS x64 (Intel) and arm64 (Apple Silicon)**. Windows on ARM and 32-bit ARM Linux are not supported (there is no ARM64 R for Windows yet); build from source elsewhere and point `JOVIAN_NATIVE_DIR` at `dist/native/Release`. npm installs the matching `@damurka/jovian-<os>-<cpu>` package automatically as an optional dependency, so do not install with `--omit=optional` / `--no-optional`. Node.js ≥ 22.13 is required. The package is an ES module: use `import` (in a project with `"type": "module"`, or `.mjs`/`.mts` files), or `require()` it from CommonJS on Node 22.13+.
|
|
40
40
|
|
|
41
41
|
What you must already have on the machine:
|
|
42
42
|
|
|
43
|
-
- **R** (4.2 or newer; a build with a shared library, which the CRAN/Posit binaries and distribution packages are) for R sessions.
|
|
43
|
+
- **R** (4.2 or newer; a build with a shared library, which the CRAN/Posit binaries and distribution packages are) for R sessions. If `rHome` is not passed, it is found from `$R_HOME`, then `R RHOME` (R on `PATH`), then the Windows registry; pass `rHome` to choose a specific installation. You do not need to install any R packages yourself. The `hera` R package every R session needs ships inside the npm package, and before the **first** R session the library installs it, together with its CRAN dependencies (`cli`, `evaluate`, `glue`, `IRdisplay`, `jsonlite`, `R6`, `repr`, `rlang` and what they need), into your R library. Nothing else (not even `remotes`) has to be installed first. This needs an internet connection, takes about 20 seconds where CRAN has binaries (Windows, macOS) and a few minutes on Linux, where packages are compiled from source and need a compiler (Ubuntu: `sudo apt install build-essential`). It happens once; later sessions start straight away. A package that is installed but cannot be loaded (Debian/Ubuntu `r-cran-*` packages built for an older R fail with `undefined symbol: SETLENGTH`) is reinstalled from CRAN into your own library. If it cannot finish, `createSession()` rejects with R's own reason. Set `JOVIAN_SKIP_R_SETUP=1` to skip this step and manage the packages yourself.
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
install.packages(c("remotes", "cli", "evaluate", "glue", "IRdisplay", "jsonlite", "R6", "repr", "rlang"))
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
- **Python 3** with its shared library (optional, for Python sessions); pass `pythonHome` (`python3 -c "import sys; print(sys.prefix)"`).
|
|
45
|
+
- **Python 3** with its shared library (optional, for Python sessions); if `pythonHome` is not passed, it is found from `$PYTHONHOME`, then the first `python3` / `python` on `PATH` (its `sys.base_prefix`); pass `pythonHome` to choose one.
|
|
50
46
|
- **Linux:** `libuuid` (`libuuid1`, present on nearly every system) and a glibc at least as new as the one the binaries were built against (Ubuntu 24.04's, 2.39). On an older distribution, [build from source](#requirements).
|
|
51
47
|
- **macOS:** 14 or newer.
|
|
52
48
|
- **Windows:** the [Microsoft Visual C++ Redistributable](https://learn.microsoft.com/cpp/windows/latest-supported-vc-redist) (x64, 2015–2022) — the binaries use the dynamic C++ runtime; most machines already have it.
|
|
@@ -117,9 +113,9 @@ Jovian builds C++ (Adrastea, Elara, Carpo, Themisto) and TypeScript. R and Pytho
|
|
|
117
113
|
| Variable | Used by | Meaning |
|
|
118
114
|
|---|---|---|
|
|
119
115
|
| `VCPKG_ROOT` | build | vcpkg checkout; used by `npm run build` and the CMake presets. |
|
|
120
|
-
| `R_HOME` | runtime, tests, examples | R installation to use when `rHome` is not passed
|
|
116
|
+
| `R_HOME` | runtime, tests, examples | R installation to use when `rHome` is not passed; otherwise the library asks `R RHOME`. |
|
|
121
117
|
| `R_PATH`, `R_LIBS` | examples, playground | Passed as `rPath` / `rLibs`. |
|
|
122
|
-
| `PYTHONHOME` | runtime, tests | Python installation prefix when `pythonHome` is not passed. |
|
|
118
|
+
| `PYTHONHOME` | runtime, tests | Python installation prefix when `pythonHome` is not passed; otherwise the library asks `python3` / `python` for its `sys.base_prefix`. |
|
|
123
119
|
| `JOVIAN_NATIVE_DIR` | `lib/` | Directory holding `themisto`, `elara` and `carpo`. Default: the installed `@damurka/jovian-<os>-<cpu>` package, else `dist/native/Release` in a source checkout. Use it to run against a *copy* of the binaries (Windows will not let you overwrite a running `.exe`). |
|
|
124
120
|
| `ELARA_HERA_SRC` | Elara | Set for you from the `heraSrcPath` option: where Elara installs `hera` from if it is missing or older than the source. |
|
|
125
121
|
|
package/docs/api/README.md
CHANGED
|
@@ -21,9 +21,18 @@ import {
|
|
|
21
21
|
Creates sessions and owns the one shared supervisor process (`themisto`) they all talk to. The supervisor is spawned lazily on the first `createSession()` and killed by `stopAll()` / `killAll()` (and, as a safety net, when the Node process exits).
|
|
22
22
|
|
|
23
23
|
```typescript
|
|
24
|
-
const manager = new SessionManager();
|
|
24
|
+
const manager = new SessionManager(); // quiet
|
|
25
|
+
const verbose = new SessionManager({ logLevel: 'debug' }); // everything, plus the kernels' own output
|
|
25
26
|
```
|
|
26
27
|
|
|
28
|
+
**Logging.** The library is quiet by default: it prints only one-time setup notices (the install of the R packages on the first R session), warnings and errors. A kernel that fails to start reports its own error in the exception's message (`Kernel output: …`). To see more:
|
|
29
|
+
|
|
30
|
+
| Option / variable | Effect |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `logLevel: 'trace' \| 'debug' \| 'info' \| 'notice' \| 'warn' \| 'error' \| 'silent'` (or env `JOVIAN_LOG_LEVEL`) | What the built-in console logger prints: this level and above. Default `'notice'`. `'debug'` and `'trace'` also print the kernels' start-up output. |
|
|
33
|
+
| `logger: (level, message, data?) => void` | Receives **every** message (do your own filtering) instead of the console. A session's own `logger` option takes precedence for that session. |
|
|
34
|
+
| `kernelOutput: boolean` (or env `JOVIAN_KERNEL_OUTPUT=1`) | Print the kernels' own `[elara]` / `[carpo]` start-up output to stderr as it happens. Default off (on for `debug`/`trace`). |
|
|
35
|
+
|
|
27
36
|
| Member | Description |
|
|
28
37
|
|---|---|
|
|
29
38
|
| `createSession(options?: EngineOptions): Promise<Session>` | Spawns a kernel process (per `options.kernelType`), waits until it has registered and the session's WebSocket is ready, and resolves with the `Session`. Rejects with the supervisor's message if the kernel could not start: `workingDirectory does not exist or is not a directory: …`, `no kernel executable is configured for kernelType 'python' …`, or — when the kernel process itself failed (R or Python not found, …) — `Kernel process exited before it could register -- check its stderr output for the actual error.` The *actual* R/Python error is printed on stderr with an `[elara]` / `[carpo]` prefix (see [Troubleshooting](../troubleshooting.md)). |
|
package/docs/api/types.md
CHANGED
|
@@ -9,12 +9,12 @@ Passed to `SessionManager.createSession()`; also `Partial<EngineOptions>` to `Se
|
|
|
9
9
|
| Field | Type | Default | Meaning |
|
|
10
10
|
|---|---|---|---|
|
|
11
11
|
| `kernelType` | `'r'` \| `'python'` | `'r'` | Which kernel the session runs. Selects which of the fields below apply and which executable (`elara` / `carpo`) is spawned. |
|
|
12
|
-
| `rHome` | string |
|
|
12
|
+
| `rHome` | string | discovered | R installation (`R RHOME`). When omitted it is found from `$R_HOME`, then `R RHOME` (R on PATH), then the Windows registry; pass it to pick a specific installation. |
|
|
13
13
|
| `rPath` | string | `<rHome>/bin/x64` (Windows) | Directory containing `R.dll`; put on the kernel's `PATH`. |
|
|
14
14
|
| `rLibs` | string | — | Extra library path (`R_LIBS`, `R_LIBS_USER`, and `R_LIBS_SITE` on Windows); where `hera` is looked up and installed. |
|
|
15
15
|
| `pandocPath` | string | — | Directory with a `pandoc` binary, for bundled R installs that do not ship it on `PATH` (`RSTUDIO_PANDOC`). |
|
|
16
16
|
| `heraSrcPath` | string | — | Source directory of the `hera` package. If set, Elara installs it (`remotes::install_local`, needs `remotes`) when it is missing or older than the source. **There is no built-in default**: if `hera` is not installed and this is unset, R code cannot run (see [Troubleshooting](../troubleshooting.md#hera-is-not-installed)). |
|
|
17
|
-
| `pythonHome` | string |
|
|
17
|
+
| `pythonHome` | string | discovered | Python installation prefix (`PYTHONHOME`). Its shared library is loaded from here. When omitted it is found from `$PYTHONHOME`, then `python3` / `python` on PATH (`sys.base_prefix`). |
|
|
18
18
|
| `pythonPath` | string | — | Extra `PYTHONPATH`. |
|
|
19
19
|
| `venvPath` | string | — | A venv whose `site-packages` is added to `sys.path`. `pythonHome` must still point at the *base* install. |
|
|
20
20
|
| `workingDirectory` | string | supervisor's cwd | Directory the kernel process starts in (`getwd()` / `os.getcwd()`); relative paths resolve against it. Must exist, or `createSession()` rejects with `workingDirectory does not exist or is not a directory: <path>`. Kept across `restart()`. |
|
package/docs/development.md
CHANGED
|
@@ -88,8 +88,8 @@ The npm packages are built and published by `.github/workflows/release.yml` from
|
|
|
88
88
|
|
|
89
89
|
## Debugging
|
|
90
90
|
|
|
91
|
-
- **Kernel logs.** Everything a kernel prints (`[R Interpreter] …`, `[carpo] …`, and anything R/Python writes outside an execution) appears on the *supervisor's* stderr,
|
|
92
|
-
- **Library logs.**
|
|
91
|
+
- **Kernel logs.** Everything a kernel prints (`[R Interpreter] …`, `[carpo] …`, and anything R/Python writes outside an execution) appears on the *supervisor's* stderr, prefixed `[elara]` / `[carpo]`. `lib/` keeps it quiet by default (and puts the relevant lines in the error when a kernel fails to start); set `JOVIAN_LOG_LEVEL=debug` (or `JOVIAN_KERNEL_OUTPUT=1`, or `new SessionManager({ kernelOutput: true })`) to forward it to your stderr. That is the first place to look when a session fails to start.
|
|
92
|
+
- **Library logs.** Quiet by default; `JOVIAN_LOG_LEVEL=trace` (or `new SessionManager({ logLevel: 'trace' })`) shows queueing, request ids and timeouts, and `logger` receives every message.
|
|
93
93
|
- **Run a kernel by hand.** Generate a kernelspec (`npm run jupyter:kernelspec`) and start it from `jupyter console --kernel elara`, or run `elara -f <connection-file> --r-home …` yourself — no supervisor involved.
|
|
94
94
|
- **Talk to the supervisor directly.** Start `themisto` (it prints `{"type":"supervisorReady","httpPort":…,"wsPort":…}`) and use `curl` against [its HTTP API](protocol.md#2-themistos-http-api) and any WebSocket client against `ws://127.0.0.1:<wsPort>/sessions/<id>/messages`.
|
|
95
95
|
- **One native test.** `dist/native/Release/session_registry_test.exe --gtest_filter=*Interrupt*` (kernel log noise goes to the same stdout; filter with `grep -v "^\[elara\]"`).
|
package/docs/getting-started.md
CHANGED
|
@@ -55,7 +55,7 @@ import { SessionManager } from './dist/lib/index.js';
|
|
|
55
55
|
const manager = new SessionManager();
|
|
56
56
|
const session = await manager.createSession({
|
|
57
57
|
kernelType: 'r',
|
|
58
|
-
rHome: process.env.R_HOME, //
|
|
58
|
+
rHome: process.env.R_HOME, // optional: found from $R_HOME / `R RHOME` / the Windows registry when omitted
|
|
59
59
|
rPath: process.env.R_PATH, // Windows only: e.g. "C:/Program Files/R/R-4.6.0/bin/x64"
|
|
60
60
|
workingDirectory: process.cwd(), // where getwd() will point
|
|
61
61
|
});
|
|
@@ -78,7 +78,7 @@ await manager.stopAll(); // always: it also end
|
|
|
78
78
|
```javascript
|
|
79
79
|
const py = await manager.createSession({
|
|
80
80
|
kernelType: 'python',
|
|
81
|
-
pythonHome: process.env.PYTHONHOME, //
|
|
81
|
+
pythonHome: process.env.PYTHONHOME, // optional: found from $PYTHONHOME / python3 / python when omitted (the prefix containing libpython)
|
|
82
82
|
workingDirectory: process.cwd(),
|
|
83
83
|
});
|
|
84
84
|
py.on('error', () => {});
|
package/docs/releasing.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Releasing
|
|
2
2
|
|
|
3
|
-
Jovian is published to npm as
|
|
3
|
+
Jovian is published to npm as six packages under the `@damurka` scope, one library and five platform packages (Windows x64, Linux x64 and arm64, macOS x64 and arm64; see [Platforms that are not supported](#platforms-that-are-not-supported)):
|
|
4
4
|
|
|
5
5
|
| Package | Contents |
|
|
6
6
|
|---|---|
|
|
7
7
|
| `@damurka/jovian` | The compiled TypeScript library, the `hera` R package, docs. Lists the platform packages below as `optionalDependencies`. This is the one users install. |
|
|
8
8
|
| `@damurka/jovian-win32-x64` | `themisto.exe`, `elara.exe`, `carpo.exe` and the DLLs they need. |
|
|
9
|
-
| `@damurka/jovian-linux-x64`, `-darwin-arm64` | `themisto`, `elara`, `carpo`. |
|
|
9
|
+
| `@damurka/jovian-linux-x64`, `-linux-arm64`, `-darwin-x64`, `-darwin-arm64` | `themisto`, `elara`, `carpo`. |
|
|
10
10
|
|
|
11
11
|
Each platform package declares `os` and `cpu`, so npm installs only the one that matches the machine. At run time the library finds the binaries in that package (see [`lib/session/native-paths.ts`](../lib/session/native-paths.ts): `JOVIAN_NATIVE_DIR`, then the platform package, then a source checkout's `dist/native/Release`). All four packages are published at the **same version**; the main package pins the platform packages to it.
|
|
12
12
|
|
|
@@ -15,10 +15,25 @@ The repository's own `package.json` is `"private": true` — it is the developme
|
|
|
15
15
|
## One-time setup
|
|
16
16
|
|
|
17
17
|
1. **The scope.** `@damurka` must be a user or organization you can publish to on npmjs.com. If you use a different scope, change it in two places — `SCOPE` in `scripts/release.mjs` and `PACKAGE_SCOPE` in `lib/session/native-paths.ts` (a unit test fails if they differ) — plus the names in `README.md`, `docs/`, and the tarball globs in `.github/workflows/release.yml`.
|
|
18
|
-
2. **A token.** On npmjs.com create an *automation* access token (or a granular token with read/write on the `@damurka` packages) and add it to the GitHub repository as the secret **`NPM_TOKEN`** (Settings → Secrets and variables → Actions).
|
|
18
|
+
2. **A token.** On npmjs.com create an *automation* access token (or a granular token with read/write on the `@damurka` packages) and add it to the GitHub repository as the secret **`NPM_TOKEN`** (Settings → Secrets and variables → Actions). Staging needs no 2FA and works with any token type (npm also offers stage-only tokens, which cannot publish directly); the 2FA happens when you approve. A token that still asks for a one-time password on a direct `npm publish` fails in CI with `EOTP`.
|
|
19
19
|
3. Scoped packages are private by default on npm; the packages carry `publishConfig.access: public`, and the workflow passes `--access public`.
|
|
20
20
|
4. The workflow publishes with **provenance** (`--provenance`, needs the `id-token: write` permission it declares), which requires the GitHub repository to be public.
|
|
21
21
|
|
|
22
|
+
## The first release is published by hand
|
|
23
|
+
|
|
24
|
+
npm's staged publishing (below) only works for a package that **already exists** on the registry; a first-time publish of a new package cannot be staged. So the very first version (`0.1.0`) is published from your machine, where npm can ask for your 2FA code:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm login
|
|
28
|
+
# the tarballs CI built and smoke-tested (download them from the Release run's artifacts, or `gh run download <run-id>`):
|
|
29
|
+
npm publish ./tarballs-win32-x64/damurka-jovian-win32-x64-0.1.0.tgz --access public
|
|
30
|
+
npm publish ./tarballs-linux-x64/damurka-jovian-linux-x64-0.1.0.tgz --access public
|
|
31
|
+
npm publish ./tarballs-darwin-arm64/damurka-jovian-darwin-arm64-0.1.0.tgz --access public
|
|
32
|
+
npm publish ./tarballs-linux-x64/damurka-jovian-0.1.0.tgz --access public # last: it depends on the three above
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The main package goes last so nothing depends on a version that is not there yet. This first version carries no provenance badge. From the next version on, use the workflow.
|
|
36
|
+
|
|
22
37
|
## Cutting a release
|
|
23
38
|
|
|
24
39
|
1. Make sure `main` is green on CI.
|
|
@@ -31,7 +46,8 @@ The repository's own `package.json` is `"private": true` — it is the developme
|
|
|
31
46
|
```
|
|
32
47
|
|
|
33
48
|
4. `release.yml` runs. For each platform it builds the native binaries in Release, compiles the library, stages both packages, packs them, and **smoke-tests the packed tarballs**: `scripts/release-smoke.mjs` installs the platform tarball and the main tarball into an empty project (outside the repository, with an empty R library so the bundled `hera` has to install from the package) and starts a real R kernel and a real Python kernel from them.
|
|
34
|
-
5. Only if every platform passed does the `publish` job run
|
|
49
|
+
5. Only if every platform passed does the `publish` job run. It **stages** the packages with `npm stage publish` (npm requires this for CI; needs npm ≥ 11.15, which the job installs), platform packages first, then the main package. Nothing is public yet.
|
|
50
|
+
6. **Approve them**, with 2FA, in the same order: `npm stage list` shows the queue, `npm stage approve <stage-id>` publishes one (or approve on npmjs.com). Approve the main package last. `npm stage reject <stage-id>` discards one.
|
|
35
51
|
|
|
36
52
|
A version with a hyphen (`v0.2.0-rc.1`) is published under the `next` dist-tag, so it does not become what `npm install` picks by default.
|
|
37
53
|
|
|
@@ -53,20 +69,26 @@ node scripts/release-smoke.mjs --dir dist/release/tarballs --python
|
|
|
53
69
|
|
|
54
70
|
Nothing there publishes. Publishing by hand is `npm publish <tarball> --access public`, platform packages first.
|
|
55
71
|
|
|
56
|
-
##
|
|
72
|
+
## Platforms that are not supported
|
|
73
|
+
|
|
74
|
+
A platform is published only when there is a real R for it to pair with and a runner that can build and smoke-test it end to end.
|
|
75
|
+
|
|
76
|
+
- **Windows on ARM (`win32-arm64`).** R for Windows on ARM64 is experimental and not on CRAN (no binary packages either), so there is no R for a native ARM64 kernel to load. People on Windows on ARM run x64 R under emulation, which would need the x64 kernel, but npm installs the x64 package only on x64 CPUs. Not supported until R for Windows on ARM64 is released.
|
|
77
|
+
- **32-bit ARM Linux (`linux-armhf`).** GitHub's hosted runners cannot run 32-bit ARM code (the arm64 machines lack AArch32), so it can only be built by cross-compiling and tested under QEMU emulation, which is not set up.
|
|
57
78
|
|
|
58
|
-
|
|
79
|
+
Their users get `jovian: there are no prebuilt kernels for <os>-<cpu>` and can build from source and set `JOVIAN_NATIVE_DIR`.
|
|
80
|
+
|
|
81
|
+
## Adding a platform
|
|
59
82
|
|
|
60
83
|
1. Add the target to `TARGETS` in `scripts/release.mjs` and to `SUPPORTED_PLATFORMS` in `lib/session/native-paths.ts` (a unit test fails if they differ).
|
|
61
|
-
2. Add it to the build matrix in `.github/workflows/release.yml
|
|
84
|
+
2. Add it to the build matrix in `.github/workflows/release.yml`.
|
|
62
85
|
3. Run the workflow by hand (a dry run) and fix what the smoke test finds before tagging a release.
|
|
63
|
-
|
|
64
|
-
Until a platform is added, its users get `jovian: there are no prebuilt kernels for <os>-<cpu>` and can build from source and set `JOVIAN_NATIVE_DIR`.
|
|
86
|
+
4. A new platform is a **new package on npm**, and npm cannot stage the first version of a package: publish it by hand once (see [The first release is published by hand](#the-first-release-is-published-by-hand)) before the main package that depends on it is approved. The workflow prints a warning naming each such package and does not fail.
|
|
65
87
|
|
|
66
88
|
## Things to know
|
|
67
89
|
|
|
68
90
|
- **hera upgrades.** npm resets file modification times, so the kernel's usual "is the hera source newer than the installed one" check cannot fire for an npm install. The staged `hera` `DESCRIPTION` is stamped with `Config/jovian/release: <version>`, and the kernel reinstalls `hera` when that stamp differs from the installed copy's. Users therefore get the matching `hera` after upgrading the package, once.
|
|
69
|
-
- **Linux binaries and glibc.** They are built on `ubuntu-24.04` (glibc 2.39) and run on any distribution with at least that glibc. Building on an older image would widen compatibility;
|
|
91
|
+
- **Linux binaries and glibc.** They are built on `ubuntu-24.04` (glibc 2.39) and run on any distribution with at least that glibc. Building on an older image would widen compatibility; the `linux-arm64` build runs on `ubuntu-24.04-arm`.
|
|
70
92
|
- **macOS.** The build sets `MACOSX_DEPLOYMENT_TARGET=14.0`. The binaries are not code-signed or notarized; binaries installed through npm are not quarantined by Gatekeeper, so this works, but bundling them into a downloaded `.app` would require signing.
|
|
71
93
|
- **Windows.** The DLLs come from vcpkg's `x64-windows` triplet and are copied next to the executables; the binaries link the dynamic Visual C++ runtime (`MSVCP140.dll`, `VCRUNTIME140.dll`, checked with `dumpbin /dependents`), which is **not** bundled: users need the "Microsoft Visual C++ Redistributable" (x64, 2015–2022), and the README's Install section says so. Bundling the runtime DLLs into the package, or linking the runtime statically, would remove that requirement.
|
|
72
94
|
- **Executable bit.** npm does not reliably preserve it; the library `chmod`s the kernels before starting them.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -21,13 +21,13 @@ Messages below are quoted from the code. **First habit:** a kernel that fails to
|
|
|
21
21
|
| Message | Meaning and fix |
|
|
22
22
|
|---|---|
|
|
23
23
|
| `jovian: the kernel binaries were not found. Expected the '@damurka/jovian-<os>-<cpu>' package …` | The platform package was not installed: it is an optional dependency, so `npm install --omit=optional` / `--no-optional` skips it (reinstall without the flag), or a lockfile made on another OS omitted it (`npm install` on this OS). In a source checkout: run `npm run build`, or set `$JOVIAN_NATIVE_DIR`. |
|
|
24
|
-
| `jovian: there are no prebuilt kernels for <os>-<cpu> (supported: …)` | Prebuilt packages exist for `win32-x64`, `linux-x64` and `darwin-arm64` only (
|
|
24
|
+
| `jovian: there are no prebuilt kernels for <os>-<cpu> (supported: …)` | Prebuilt packages exist for `win32-x64`, `linux-x64`, `linux-arm64`, `darwin-x64` and `darwin-arm64` only (Windows on ARM and 32-bit ARM Linux are not supported). Elsewhere, [build from source](../README.md#requirements) and set `$JOVIAN_NATIVE_DIR` to `dist/native/Release`. |
|
|
25
25
|
| `version 'GLIBC_2.xx' not found` on Linux | The prebuilt Linux binaries need a glibc at least as new as the one they were built with (Ubuntu 24.04: 2.39). Build from source on the older system. |
|
|
26
26
|
| `Supervisor process exited before it was ready (code 1)` | `themisto` refused to start — typically `[themisto] FATAL: kernel executable not found at … (pass --kernel-exe to override)`: `elara` is missing next to `themisto`. |
|
|
27
27
|
| `no kernel executable is configured for kernelType 'python' …` | `carpo` was not built (or not found beside `themisto`). It is built by default; check `dist/native/Release/carpo[.exe]` and Themisto's `[themisto] NOTE: no Python kernel executable found` line. R sessions are unaffected. |
|
|
28
28
|
| `workingDirectory does not exist or is not a directory: <path>` | Create the directory first, or fix the path. |
|
|
29
|
-
| `Kernel process exited before it could register -- check its stderr output for the actual error.` | The kernel crashed at start-up.
|
|
30
|
-
| `Did not receive kernel configuration within 60s -- the kernel process is still running but never registered. Check its stderr output for what it's doing.` | The kernel started but hung before registering — e.g. R blocked loading packages, a very slow disk, or a security product scanning the process.
|
|
29
|
+
| `Kernel process exited before it could register -- check its stderr output for the actual error.` | The kernel crashed at start-up. The exception's message ends with `Kernel output:` and the kernel's own error lines; for the full output run with `JOVIAN_LOG_LEVEL=debug` (or `new SessionManager({ kernelOutput: true })`). See the R / Python sections below. |
|
|
30
|
+
| `Did not receive kernel configuration within 60s -- the kernel process is still running but never registered. Check its stderr output for what it's doing.` | The kernel started but hung before registering — e.g. R blocked loading packages, a very slow disk, or a security product scanning the process. Run with `JOVIAN_LOG_LEVEL=debug` to print the kernel's output and see how far it got. |
|
|
31
31
|
| `WebSocket connection to session <id> failed` / `Session <id> closed before it was ready` | The supervisor accepted the session but the WebSocket could not be established/kept — usually the supervisor died. Look for its exit. |
|
|
32
32
|
|
|
33
33
|
### R
|
|
@@ -35,7 +35,12 @@ Messages below are quoted from the code. **First habit:** a kernel that fails to
|
|
|
35
35
|
| Symptom | Cause / fix |
|
|
36
36
|
|---|---|
|
|
37
37
|
| `Could not load R.dll (…). Is R installed? Checked PATH and R_HOME=… Install R from https://cran.r-project.org, or make sure R_HOME/the R bin directory is configured correctly.` | Windows: `rHome` wrong, or `rPath` (the folder containing `R.dll`, normally `<R_HOME>\bin\x64`) is not right. |
|
|
38
|
-
| `R_HOME is not set -- elara needs a working R installation to run. …` |
|
|
38
|
+
| `R_HOME is not set -- elara needs a working R installation to run. …` | The library could not find R (it tries `$R_HOME`, `R RHOME`, and on Windows the registry): put R on `PATH` or pass `rHome` (`R RHOME` prints it). |
|
|
39
|
+
| `No libpython3.*.so* … was found under '/lib' … Is Python installed at ''?` | The library could not find Python (it tries `$PYTHONHOME`, then `python3`/`python` on `PATH`): install Python 3 (`sudo apt install python3`) or pass `pythonHome`. |
|
|
40
|
+
| `Could not set up the R packages the kernel needs: …` | The one-time install of `hera` and its dependencies (see the README's Install section) failed; the text after the colon is R's own reason (no internet access to CRAN, no compiler on Linux, a library that cannot be written to). Fix that and call `createSession()` again; it retries. `JOVIAN_SKIP_R_SETUP=1` skips the step. |
|
|
41
|
+
| `ERROR: failed to lock directory '<library>' for modifying. Try removing '<library>/00LOCK-<pkg>'` | An earlier install of that package was interrupted (Ctrl+C, a kill, a timeout) and left its lock folder, which makes R refuse every later install of it. The automatic setup removes locks older than 5 minutes itself; to clear one now: `rm -rf <library>/00LOCK-*` (Windows: delete the `00LOCK-*` folder), then run again. |
|
|
42
|
+
| `unable to load shared object '/usr/lib/R/site-library/<pkg>/libs/<pkg>.so': undefined symbol: SETLENGTH` (or another `undefined symbol`) | Only when the automatic setup is skipped or you install by hand. Debian/Ubuntu: a package installed with `apt` (`r-cran-*`, in `/usr/lib/R/site-library`) was built for an older R than yours (the automatic setup reinstalls these itself). Reinstall it, and everything it needs, from CRAN into your own library, which R searches first. Needs a compiler (`sudo apt install build-essential`):<br>`wanted <- c("remotes","cli","evaluate","glue","IRdisplay","jsonlite","R6","repr","rlang")`<br>`all <- unique(c(wanted, unlist(tools::package_dependencies(wanted, db = available.packages(), recursive = TRUE), use.names = FALSE)))`<br>`ip <- installed.packages(); install.packages(setdiff(all, rownames(ip)[!is.na(ip[, "Priority"])]), repos = "https://cloud.r-project.org")` |
|
|
43
|
+
| `WARNING: 'hera' package could not be loaded (status: install_failed: …)` | Elara could not install the bundled `hera` R package; the text after `install_failed:` is R's own reason. Usually one of `hera`'s CRAN dependencies is missing or cannot be built (install them first: `install.packages(c("remotes", "cli", "evaluate", "glue", "IRdisplay", "jsonlite", "R6", "repr", "rlang"))`), or the R library is not writable. To see the full output run `R CMD INSTALL node_modules/@damurka/jovian/packages/hera`. |
|
|
39
44
|
| `Could not load …/lib/libR.so (…). Is R installed at '…'? If this R was built from source, it needs to have been configured with --enable-R-shlib, or no libR.so exists at all` | Use a distribution/CRAN R, or rebuild R with `--enable-R-shlib`. |
|
|
40
45
|
| No `libR.dylib` on macOS | Point `rHome` at the framework's `Resources` directory (what `R RHOME` prints). |
|
|
41
46
|
|
|
@@ -141,7 +141,7 @@ export class ExecutionQueue {
|
|
|
141
141
|
break;
|
|
142
142
|
case 'error':
|
|
143
143
|
pending.output.push(message);
|
|
144
|
-
this.logger?.
|
|
144
|
+
this.logger?.debug(`Execution ${message.parentMsgId} reported an R error`, { evalue: message.content?.evalue });
|
|
145
145
|
if (pending.stopOnError) {
|
|
146
146
|
this.abortQueued();
|
|
147
147
|
}
|
|
@@ -7,7 +7,7 @@ export declare const PACKAGE_NAME = "@damurka/jovian";
|
|
|
7
7
|
* the main package only where it matches (each declares `os`/`cpu`). These are
|
|
8
8
|
* the platforms that get a package.
|
|
9
9
|
*/
|
|
10
|
-
export declare const SUPPORTED_PLATFORMS: readonly ['win32-x64', 'linux-x64', 'darwin-arm64'];
|
|
10
|
+
export declare const SUPPORTED_PLATFORMS: readonly ['win32-x64', 'linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64'];
|
|
11
11
|
export declare function platformPackageName(platform?: string, arch?: string): string | undefined;
|
|
12
12
|
export interface NativeLocation {
|
|
13
13
|
/** Directory holding themisto, elara and carpo. */
|
|
@@ -11,7 +11,7 @@ export const PACKAGE_NAME = `${PACKAGE_SCOPE}/jovian`;
|
|
|
11
11
|
* the main package only where it matches (each declares `os`/`cpu`). These are
|
|
12
12
|
* the platforms that get a package.
|
|
13
13
|
*/
|
|
14
|
-
export const SUPPORTED_PLATFORMS = ['win32-x64', 'linux-x64', 'darwin-arm64'];
|
|
14
|
+
export const SUPPORTED_PLATFORMS = ['win32-x64', 'linux-x64', 'linux-arm64', 'darwin-x64', 'darwin-arm64'];
|
|
15
15
|
export function platformPackageName(platform = process.platform, arch = process.arch) {
|
|
16
16
|
const key = `${platform}-${arch}`;
|
|
17
17
|
return SUPPORTED_PLATFORMS.includes(key) ? `${PACKAGE_NAME}-${key}` : undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EngineOptions } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Runs once, before the first R session, when the library was given a copy of
|
|
4
|
+
* the 'hera' R package (always the case for an npm install): makes sure hera
|
|
5
|
+
* and everything it needs are installed, so a person who has just installed R
|
|
6
|
+
* has nothing to do by hand.
|
|
7
|
+
*
|
|
8
|
+
* It is a separate step, not part of the kernel's start-up, because the
|
|
9
|
+
* supervisor gives a kernel 60 seconds to register and installing a dozen
|
|
10
|
+
* packages (compiling some, on Linux) takes longer. A kernel started after
|
|
11
|
+
* this finds hera already there.
|
|
12
|
+
*
|
|
13
|
+
* Only what is missing is installed, into the first writable library, and
|
|
14
|
+
* nothing is installed at all when hera is already current. A package that is
|
|
15
|
+
* installed but cannot be loaded (Debian/Ubuntu `r-cran-*` packages built for
|
|
16
|
+
* an older R fail with "undefined symbol: SETLENGTH") counts as missing and is
|
|
17
|
+
* reinstalled from CRAN into the user's own library, which R searches first.
|
|
18
|
+
*/
|
|
19
|
+
export declare const R_SETUP_SCRIPT: string;
|
|
20
|
+
export interface SetupLogger {
|
|
21
|
+
debug(message: string): void;
|
|
22
|
+
info(message: string): void;
|
|
23
|
+
notice(message: string): void;
|
|
24
|
+
}
|
|
25
|
+
export interface SetupProcessOutcome {
|
|
26
|
+
code: number | null;
|
|
27
|
+
timedOut: boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface SetupDeps {
|
|
30
|
+
env: NodeJS.ProcessEnv;
|
|
31
|
+
platform: string;
|
|
32
|
+
exists: (path: string) => boolean;
|
|
33
|
+
/** Runs `rscript scriptFile ...args`, reporting each stdout/stderr line. */
|
|
34
|
+
run: (rscript: string, scriptFile: string, args: string[], env: NodeJS.ProcessEnv, onLine: (line: string) => void, timeoutMs: number) => Promise<SetupProcessOutcome>;
|
|
35
|
+
timeoutMs: number;
|
|
36
|
+
}
|
|
37
|
+
export declare function rscriptPath(rHome: string, deps?: Pick<SetupDeps, 'platform' | 'exists'>): string | undefined;
|
|
38
|
+
/**
|
|
39
|
+
* Makes sure hera and its dependencies are installed for this R (see the
|
|
40
|
+
* comment on R_SETUP_SCRIPT). Resolves immediately when there is nothing to
|
|
41
|
+
* do: a Python session, no bundled hera (a source checkout), no R found, or
|
|
42
|
+
* JOVIAN_SKIP_R_SETUP set. Concurrent calls for the same R share one run, and
|
|
43
|
+
* a run that succeeded is not repeated by this process. Rejects with R's own
|
|
44
|
+
* explanation when the packages could not be installed.
|
|
45
|
+
*/
|
|
46
|
+
export declare function ensureRPackages(options: EngineOptions, logger: SetupLogger, deps?: SetupDeps): Promise<void>;
|
|
47
|
+
//# sourceMappingURL=r-setup.d.ts.map
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
/**
|
|
8
|
+
* Runs once, before the first R session, when the library was given a copy of
|
|
9
|
+
* the 'hera' R package (always the case for an npm install): makes sure hera
|
|
10
|
+
* and everything it needs are installed, so a person who has just installed R
|
|
11
|
+
* has nothing to do by hand.
|
|
12
|
+
*
|
|
13
|
+
* It is a separate step, not part of the kernel's start-up, because the
|
|
14
|
+
* supervisor gives a kernel 60 seconds to register and installing a dozen
|
|
15
|
+
* packages (compiling some, on Linux) takes longer. A kernel started after
|
|
16
|
+
* this finds hera already there.
|
|
17
|
+
*
|
|
18
|
+
* Only what is missing is installed, into the first writable library, and
|
|
19
|
+
* nothing is installed at all when hera is already current. A package that is
|
|
20
|
+
* installed but cannot be loaded (Debian/Ubuntu `r-cran-*` packages built for
|
|
21
|
+
* an older R fail with "undefined symbol: SETLENGTH") counts as missing and is
|
|
22
|
+
* reinstalled from CRAN into the user's own library, which R searches first.
|
|
23
|
+
*/
|
|
24
|
+
export const R_SETUP_SCRIPT = String.raw `
|
|
25
|
+
src <- commandArgs(trailingOnly = TRUE)[1]
|
|
26
|
+
say <- function(...) cat("JOVIAN_R_SETUP: ", ..., "\n", sep = "")
|
|
27
|
+
note <- function(...) cat("JOVIAN_R_SETUP_INFO: ", ..., "\n", sep = "")
|
|
28
|
+
fail <- function(...) {
|
|
29
|
+
cat("JOVIAN_R_SETUP_ERROR: ", ..., "\n", sep = "")
|
|
30
|
+
quit(save = "no", status = 1)
|
|
31
|
+
}
|
|
32
|
+
field <- function(path, name) {
|
|
33
|
+
value <- tryCatch(read.dcf(path, fields = name)[1, 1], error = function(e) NA_character_)
|
|
34
|
+
if (is.na(value)) NA_character_ else value
|
|
35
|
+
}
|
|
36
|
+
stamp_wanted <- field(file.path(src, "DESCRIPTION"), "Config/jovian/release")
|
|
37
|
+
|
|
38
|
+
hera_current <- function() {
|
|
39
|
+
installed <- tryCatch(find.package("hera", quiet = TRUE), error = function(e) character(0))
|
|
40
|
+
if (!length(installed)) return(FALSE)
|
|
41
|
+
if (!is.na(stamp_wanted) && !identical(field(file.path(installed, "DESCRIPTION"), "Config/jovian/release"), stamp_wanted)) return(FALSE)
|
|
42
|
+
suppressWarnings(suppressMessages(requireNamespace("hera", quietly = TRUE)))
|
|
43
|
+
}
|
|
44
|
+
if (hera_current()) {
|
|
45
|
+
note("hera is already installed")
|
|
46
|
+
quit(save = "no", status = 0)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
repos <- getOption("repos")
|
|
50
|
+
if (is.null(repos) || is.na(repos["CRAN"]) || identical(unname(repos["CRAN"]), "@CRAN@")) {
|
|
51
|
+
options(repos = c(CRAN = "https://cloud.r-project.org"))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
is_writable <- function(path) dir.exists(path) && file.access(path, 2) == 0
|
|
55
|
+
lib <- Filter(is_writable, .libPaths())[1]
|
|
56
|
+
if (is.na(lib)) {
|
|
57
|
+
lib <- Sys.getenv("R_LIBS_USER")
|
|
58
|
+
if (!nzchar(lib)) lib <- file.path(Sys.getenv("HOME"), "R", "library")
|
|
59
|
+
dir.create(lib, recursive = TRUE, showWarnings = FALSE)
|
|
60
|
+
.libPaths(c(lib, .libPaths()))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (lock in Sys.glob(file.path(lib, "00LOCK-*"))) {
|
|
64
|
+
# An install that was killed (Ctrl+C, a timeout) leaves its lock behind, and
|
|
65
|
+
# every later install of that package is refused until it is removed.
|
|
66
|
+
if (difftime(Sys.time(), file.info(lock)$mtime, units = "mins") > 5) {
|
|
67
|
+
say("removing a stale lock left by an interrupted install: ", lock)
|
|
68
|
+
unlink(lock, recursive = TRUE)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
ip <- installed.packages()
|
|
73
|
+
base_packages <- rownames(ip)[!is.na(ip[, "Priority"])]
|
|
74
|
+
fields <- tryCatch(read.dcf(file.path(src, "DESCRIPTION"), fields = c("Depends", "Imports", "LinkingTo")), error = function(e) NULL)
|
|
75
|
+
direct <- unique(trimws(sub("[(].*$", "", unlist(strsplit(paste(stats::na.omit(as.vector(fields)), collapse = ","), ",")))))
|
|
76
|
+
direct <- setdiff(direct[nzchar(direct)], c("R", base_packages))
|
|
77
|
+
|
|
78
|
+
available <- tryCatch(suppressWarnings(available.packages()), error = function(e) NULL)
|
|
79
|
+
wanted <- direct
|
|
80
|
+
if (!is.null(available) && nrow(available) > 0) {
|
|
81
|
+
everything <- tools::package_dependencies(direct, db = available, recursive = TRUE)
|
|
82
|
+
wanted <- setdiff(unique(c(direct, unlist(everything, use.names = FALSE))), base_packages)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
usable <- function(package) suppressWarnings(suppressMessages(requireNamespace(package, quietly = TRUE)))
|
|
86
|
+
missing <- wanted[!vapply(wanted, usable, logical(1))]
|
|
87
|
+
warnings_seen <- character()
|
|
88
|
+
collect <- function(expr) withCallingHandlers(expr, warning = function(w) {
|
|
89
|
+
warnings_seen <<- c(warnings_seen, conditionMessage(w))
|
|
90
|
+
invokeRestart("muffleWarning")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
if (length(missing) > 0) {
|
|
94
|
+
if (is.null(available) || nrow(available) == 0) {
|
|
95
|
+
fail("these R packages are needed but not installed: ", paste(missing, collapse = ", "),
|
|
96
|
+
" -- and CRAN could not be reached to install them (check the internet connection)")
|
|
97
|
+
}
|
|
98
|
+
say("installing ", length(missing), " R package(s) from CRAN into ", lib, " (first run only): ", paste(missing, collapse = ", "))
|
|
99
|
+
tryCatch(collect(utils::install.packages(missing, lib = lib, repos = getOption("repos"), quiet = TRUE)),
|
|
100
|
+
error = function(e) fail("installing R packages failed: ", conditionMessage(e)))
|
|
101
|
+
still_missing <- missing[!vapply(missing, usable, logical(1))]
|
|
102
|
+
if (length(still_missing) > 0) {
|
|
103
|
+
# The warnings only say "non-zero exit status". Packages that need
|
|
104
|
+
# another one that failed fail too, so re-run the install of the ones
|
|
105
|
+
# that do not, on their own, and report what R and the compiler said.
|
|
106
|
+
needs <- tryCatch(tools::package_dependencies(still_missing, db = available, recursive = FALSE), error = function(e) list())
|
|
107
|
+
roots <- still_missing[vapply(still_missing, function(p) !any(needs[[p]] %in% still_missing), logical(1))]
|
|
108
|
+
reasons <- character()
|
|
109
|
+
for (p in utils::head(roots, 2)) {
|
|
110
|
+
why <- tryCatch({
|
|
111
|
+
tarball <- utils::download.packages(p, destdir = tempdir(), repos = getOption("repos"), type = "source", quiet = TRUE)[1, 2]
|
|
112
|
+
out <- suppressWarnings(system2(file.path(R.home("bin"), "R"), c("CMD", "INSTALL", paste0("--library=", shQuote(lib)), shQuote(tarball)), stdout = TRUE, stderr = TRUE))
|
|
113
|
+
if (is.null(attr(out, "status")) || identical(attr(out, "status"), 0L)) {
|
|
114
|
+
"it installed when retried on its own, so the failure may be temporary: try again"
|
|
115
|
+
} else {
|
|
116
|
+
important <- grep("error|ERROR|undefined|fatal|cannot|not found|No such file|Killed", out, value = TRUE)
|
|
117
|
+
substr(gsub("[[:space:]]+", " ", paste(utils::tail(if (length(important)) important else out, 5), collapse = " ; ")), 1, 700)
|
|
118
|
+
}
|
|
119
|
+
}, error = function(e) conditionMessage(e))
|
|
120
|
+
reasons <- c(reasons, paste0(p, ": ", why))
|
|
121
|
+
}
|
|
122
|
+
fail("could not install these R packages: ", paste(still_missing, collapse = ", "),
|
|
123
|
+
if (length(reasons)) paste0(" -- ", paste(reasons, collapse = " | ")) else "",
|
|
124
|
+
" (on Linux packages are compiled from source and need a compiler: sudo apt install build-essential)")
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
say("installing hera into ", lib)
|
|
129
|
+
tryCatch(collect(utils::install.packages(src, repos = NULL, type = "source", lib = lib, quiet = TRUE)),
|
|
130
|
+
error = function(e) NULL)
|
|
131
|
+
if (!hera_current()) {
|
|
132
|
+
output <- tryCatch(
|
|
133
|
+
suppressWarnings(system2(file.path(R.home("bin"), "R"), c("CMD", "INSTALL", paste0("--library=", shQuote(lib)), shQuote(src)), stdout = TRUE, stderr = TRUE)),
|
|
134
|
+
error = function(e) conditionMessage(e))
|
|
135
|
+
lines <- grep("ERROR|error|not available|cannot|denied|failed", output, value = TRUE)
|
|
136
|
+
reason <- paste(utils::tail(if (length(lines)) lines else output, 6), collapse = " | ")
|
|
137
|
+
fail("hera could not be installed: ", substr(gsub("[[:space:]]+", " ", reason), 1, 800))
|
|
138
|
+
}
|
|
139
|
+
say("done")
|
|
140
|
+
`;
|
|
141
|
+
const runRscript = async (rscript, scriptFile, args, env, onLine, timeoutMs) => {
|
|
142
|
+
const child = spawn(rscript, [scriptFile, ...args], { env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
143
|
+
for (const stream of [child.stdout, child.stderr]) {
|
|
144
|
+
createInterface({ input: stream }).on('line', onLine);
|
|
145
|
+
}
|
|
146
|
+
let timedOut = false;
|
|
147
|
+
const timer = setTimeout(() => {
|
|
148
|
+
timedOut = true;
|
|
149
|
+
child.kill();
|
|
150
|
+
}, timeoutMs);
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
child.once('error', (error) => {
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
reject(error);
|
|
155
|
+
});
|
|
156
|
+
child.once('close', (code) => {
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
resolve({ code, timedOut });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
const defaultDeps = () => ({
|
|
163
|
+
env: process.env,
|
|
164
|
+
platform: process.platform,
|
|
165
|
+
exists: existsSync,
|
|
166
|
+
run: runRscript,
|
|
167
|
+
timeoutMs: 30 * 60 * 1000
|
|
168
|
+
});
|
|
169
|
+
export function rscriptPath(rHome, deps = defaultDeps()) {
|
|
170
|
+
const name = deps.platform === 'win32' ? 'Rscript.exe' : 'Rscript';
|
|
171
|
+
const candidates = [join(rHome, 'bin', name)];
|
|
172
|
+
if (deps.platform === 'win32')
|
|
173
|
+
candidates.push(join(rHome, 'bin', 'x64', name));
|
|
174
|
+
return candidates.find((candidate) => deps.exists(candidate));
|
|
175
|
+
}
|
|
176
|
+
const inFlight = new Map();
|
|
177
|
+
/**
|
|
178
|
+
* Makes sure hera and its dependencies are installed for this R (see the
|
|
179
|
+
* comment on R_SETUP_SCRIPT). Resolves immediately when there is nothing to
|
|
180
|
+
* do: a Python session, no bundled hera (a source checkout), no R found, or
|
|
181
|
+
* JOVIAN_SKIP_R_SETUP set. Concurrent calls for the same R share one run, and
|
|
182
|
+
* a run that succeeded is not repeated by this process. Rejects with R's own
|
|
183
|
+
* explanation when the packages could not be installed.
|
|
184
|
+
*/
|
|
185
|
+
export function ensureRPackages(options, logger, deps = defaultDeps()) {
|
|
186
|
+
const { rHome, heraSrcPath } = options;
|
|
187
|
+
if (options.kernelType === 'python' || !rHome || !heraSrcPath)
|
|
188
|
+
return Promise.resolve();
|
|
189
|
+
if (deps.env.JOVIAN_SKIP_R_SETUP)
|
|
190
|
+
return Promise.resolve();
|
|
191
|
+
const rscript = rscriptPath(rHome, deps);
|
|
192
|
+
if (!rscript) {
|
|
193
|
+
logger.debug(`No Rscript under ${rHome}; leaving the R packages to the kernel`);
|
|
194
|
+
return Promise.resolve();
|
|
195
|
+
}
|
|
196
|
+
const key = [rHome, heraSrcPath, options.rLibs ?? ''].join('|');
|
|
197
|
+
let pending = inFlight.get(key);
|
|
198
|
+
if (!pending) {
|
|
199
|
+
pending = setUp(rscript, heraSrcPath, options.rLibs, logger, deps).catch((error) => {
|
|
200
|
+
inFlight.delete(key);
|
|
201
|
+
throw error;
|
|
202
|
+
});
|
|
203
|
+
inFlight.set(key, pending);
|
|
204
|
+
}
|
|
205
|
+
return pending;
|
|
206
|
+
}
|
|
207
|
+
async function setUp(rscript, heraSrcPath, rLibs, logger, deps) {
|
|
208
|
+
const directory = await mkdtemp(join(tmpdir(), 'jovian-r-setup-'));
|
|
209
|
+
try {
|
|
210
|
+
const scriptFile = join(directory, 'setup.R');
|
|
211
|
+
await writeFile(scriptFile, R_SETUP_SCRIPT);
|
|
212
|
+
const env = { ...deps.env, ...(rLibs ? { R_LIBS: rLibs } : {}) };
|
|
213
|
+
let failure;
|
|
214
|
+
const otherOutput = [];
|
|
215
|
+
const onLine = (line) => {
|
|
216
|
+
if (line.startsWith('JOVIAN_R_SETUP_ERROR: '))
|
|
217
|
+
failure = line.slice('JOVIAN_R_SETUP_ERROR: '.length);
|
|
218
|
+
else if (line.startsWith('JOVIAN_R_SETUP: '))
|
|
219
|
+
logger.notice(`R setup: ${line.slice('JOVIAN_R_SETUP: '.length)}`);
|
|
220
|
+
else if (line.startsWith('JOVIAN_R_SETUP_INFO: '))
|
|
221
|
+
logger.info(`R setup: ${line.slice('JOVIAN_R_SETUP_INFO: '.length)}`);
|
|
222
|
+
else if (line.trim())
|
|
223
|
+
otherOutput.push(line.trim());
|
|
224
|
+
};
|
|
225
|
+
const outcome = await deps.run(rscript, scriptFile, [heraSrcPath], env, onLine, deps.timeoutMs);
|
|
226
|
+
if (outcome.timedOut) {
|
|
227
|
+
throw new Error(`Setting up the R packages the kernel needs took longer than ${Math.round(deps.timeoutMs / 60000)} minutes and was stopped.`);
|
|
228
|
+
}
|
|
229
|
+
if (outcome.code !== 0) {
|
|
230
|
+
const unexplained = `Rscript exited with code ${outcome.code}${otherOutput.length ? `: ${otherOutput.slice(-5).join(' | ')}` : ''}`;
|
|
231
|
+
throw new Error(`Could not set up the R packages the kernel needs: ${failure ?? unexplained}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
await rm(directory, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=r-setup.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { EngineOptions } from '../types/index.js';
|
|
2
|
+
/** Runs a command and returns its last non-empty stdout line, or undefined if it failed or printed nothing. */
|
|
3
|
+
export type Runner = (command: string, args: string[]) => string | undefined;
|
|
4
|
+
export interface DiscoveryContext {
|
|
5
|
+
run: Runner;
|
|
6
|
+
env: Record<string, string | undefined>;
|
|
7
|
+
platform: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Where R lives, when the caller did not say: $R_HOME, else what `R RHOME`
|
|
11
|
+
* prints (R's own answer, valid on every platform, if R is on PATH), else on
|
|
12
|
+
* Windows the install path R's installer records in the registry.
|
|
13
|
+
*/
|
|
14
|
+
export declare function discoverRHome(context?: DiscoveryContext): string | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Which Python to embed, when the caller did not say: $PYTHONHOME, else the
|
|
17
|
+
* installation prefix of the first python on PATH. sys.base_prefix, not
|
|
18
|
+
* sys.prefix: inside a virtual environment the latter is the venv, which has
|
|
19
|
+
* no libpython to load.
|
|
20
|
+
*/
|
|
21
|
+
export declare function discoverPythonHome(context?: DiscoveryContext): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* The options with rHome (R sessions) or pythonHome (Python sessions) filled
|
|
24
|
+
* in when the caller left them out and the runtime could be found. Anything
|
|
25
|
+
* the caller passed is kept as it is; if nothing is found the field stays
|
|
26
|
+
* unset, and the kernel reports what it could not find.
|
|
27
|
+
*/
|
|
28
|
+
export declare function withDiscoveredRuntime(options: EngineOptions, context?: DiscoveryContext): EngineOptions;
|
|
29
|
+
//# sourceMappingURL=runtimes.d.ts.map
|