Skip to content

Commit

Permalink
hi!
Browse files Browse the repository at this point in the history
  • Loading branch information
zimbatm committed Aug 4, 2020
0 parents commit f8a0773
Show file tree
Hide file tree
Showing 7 changed files with 491 additions and 0 deletions.
79 changes: 79 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# devshell - a shell for developers.

When switching from project to project, a common issue is to get all the
development dependencies.

Builds on top of Nix.

## Features

### A `devshell-menu`

When entering new development environments, it would be nice if it was
possible to type a standard command and get a list of the available tools.

### MOTD

Similar to the dev menu, to keep developers informed of the development
environment changes. This requires to record what version of the MOTD the
developer has seen and only show the new entries.

### `devshell.toml`

## Integrations

* nix-shell
* nix flakes
* direnv

## Features

### Bash completion by default

Life is not complete otherwise. Huhu.

Packages that contain bash completions will automatically be loaded by the
devshell.

## TODO

A lot of things!

### Documentation

Explain how all of this works and all the use-cases.

### Testing

Write integration tests for all of the use-cases.

### Lazy dependencies

This requires some coordination with the repository structure. To keep the
dev closure small, it would be nice to be able to load some of the
dependencies on demand.

### No nixpkgs

Nixpkgs is a fairly large dependency. It would be nice if the developer wasn't
forced to load it.

### --pure mode

Sometimes you want to run things in a slightly more pure mode. For example in
a CI environment, to make sure that all the dev dependencies are captured.

### Nix bootstrap

Some developers might not have Nix installed. I know! Ludicrous :-p but it
happens. Is it possible to bootstrap Nix.

### Doctor / nix version check

Not everything can be nicely sandboxed. Is it possible to get a fast doctor
script that checks that everything is in good shape?

### Support other shells

What? Not everyone is using bash?

311 changes: 311 additions & 0 deletions default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,311 @@
{ lib
, bashInteractive
, buildEnv
, coreutils
, gnused
, pkgs
, system
, writeText
, writeTextFile
, writeShellScriptBin
, inNixShell ? false
}:
let
_inNixShell = inNixShell;

bashPath = "${bashInteractive}/bin/bash";

# transform the env vars into bash instructions
envToBash = env:
builtins.concatStringsSep "\n"
(lib.mapAttrsToList
(k: v: "export ${k}=${lib.escapeShellArg (toString v)}")
env
)
;

aliasToBash = { name, command }:
"alias ${name}=${lib.escapeShellArg (toString command)}";

