commit fbacfc149b59acf68efa97951418a9a7a7022707 Author: technofab Date: Thu Jul 31 12:37:19 2025 +0200 chore: initial commit diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..7ae2fb3 --- /dev/null +++ b/.envrc @@ -0,0 +1,3 @@ +source $(fetchurl https://gitlab.com/rensa-nix/direnv/-/raw/v0.2.0/direnvrc "sha256-PFFxlZWNz/LLuNHA1Zpu2qdC3MF+oukv/TxFj5Utixk=") +REN_FLAKE_ATTR=__std +use ren //repo/devShells/default diff --git a/.nix/repo/benchmark.nix b/.nix/repo/benchmark.nix new file mode 100644 index 0000000..3afb05b --- /dev/null +++ b/.nix/repo/benchmark.nix @@ -0,0 +1,19 @@ +{ + inputs, + cell, +}: { + bench = inputs.nixpkgs.writeShellApplication { + name = "benchmark"; + runtimeInputs = [inputs.nixpkgs.hyperfine]; + text = '' + echo "Comparison cases first:" + hyperfine -w 3 \ + 'nix-instantiate ${inputs.self}/benchmark/shared.nix' \ + 'nix-instantiate ${inputs.self}/benchmark/empty.nix' + echo "Now real benchmark:" + hyperfine -w 3 \ + 'nix-instantiate ${inputs.self}/benchmark/nixpkgs-shell.nix' \ + 'nix-instantiate ${inputs.self}/benchmark/devshell.nix' + ''; + }; +} diff --git a/.nix/repo/devShells.nix b/.nix/repo/devShells.nix new file mode 100644 index 0000000..2ceb786 --- /dev/null +++ b/.nix/repo/devShells.nix @@ -0,0 +1,17 @@ +{ + inputs, + cell, +}: let + devshell = import "${inputs.self}/lib" { + pkgs = inputs.nixpkgs; + }; +in { + default = devshell.mkShell { + packages = [inputs.nixpkgs.alejandra]; + env."HELLO".value = "world!"; + enterShellCommands.test = { + text = "echo Hello $HELLO"; + deps = ["env"]; + }; + }; +} diff --git a/.nix/repo/flake.lock b/.nix/repo/flake.lock new file mode 100644 index 0000000..d60415a --- /dev/null +++ b/.nix/repo/flake.lock @@ -0,0 +1,28 @@ +{ + "nodes": { + "nixtest-lib": { + "locked": { + "dir": "lib", + "lastModified": 1753957623, + "narHash": "sha256-kdImwKx57N0QL8HPUUb5ADwXFgSjaNOk39b/eKlzyTo=", + "owner": "TECHNOFAB", + "repo": "nixtest", + "rev": "22b43c9fe83be73c3f0648bbb54bc3c1cf7f96df", + "type": "gitlab" + }, + "original": { + "dir": "lib", + "owner": "TECHNOFAB", + "repo": "nixtest", + "type": "gitlab" + } + }, + "root": { + "inputs": { + "nixtest-lib": "nixtest-lib" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/.nix/repo/flake.nix b/.nix/repo/flake.nix new file mode 100644 index 0000000..ff22c86 --- /dev/null +++ b/.nix/repo/flake.nix @@ -0,0 +1,6 @@ +{ + inputs = { + nixtest-lib.url = "gitlab:TECHNOFAB/nixtest?dir=lib"; + }; + outputs = i: i; +} diff --git a/.nix/repo/tests.nix b/.nix/repo/tests.nix new file mode 100644 index 0000000..e1b14b1 --- /dev/null +++ b/.nix/repo/tests.nix @@ -0,0 +1,15 @@ +{ + inputs, + cell, +}: let + pkgs = inputs.nixpkgs; + ntlib = inputs.nixtest-lib.lib {inherit pkgs;}; + devshell = import "${inputs.self}/lib" {inherit pkgs;}; +in { + tests = ntlib.mkNixtest { + modules = ntlib.autodiscover {dir = "${inputs.self}/tests";}; + args = { + inherit ntlib devshell pkgs; + }; + }; +} diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3c69e62 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 TECHNOFAB +Copyright (c) 2021 Numtide and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..b62783e --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Rensa DevShell + +Minimal devshell implementation using Modules. +Inspired by [numtide/devshell](https://github.com/numtide/devshell). diff --git a/benchmark/devshell.nix b/benchmark/devshell.nix new file mode 100644 index 0000000..464331e --- /dev/null +++ b/benchmark/devshell.nix @@ -0,0 +1,5 @@ +let + shared = import ./shared.nix; + devshell = import ./../lib {inherit (shared) pkgs;}; +in + devshell.mkShell {} diff --git a/benchmark/empty.nix b/benchmark/empty.nix new file mode 100644 index 0000000..cb746c9 --- /dev/null +++ b/benchmark/empty.nix @@ -0,0 +1,6 @@ +# minimal derivation to see what the fastest time of nix-instantiate can be +derivation { + name = "empty"; + builder = "true"; + system = builtins.currentSystem; +} diff --git a/benchmark/nixpkgs-shell.nix b/benchmark/nixpkgs-shell.nix new file mode 100644 index 0000000..cf519bc --- /dev/null +++ b/benchmark/nixpkgs-shell.nix @@ -0,0 +1,4 @@ +let + shared = import ./shared.nix; +in + shared.pkgs.mkShell {} diff --git a/benchmark/shared.nix b/benchmark/shared.nix new file mode 100644 index 0000000..dfdb33b --- /dev/null +++ b/benchmark/shared.nix @@ -0,0 +1,12 @@ +rec { + nixpkgs = let + gitRev = "835e9a7acce59c78130d2d4cc66a07a20403b185"; + in + builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/${gitRev}.tar.gz"; + sha256 = "0j86vmk6z9fi19nqyp22cpml0zgslaw20b66v3w9lw988mql3615"; + }; + pkgs = import nixpkgs { + system = builtins.currentSystem; + }; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..61c301a --- /dev/null +++ b/flake.lock @@ -0,0 +1,208 @@ +{ + "nodes": { + "call-flake": { + "locked": { + "lastModified": 1687380775, + "narHash": "sha256-bmhE1TmrJG4ba93l9WQTLuYM53kwGQAjYHRvHOeuxWU=", + "owner": "divnix", + "repo": "call-flake", + "rev": "74061f6c241227cd05e79b702db9a300a2e4131a", + "type": "github" + }, + "original": { + "owner": "divnix", + "repo": "call-flake", + "type": "github" + } + }, + "haumea": { + "inputs": { + "nixpkgs": [ + "std", + "lib" + ] + }, + "locked": { + "lastModified": 1685133229, + "narHash": "sha256-FePm/Gi9PBSNwiDFq3N+DWdfxFq0UKsVVTJS3cQPn94=", + "owner": "nix-community", + "repo": "haumea", + "rev": "34dd58385092a23018748b50f9b23de6266dffc2", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "v0.2.2", + "repo": "haumea", + "type": "github" + } + }, + "incl": { + "inputs": { + "nixlib": [ + "std", + "lib" + ] + }, + "locked": { + "lastModified": 1693483555, + "narHash": "sha256-Beq4WhSeH3jRTZgC1XopTSU10yLpK1nmMcnGoXO0XYo=", + "owner": "divnix", + "repo": "incl", + "rev": "526751ad3d1e23b07944b14e3f6b7a5948d3007b", + "type": "github" + }, + "original": { + "owner": "divnix", + "repo": "incl", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1753694789, + "narHash": "sha256-cKgvtz6fKuK1Xr5LQW/zOUiAC0oSQoA9nOISB0pJZqM=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "dc9637876d0dcc8c9e5e22986b857632effeb727", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nosys": { + "locked": { + "lastModified": 1668010795, + "narHash": "sha256-JBDVBnos8g0toU7EhIIqQ1If5m/nyBqtHhL3sicdPwI=", + "owner": "divnix", + "repo": "nosys", + "rev": "feade0141487801c71ff55623b421ed535dbdefa", + "type": "github" + }, + "original": { + "owner": "divnix", + "repo": "nosys", + "type": "github" + } + }, + "paisano": { + "inputs": { + "call-flake": "call-flake", + "nixpkgs": [ + "std", + "nixpkgs" + ], + "nosys": "nosys", + "yants": [ + "std", + "yants" + ] + }, + "locked": { + "lastModified": 1708640854, + "narHash": "sha256-EpcAmvIS4ErqhXtVEfd2GPpU/E/s8CCRSfYzk6FZ/fY=", + "owner": "paisano-nix", + "repo": "core", + "rev": "adcf742bc9463c08764ca9e6955bd5e7dcf3a3fe", + "type": "github" + }, + "original": { + "owner": "paisano-nix", + "ref": "0.2.0", + "repo": "core", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs", + "std": "std" + } + }, + "std": { + "inputs": { + "arion": [ + "std", + "blank" + ], + "blank": [], + "devshell": [ + "std", + "blank" + ], + "dmerge": [], + "haumea": "haumea", + "incl": "incl", + "lib": [ + "nixpkgs" + ], + "makes": [ + "std", + "blank" + ], + "microvm": [ + "std", + "blank" + ], + "n2c": [ + "std", + "blank" + ], + "nixago": [ + "std", + "blank" + ], + "nixpkgs": [ + "nixpkgs" + ], + "paisano": "paisano", + "paisano-tui": [], + "terranix": [ + "std", + "blank" + ], + "yants": "yants" + }, + "locked": { + "lastModified": 1747378812, + "narHash": "sha256-bx+Bt2tEpCkrY7ImaklaTQvH6VGrB7FAmnfs7tItYIs=", + "owner": "divnix", + "repo": "std", + "rev": "29f79b7ae7d1716ff13944b698fe76cb0675c5f6", + "type": "github" + }, + "original": { + "owner": "divnix", + "repo": "std", + "type": "github" + } + }, + "yants": { + "inputs": { + "nixpkgs": [ + "std", + "lib" + ] + }, + "locked": { + "lastModified": 1686863218, + "narHash": "sha256-kooxYm3/3ornWtVBNHM3Zh020gACUyFX2G0VQXnB+mk=", + "owner": "divnix", + "repo": "yants", + "rev": "8f0da0dba57149676aa4817ec0c880fbde7a648d", + "type": "github" + }, + "original": { + "owner": "divnix", + "repo": "yants", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2f9ed7c --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + std = { + url = "github:divnix/std"; + inputs = { + nixpkgs.follows = "nixpkgs"; + lib.follows = "nixpkgs"; + paisano-tui.follows = ""; + dmerge.follows = ""; + blank.follows = ""; + }; + }; + }; + + outputs = { + self, + std, + ... + } @ inputs: + std.growOn + { + inherit inputs; + cellsFrom = ./.nix; + cellBlocks = with std.blockTypes; [ + (devshells "devShells") + (pkgs "tests") + (runnables "benchmark") + ]; + } + { + packages = std.harvest self [["repo" "tests"] ["repo" "benchmark"]]; + }; +} diff --git a/lib/default.nix b/lib/default.nix new file mode 100644 index 0000000..b902d77 --- /dev/null +++ b/lib/default.nix @@ -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; +} diff --git a/lib/flake.nix b/lib/flake.nix new file mode 100644 index 0000000..1bb6f53 --- /dev/null +++ b/lib/flake.nix @@ -0,0 +1,5 @@ +{ + outputs = i: { + lib = import ./.; + }; +} diff --git a/lib/modules/default.nix b/lib/modules/default.nix new file mode 100644 index 0000000..a0b6428 --- /dev/null +++ b/lib/modules/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./main.nix + ./env.nix + ]; +} diff --git a/lib/modules/env.nix b/lib/modules/env.nix new file mode 100644 index 0000000..f8cdcf7 --- /dev/null +++ b/lib/modules/env.nix @@ -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)); + }; +} diff --git a/lib/modules/main.nix b/lib/modules/main.nix new file mode 100644 index 0000000..4992303 --- /dev/null +++ b/lib/modules/main.nix @@ -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; + }; + }; +} diff --git a/lib/utils/mkNakedShell.nix b/lib/utils/mkNakedShell.nix new file mode 100644 index 0000000..50ce52d --- /dev/null +++ b/lib/utils/mkNakedShell.nix @@ -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 diff --git a/tests/devshell_test.nix b/tests/devshell_test.nix new file mode 100644 index 0000000..ca1af04 --- /dev/null +++ b/tests/devshell_test.nix @@ -0,0 +1,53 @@ +{ + pkgs, + ntlib, + devshell, + ... +}: { + suites."Devshell Tests" = { + pos = __curPos; + tests = [ + { + name = "basic"; + type = "script"; + script = let + shell = devshell.mkShell {}; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + assert_file_contains ${shell}/env.bash "XDG_DATA_DIRS" "should contain XDG_DATA_DIRS" + ''; + } + { + name = "packages"; + type = "script"; + script = let + shell = devshell.mkShell { + packages = [pkgs.hello]; + }; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + assert "-f ${shell}/bin/hello" "/bin/hello should exist" + ''; + } + { + name = "env"; + type = "script"; + script = let + shell = devshell.mkShell { + env."HELLO".value = "world"; + }; + in + # sh + '' + ${ntlib.helpers.scriptHelpers} + assert_file_contains ${shell}/env.bash "HELLO" "should contain HELLO" + assert_file_contains ${shell}/env.bash "world" "should contain world" + ''; + } + ]; + }; +}