chore: initial commit

This commit is contained in:
technofab 2025-07-31 12:37:19 +02:00
commit fbacfc149b
Signed by: technofab
SSH key fingerprint: SHA256:bV4h88OqS/AxjbPn66uUdvK9JsgIW4tv3vwJQ8tpMqQ
21 changed files with 748 additions and 0 deletions

19
lib/default.nix Normal file
View file

@ -0,0 +1,19 @@
{
pkgs,
lib ? pkgs.lib,
...
}: rec {
modules = import ./modules;
eval = {config, ...}: let
res = lib.evalModules {
modules = [config modules];
specialArgs = {
inherit pkgs;
};
};
in {
inherit (res) config options;
shell = res.config.shell.finalPackage;
};
mkShell = config: (eval {inherit config;}).shell;
}

5
lib/flake.nix Normal file
View file

@ -0,0 +1,5 @@
{
outputs = i: {
lib = import ./.;
};
}

6
lib/modules/default.nix Normal file
View file

@ -0,0 +1,6 @@
{
imports = [
./main.nix
./env.nix
];
}

99
lib/modules/env.nix Normal file
View file

@ -0,0 +1,99 @@
{
lib,
pkgs,
config,
...
}: let
inherit (lib) filter assertMsg head length escapeShellArg types mkOption concatStringsSep;
envToBash = {
name,
value,
eval,
prefix,
...
} @ args: let
vals = filter (key: args.${key} != null && args.${key} != false) [
"eval"
"prefix"
"unset"
"value"
];
valType = head vals;
in
assert assertMsg (
(length vals) > 0
) "[devshell/env]: ${name} expected one of (value|eval|prefix|unset) to be set.";
assert assertMsg ((length vals) < 2)
"[devshell/env]: ${name} expected only one of (value|eval|prefix|unset) to be set. Not ${toString vals}";
assert assertMsg (!(name == "PATH" && valType == "value")) "[devshell/env]: ${name} should not override the value. Use 'prefix' instead.";
if valType == "value"
then "export ${name}=${escapeShellArg (toString value)}"
else if valType == "eval"
then "export ${name}=${eval}"
else if valType == "prefix"
then ''export ${name}=$(${pkgs.coreutils}/bin/realpath --canonicalize-missing "${prefix}")''${${name}+:''${${name}}}''
else if valType == "unset"
then ''unset ${name}''
else throw "[devshell] BUG in the env module. This should never be reached.";
envType = types.submodule ({name, ...}: {
options = {
name = mkOption {
type = types.str;
default = name;
};
value = mkOption {
type = with types;
nullOr (oneOf [
str
int
bool
package
]);
default = null;
};
eval = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Like value but not evaluated by Bash. This allows to inject other
variable names or even commands using the `$()` notation.
'';
example = "$OTHER_VAR";
};
prefix = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Prepend to PATH-like environment variables.
For example name = "PATH"; prefix = "bin"; will expand the path of
./bin and prepend it to the PATH, separated by ':'.
'';
example = "bin";
};
unset = mkOption {
type = types.bool;
default = false;
description = ''Unset the env variable'';
};
};
});
in {
options.env = mkOption {
type = types.attrsOf envType;
default = {};
description = '''';
};
config = {
env = {
"XDG_DATA_DIRS".eval = "$DEVSHELL_DIR/share:\${XDG_DATA_DIRS-}";
};
enterShellCommands."env".text = concatStringsSep "\n" (map envToBash (builtins.attrValues config.env));
};
}

113
lib/modules/main.nix Normal file
View file

@ -0,0 +1,113 @@
{
pkgs,
lib ? pkgs.lib,
config,
...
}: let
inherit (lib) mapAttrs mkOption types textClosureMap id attrNames;
mkNakedShell = pkgs.callPackage ./../utils/mkNakedShell.nix {};
addAttributeName = prefix:
mapAttrs (
k: v:
v
// {
text = ''
#### ${prefix}.${k}
${v.text}
'';
}
);
envBash =
pkgs.writeText "devshell-env.bash"
# sh
''
export DEVSHELL_DIR=@DEVSHELL_DIR@
# nicely handle it at the front or between/end
PATH=''${PATH#/path-not-set:}
PATH=''${PATH#:/path-not-set}
export PATH=$DEVSHELL_DIR/bin:$PATH
${textClosureMap
id
(addAttributeName "startup" config.enterShellCommands)
(attrNames config.enterShellCommands)}
# interactive session
if [[ $- == *i* ]]; then
true
${textClosureMap
id
(addAttributeName "interactive" config.interactiveShellCommands)
(attrNames config.interactiveShellCommands)}
fi # interactive session
unset DEVSHELL_DIR
'';
entryType = types.submodule {
options = {
text = mkOption {
type = types.str;
description = ''
Script to run.
'';
};
deps = mkOption {
type = types.listOf types.str;
default = [];
description = ''
A list of other steps that this one depends on.
'';
};
};
};
in {
options = {
shell = {
finalPackage = mkOption {
type = types.package;
internal = true;
};
finalProfile = mkOption {
type = types.package;
internal = true;
};
};
name = mkOption {
type = types.str;
default = "devshell";
};
packages = mkOption {
type = types.listOf types.package;
default = [];
};
enterShellCommands = mkOption {
type = types.attrsOf entryType;
default = {};
};
interactiveShellCommands = mkOption {
type = types.attrsOf entryType;
default = {};
};
};
config.shell = {
finalProfile = pkgs.buildEnv {
name = "${config.name}-profile";
paths = config.packages;
postBuild = ''
substitute ${envBash} $out/env.bash --subst-var-by DEVSHELL_DIR $out
# NOTE: maybe also add an entrypoint shell script at $out/bin/devshell-${config.name}?
'';
meta.mainProgram = "devshell-${config.name}";
};
finalPackage = mkNakedShell {
name = config.name;
profile = config.shell.finalProfile;
};
};
}

View file

@ -0,0 +1,70 @@
{
bashInteractive,
coreutils,
stdenv,
writeTextFile,
}: let
bashPath = "${bashInteractive}/bin/bash";
nakedStdenv = writeTextFile {
name = "naked-stdenv";
destination = "/setup";
text = ''
# Fix for `nix develop`
: ''${outputs:=out}
runHook() {
eval "$shellHook"
unset runHook
}
'';
};
in
{
name,
# A path to a buildEnv that will be loaded by the shell.
# We assume that the buildEnv contains an ./env.bash script.
profile,
meta ? {},
passthru ? {},
}:
(derivation {
inherit name;
system = stdenv.hostPlatform.system;
# `nix develop` actually checks and uses builder. And it must be bash.
builder = bashPath;
# Bring in the dependencies on `nix-build`
args = [
"-ec"
"${coreutils}/bin/ln -s ${profile} $out; exit 0"
];
# $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 = nakedStdenv;
# 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_STORE
unset TEMP TEMPDIR TMP TMPDIR
unset builder out shellHook stdenv system name
# Flakes stuff
unset dontAddDisableDepTrack outputs
# For `nix develop`. We get /noshell on Linux and /sbin/nologin on macOS.
if [[ "$SHELL" == "/noshell" || "$SHELL" == "/sbin/nologin" ]]; then
export SHELL=${bashPath}
fi
# Load the environment
source "${profile}/env.bash"
'';
})
// {
inherit meta passthru;
}
// passthru