aliasesToBash = aliases:
builtins.concatStringsSep "\n"
(lib.mapAttrsToList
(name: val: aliasToBash ({ inherit name; } // val))
aliases
)
;

# A developer shell that works in all scenarios
#
# * nix-build
# * nix-shell
# * flake app
# * direnv integration
#
# TODO:
# * a lot more testing
# * add --pure mode that makes sense
# * remove /path-not-set from the PATH. build-env sets this fake PATH.
# https://github.com/nixos/nix/blob/flakes/src/libstore/build.cc
mkDevShell =
{ name ? "devshell"
, # ignore this unless you plan on using nix-shell
inNixShell ? _inNixShell
, # fill this with a message of the day or welcome message
motd ? "\n### Welcome to ${name} ####\n"
, # list of derivations to merge into the environment
packages ? [ ]
, # environment variables to add to the ... environment
env ? { }
, # extra bash configuration
bash ? {
extra = "";
interactive = "";
}
, aliases ? { }
}:
let
envDrv = buildEnv {
name = "${name}-env";
paths = packages;
# TODO: support passing more arguments here
};

# write a bash profile to load
bashrc = writeText "${name}-bashrc" ''
# Set all the passed environment variables
${envToBash env}
# Prepend the devshell root to the PATH
export PATH=$DEVSHELL_DIR/bin:$PATH
# Load installed profiles
for file in "$DEVSHELL_DIR/etc/profile.d/"*.sh; do
# if that folder doesn't exist, bash loves to return the whole glob
[[ -e "$file" ]] || continue
source "$file"
done
# Use this to set even more things with bash
${bash.extra or ""}
# Interactive sessions
if [[ $- == *i* ]]; then
devshell-menu() {
echo "Commands:"
echo " devshell-menu"
echo " devshell-root"
if [[ -d "$DEVSHELL_DIR/bin" ]]; then
( cd "$DEVSHELL_DIR/bin" && ls ) | ${gnused}/bin/sed 's/^/ /g'
fi
if [[ ${toString (builtins.length (builtins.attrNames aliases))} -gt 0 ]]; then
echo
echo "Aliases:"
cat <<ALIASES
${builtins.concatStringsSep "\n " (builtins.attrNames aliases)}
ALIASES
fi
}
# Type `devshell-root` to go back to the project root
devshell-root() {
cd "$DEVSHELL_ROOT"
}
# Print information if the prompt is every displayed. We have to make
# that distinction because `nix-shell -c "cmd"` is running in
# interactive mode.
devshell-prompt() {
cat <<'MOTD'
${motd}
MOTD
# Print a menu
devshell-menu
# Make it a noop
devshell-prompt() { :; }
}
PROMPT_COMMAND=devshell-prompt''${PROMPT_COMMAND+;$PROMPT_COMMAND}
# Set a cool PS1
if [[ -n "$PS1" ]]; then
# Print the path relative to $DEVSHELL_ROOT
rel_root() {
local path
path=$(${coreutils}/bin/realpath --relative-to "$DEVSHELL_ROOT" "$PWD")
if [[ $path != . ]]; then
echo " $path "
fi
}
PS1='\[\033[0;32;40m\][${name}]$(rel_root)\$\[\033[0m\] '
fi
# Load bash completions
for file in "$DEVSHELL_DIR/share/bash-completion/completions/"* ; do
[[ -f "$file" ]] && source "$file"
done
${aliasesToBash aliases}
${bash.interactive or ""}
fi # Interactive session
'';

# This is our entrypoint for everything!
#
# Use a naked derivation so that nix-shell can be made to be broken on
# purpose. We want to enforce the use of the `inNixShell` feature there.
devShell = derivation {
inherit name system;

# Define our own minimal builder.
builder = bashPath;
args = [ "-ec" ". $buildScriptPath" ];
buildScript = ''
${coreutils}/bin/cp $envScriptPath $out
${coreutils}/bin/chmod +x $out
'';

# Break the stdenv on purpose to avoid nix-shell here
stdenv = writeTextFile {
name = "devshell-stdenv";
destination = "/setup";
text = ''
echo "!!!!!! This is not meant to happen !!!!!!"
echo "TODO: explain how to propagate inNixShell"
exit 1
'';
};

# The actual devshell wrapper script
envScript = ''
#!${bashPath}
# Script that sets-up the environment. Can be both sourced or invoked.
#
# Usage: source @out@ # load the environment in the current shell
# Usage: @out@ # start a bash sub-shell
# Usage: @out@ <cmd> [...] # run a command in the environment
# This is the directory that contains our dependencies
export DEVSHELL_DIR=${envDrv}
# It assums that the shell is always loaded from the root of the project
# Store that for later usage.
export DEVSHELL_ROOT=$PWD
# If the file is sourced, skip all of the rest and just source the
# bashrc
if [[ $0 != "''${BASH_SOURCE[0]}" ]]; then
source "${bashrc}"
return
fi
# Be strict!
set -euo pipefail
# TODO: add --help menu?
# TODO: add --pure functionality?
if [[ $# = 0 ]]; then
# Start an interactive shell
exec "${bashPath}" --rcfile "${bashrc}" --noprofile
else
# Start a script
source "${bashrc}"
exec -- "$@"
fi
'';

passAsFile = [ "buildScript" "envScript" ];
};

# Use this to define a flake app for the environment.
flakeApp = {
type = "app";
program = "${devShell}";
};

# Use a naked derivation to limit the amount of noise passed to nix-shell.
nixShellEnv = derivation {
inherit system;
name = "${name}-nix-shell";
# `nix develop` actually checks and uses builder. And it must be bash.
builder = bashPath;
# $stdenv/setup is loaded by nix-shell during startup.
# https://github.com/nixos/nix/blob/377345e26f1ac4bbc87bb21debcc52a1d03230aa/src/nix-build/nix-build.cc#L429-L432
stdenv = writeTextFile {
name = "devshell-stdenv";
destination = "/setup";
text = ''
runHook() {
eval "$shellHook"
unset runHook
}
'';
};

# The shellHook is loaded directly by `nix develop`. But nix-shell
# requires that other trampoline.
shellHook = ''
# Remove all the unnecessary noise that is set by the build env
unset NIX_BUILD_TOP NIX_BUILD_CORES NIX_BUILD_TOP NIX_STORE
unset TEMP TEMPDIR TMP TMPDIR
unset builder name stdenv system out
unset shellHook
# Flakes stuff
unset dontAddDisableDepTrack
unset outputs
# For `nix develop`
if [[ "$SHELL" == "/noshell" ]]; then
export SHELL=${bashPath}
fi
# Load the dev shell environment
source "${devShell}"
'';
};

out =
if inNixShell then
nixShellEnv
else
devShell // {
inherit flakeApp;
};
in
out
;

resolveKey = key:
let
attrs = builtins.filter builtins.isString (builtins.split "\\." key);
in
builtins.foldl' (sum: attr: sum.${attr}) pkgs attrs
;

# Build the devshell from pure JSON-like data
fromData = data:
mkDevShell (data // {
packages = map resolveKey (data.packages or [ ]);
});

# Build the devshell from a TOML declaration
fromTOML = path:
let
data = builtins.fromTOML (builtins.readFile path);
in
fromData ((data.main or { }) // (builtins.removeAttrs data [ "main" ]))
;
in
{
inherit
fromData
fromTOML
mkDevShell
;

__functor = _: mkDevShell;
}
Loading

0 comments on commit f8a0773

Please sign in to comment.