forked from numtide/devshell
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit f8a0773
Showing
7 changed files
with
491 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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? | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.