|
1 | 1 | import fs from 'fs'
|
| 2 | +import os from 'os' |
2 | 3 | import path from 'path'
|
3 | 4 |
|
4 |
| -// @ts-ignore |
5 |
| -import spawn from 'await-spawn' |
| 5 | +import chalk from 'chalk' |
6 | 6 | import del from 'del'
|
7 | 7 | import glob from 'glob'
|
8 | 8 | import mkdirp from 'mkdirp'
|
| 9 | +import * as pty from 'node-pty' |
| 10 | +// @ts-ignore |
| 11 | +import spawn from 'await-spawn' |
| 12 | +import tmp from 'tmp' |
9 | 13 | import yaml from 'js-yaml'
|
10 | 14 |
|
11 | 15 | import * as nix from './nix'
|
@@ -289,28 +293,114 @@ export default class Environment {
|
289 | 293 | }
|
290 | 294 |
|
291 | 295 | /**
|
292 |
| - * Execute a bash command within the environment |
| 296 | + * Create variables for an environment. |
| 297 | + * |
| 298 | + * This method is used in several other metho |
| 299 | + * e.g. `within`, `enter` |
293 | 300 | *
|
294 |
| - * A 'pure' shell will only hav available the executables that |
| 301 | + * A 'pure' environment will only have available the executables that |
295 | 302 | * were exlicitly installed into the environment
|
296 | 303 | *
|
| 304 | + * @param pure Should the shell that this command is executed in be 'pure'? |
| 305 | + */ |
| 306 | + async vars (pure: boolean = false) { |
| 307 | + const location = await nix.location(this.name) |
| 308 | + |
| 309 | + let PATH = `${location}/bin:${location}/sbin` |
| 310 | + if (!pure) PATH += ':' + process.env.PATH |
| 311 | + |
| 312 | + const R_LIBS_SITE = `${location}/library` |
| 313 | + |
| 314 | + return { |
| 315 | + PATH, |
| 316 | + R_LIBS_SITE |
| 317 | + } |
| 318 | + } |
| 319 | + |
| 320 | + /** |
| 321 | + * Execute a bash command within the environment |
| 322 | + * |
297 | 323 | * @param command The command to execute
|
298 | 324 | * @param pure Should the shell that this command is executed in be 'pure'?
|
299 | 325 | */
|
300 | 326 | async within (command: string, pure: boolean = false) {
|
301 |
| - const location = await nix.location(this.name) |
302 |
| - let path = `${location}/bin:${location}/sbin` |
303 |
| - if (!pure) path += ':' + process.env.PATH |
304 |
| - // Get the path to bash because it may not be available in the PATH of |
305 |
| - // a pure shell |
306 |
| - let bash = await spawn('which', ['bash']) |
307 |
| - await spawn(bash.toString().trim(), ['-c', command], { |
| 327 | + // Get the path to bash because it may not be available in |
| 328 | + // the PATH of a pure shell |
| 329 | + let shell = await spawn('which', ['bash']) |
| 330 | + shell = shell.toString().trim() |
| 331 | + await spawn(shell, ['-c', command], { |
308 | 332 | stdio: 'inherit',
|
309 |
| - env: { |
310 |
| - PATH: path, |
311 |
| - R_LIBS_SITE: `${location}/library` |
| 333 | + env: await this.vars() |
| 334 | + }) |
| 335 | + } |
| 336 | + |
| 337 | + /** |
| 338 | + * Enter the a shell within the environment |
| 339 | + * |
| 340 | + * @param command An initial command to execute in the shell e.g. R or python |
| 341 | + * @param pure Should the shell be 'pure'? |
| 342 | + */ |
| 343 | + async enter (command: string = '', pure: boolean = true) { |
| 344 | + const shellName = os.platform() === 'win32' ? 'powershell.exe' : 'bash' |
| 345 | + const shellArgs = ['--noprofile'] |
| 346 | + |
| 347 | + // Path to the shell executable. We need to do this |
| 348 | + // because the environment may not actually have any shell |
| 349 | + // in it, in which case, when using `pure` a shell won't be available. |
| 350 | + let shellPath = await spawn('which', [shellName]) |
| 351 | + shellPath = shellPath.toString().trim() |
| 352 | + |
| 353 | + // Inject Nixster into the environment as an alias so we can use it |
| 354 | + // there without polluting the environment with additional binaries. |
| 355 | + // During development you'll need to use ---pure=false so that |
| 356 | + // node is available to run Nixster. In production, when a user |
| 357 | + // has installed a binary, this shouldn't be necessary |
| 358 | + let nixsterPath = await spawn('which', ['nixster']) |
| 359 | + const tempRcFile = tmp.fileSync() |
| 360 | + fs.writeFileSync(tempRcFile.name, `alias nixster="${nixsterPath.toString().trim()}"\n`) |
| 361 | + shellArgs.push('--rcfile', tempRcFile.name) |
| 362 | + |
| 363 | + // Environment variables |
| 364 | + let vars = await this.vars(pure) |
| 365 | + vars = Object.assign(vars, { |
| 366 | + // Let Nixster know which environment we're in. |
| 367 | + NIXSTER_ENV: this.name, |
| 368 | + // Customise the bash prompt so that the user know that they are in |
| 369 | + // a Nixster environment and which one. |
| 370 | + PS1: '☆ ' + chalk.green.bold(this.name) + ':' + chalk.blue('\\w') + '$ ' |
| 371 | + }) |
| 372 | + |
| 373 | + const shellProcess = pty.spawn(shellPath, shellArgs, { |
| 374 | + name: 'xterm-color', |
| 375 | + cols: 120, |
| 376 | + rows: 30, |
| 377 | + env: vars |
| 378 | + }) |
| 379 | + shellProcess.on('data', data => { |
| 380 | + process.stdout.write(data) |
| 381 | + }) |
| 382 | + |
| 383 | + // To prevent echoing of input set stdin to raw mode (see https://github.com/Microsoft/node-pty/issues/78) |
| 384 | + // https://nodejs.org/api/tty.html: "When in raw mode, input is always available character-by-character, |
| 385 | + // not including modifiers. Additionally, all special processing of characters |
| 386 | + // by the terminal is disabled, including echoing input characters. Note that CTRL+C |
| 387 | + // will no longer cause a SIGINT when in this mode." |
| 388 | + // @ts-ignore |
| 389 | + process.stdin.setRawMode(true) |
| 390 | + |
| 391 | + // Write the result through to the shell process |
| 392 | + // Capture Ctrl+D for special handling: |
| 393 | + // - if in the top level shell process then exit this process |
| 394 | + // - otherwise, pass on the process e.g. node, Rrm |
| 395 | + const ctrlD = Buffer.from([4]) |
| 396 | + process.stdin.on('data', data => { |
| 397 | + if (data.equals(ctrlD) && shellProcess.process === shellPath) { |
| 398 | + process.exit(1) |
312 | 399 | }
|
| 400 | + shellProcess.write(data) |
313 | 401 | })
|
| 402 | + |
| 403 | + if (command) shellProcess.write(command + '\r') |
314 | 404 | }
|
315 | 405 |
|
316 | 406 | /**
|
|
0 commit comments