Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ __pycache__
build/
dist/
MANIFEST
.ipynb_checkpoints
.ipynb_checkpoints
.DS_Store
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# Unreleased

- Add `BASH_KERNEL_CMD` environment variable to override the command used to
launch bash. Its value allows a wrapper command with arguments, e.g.
`export BASH_KERNEL_CMD="apptainer exec --nv container.sif bash"`. When a
wrapper is used, the `--rcfile` bash startup file is copied into the shared
temp directory (`$TMPDIR`/`/tmp`) so it is readable from inside the wrapper
(e.g. a container), and the kernel banner no longer fails if the wrapper's
`--version` output is unavailable or unexpected.

# Version 0.10.0 (2024-01-05)

- Support for Python 3.13, by replacing the removed imghdr standard library module
Expand Down
39 changes: 39 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,45 @@ Requirements of Bash

Bash kernel directly interacts with bash, and therefore requires a functioning interactive build of bash. In nearly all cases this will be the default, however some distributions remove GNU readline or other interactivity features of bash. Almost always, these features are provided in a separate, more complete bash package, which should be installed. See for example https://github.com/takluyver/bash_kernel/issues/142.

Using a custom bash command
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

By default the kernel launches the ``bash`` found on your ``PATH``. You can
override this by setting the ``BASH_KERNEL_CMD`` environment variable before
starting Jupyter. You can put in whatever you like here, as long as it starts an
interactive ``bash``.

This is useful for running the kernel inside a container, or via any other
wrapper that eventually launches ``bash``. For example, to run the kernel's
bash inside an `Apptainer <https://apptainer.org/>`_ (formerly Singularity)
container:

.. code:: shell

export BASH_KERNEL_CMD="apptainer exec --nv container.sif bash"
jupyter notebook

To make the override available as its own entry in the Jupyter kernel menu
(instead of exporting the variable globally), install a dedicated kernelspec
whose ``kernel.json`` sets the variable in its ``env`` block. This also lets a
plain-bash kernel and a wrapped kernel coexist:

.. code:: json

{
"argv": ["python", "-m", "bash_kernel", "-f", "{connection_file}"],
"display_name": "Bash (container)",
"language": "bash",
"env": { "BASH_KERNEL_CMD": "apptainer exec --nv container.sif bash" }
}

**Note:** the bash startup file must be readable from inside the wrapper. The
kernel places it in ``$TMPDIR`` (or ``/tmp``), so this works automatically as
long as that directory is shared with the wrapper at the same path -- Apptainer
does this for ``/tmp`` by default. Other runtimes may need an explicit mount
(e.g. Docker's ``-v /tmp:/tmp``), and pointing ``$TMPDIR`` at a directory the
wrapper can't see will prevent startup.

Displaying Rich Content
-----------------------

Expand Down
68 changes: 64 additions & 4 deletions bash_kernel/kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
import pexpect

from subprocess import check_output
import atexit
import os
import os.path
import shlex
import shutil
import tempfile
import uuid
import random
import string
Expand All @@ -17,6 +22,55 @@

from .display import (extract_contents, build_cmds)

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

def bash_command():
"""The command (and any wrapper arguments) used to launch bash.

Defaults to ``bash``, but can be overridden with the ``BASH_KERNEL_CMD``
environment variable to launch bash via a wrapper, e.g. inside a
container: ``apptainer exec --nv container.sif bash``. The value is parsed
with shell-like quoting, so paths/arguments containing spaces can be
quoted. Returns a list suitable for ``subprocess``/``pexpect``.
"""
return shlex.split(os.environ.get('BASH_KERNEL_CMD') or 'bash') or ['bash']

_bashrc_copy = None

def _remove_bashrc_copy():
global _bashrc_copy
if _bashrc_copy and os.path.exists(_bashrc_copy):
try:
os.remove(_bashrc_copy)
except OSError:
pass
_bashrc_copy = None

def bashrc_path():
"""Path to the bash startup file passed to bash via ``--rcfile``.

Normally this is pexpect's bundled ``bashrc.sh``. But when
``BASH_KERNEL_CMD`` launches bash via a wrapper (e.g. a container), that
host path may not be visible inside the wrapper, so we copy the rcfile into
the shared temp dir (``$TMPDIR`` or ``/tmp``) where it can be read.
"""
src = os.path.join(os.path.dirname(pexpect.__file__), 'bashrc.sh')
if 'BASH_KERNEL_CMD' not in os.environ:
return src

global _bashrc_copy
if _bashrc_copy is None:
tmp_dir = os.environ.get('TMPDIR', '/tmp')
fd, path = tempfile.mkstemp(prefix='bash_kernel_', suffix='.sh',
dir=tmp_dir)
os.close(fd)
shutil.copyfile(src, path)
_bashrc_copy = path
atexit.register(_remove_bashrc_copy)
return _bashrc_copy

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

class IREPLWrapper(replwrap.REPLWrapper):
"""A subclass of REPLWrapper that gives incremental output
specifically for bash_kernel.
Expand Down Expand Up @@ -81,14 +135,19 @@ class BashKernel(Kernel):
@property
def language_version(self):
m = version_pat.search(self.banner)
return m.group(1)
return m.group(1) if m else ''

_banner = None

@property
def banner(self):
if self._banner is None:
self._banner = check_output(['bash', '--version']).decode('utf-8')
try:
self._banner = check_output(bash_command() + ['--version']).decode('utf-8')
except Exception as exc:
# Don't let a missing/failing wrapper command (e.g. a container
# image that isn't available) break the kernel_info reply.
self._banner = 'bash_kernel: unable to determine bash version ({})'.format(exc)
return self._banner

language_info = {'name': 'bash',
Expand Down Expand Up @@ -119,8 +178,9 @@ def _start_bash(self):
# bash() function of pexpect/replwrap.py. Look at the
# source code there for comments and context for
# understanding the code here.
bashrc = os.path.join(os.path.dirname(pexpect.__file__), 'bashrc.sh')
child = pexpect.spawn("bash", ['--rcfile', bashrc], echo=False,
bashrc = bashrc_path()
bash_args = bash_command() + ['--rcfile', bashrc]
child = pexpect.spawn(bash_args[0], bash_args[1:], echo=False,
encoding='utf-8', codec_errors='replace')
# Following comment stolen from upstream's REPLWrap:
# If the user runs 'env', the value of PS1 will be in the output. To avoid
Expand Down