├── README.md ├── default.nix ├── modules ├── git.nix ├── github.nix ├── shell_nix.nix ├── tools.nix ├── vim.nix └── xdg.nix ├── workspaces.nix └── workspaces.sh /README.md: -------------------------------------------------------------------------------- 1 | # Nix-workspaces 2 | 3 | Reproducible workspaces with Nix modules. 4 | 5 | Workspaces encapsulate an environment and can be used to start a shell or an 6 | editor. They are declared using the same module concept used in NixOS (it's a 7 | generic [function](https://github.com/Julow/nix-workspaces/blob/f926f8288cc09fd146514028f519a6c29ea3ef6f/workspaces.nix#L69) available in `nixpkgs`), 8 | see [examples](#Examples) below. 9 | 10 | ```sh 11 | nix-env -if workspaces.nix 12 | ``` 13 | 14 | This makes sure that every workspaces are built and ready to be opened quickly. 15 | It installs a single program, `workspaces` into the environment, that allows to 16 | open them: 17 | 18 | ```sh 19 | workspaces open 20 | ``` 21 | 22 | The workspace description is very versatile, the [low-level options](./workspaces.nix) 23 | `activation_script`, `buildInputs` and `command` are equivalent to a 24 | `shell.nix`. 25 | 26 | Other options are defined in [modules/](./modules). For example to define Git 27 | remotes or to tweak your `.vimrc` for every workspace. 28 | 29 | ## Examples 30 | 31 | This defines reusable base environment and a few workspaces. 32 | 33 | ```nix 34 | { pkgs ? import { } }: 35 | 36 | let 37 | # Import this tool 38 | nix-workspaces = pkgs.callPackage (pkgs.fetchgit { 39 | url = "https://github.com/Julow/nix-workspaces"; 40 | rev = "c4ab335b9be04d7622bc3fa61defa552884fcff5"; 41 | sha256 = "1smh95p1blq2lq2l8v85lbqa5sc66j238m40y99j4xqfnigsspq6"; 42 | }) { }; 43 | 44 | # A reusable dev environment. 45 | dev_env = { 46 | buildInputs = with pkgs; [ git fd ]; 47 | vim.enable = true; 48 | }; 49 | 50 | # An other reusable environment built on top of the first one. 51 | nix_env = { 52 | imports = [ dev_env ]; 53 | buildInputs = with pkgs; [ nixfmt nix-prefetch-git ]; 54 | }; 55 | 56 | in nix-workspaces { 57 | # Define workspaces 58 | 59 | # Easy access to scratch workspaces. 60 | inherit dev_env nix_env; 61 | 62 | nix-workspaces = { 63 | imports = [ nix_env ]; 64 | git.remotes.origin = "https://github.com/Julow/nix-workspaces"; 65 | }; 66 | 67 | nixpkgs = { 68 | imports = [ nix_env ]; 69 | git.remotes.up = "https://github.com/NixOS/nixpkgs"; 70 | vim.vimrc = '' 71 | set path+=pkgs/top-level 72 | ''; 73 | }; 74 | } 75 | ``` 76 | 77 | An other example, defining a more complex `ocaml_env` environment. 78 | 79 | ```nix 80 | { pkgs ? import { } }: 81 | 82 | let 83 | nix-workspaces = pkgs.callPackage (pkgs.fetchgit { 84 | url = "https://github.com/Julow/nix-workspaces"; 85 | rev = "c4ab335b9be04d7622bc3fa61defa552884fcff5"; 86 | sha256 = "1smh95p1blq2lq2l8v85lbqa5sc66j238m40y99j4xqfnigsspq6"; 87 | }) { }; 88 | 89 | dev_env = { 90 | buildInputs = with pkgs; [ git fd ]; 91 | vim.enable = true; 92 | }; 93 | 94 | ocaml_env = { lib, config, ... }: { 95 | options = { 96 | # Define an option that every workspaces can set to a different value 97 | ocaml.ocamlformat = lib.mkOption { 98 | type = lib.types.package; 99 | default = pkgs.ocamlformat_0_17_0; 100 | }; 101 | }; 102 | imports = [ dev_env ]; 103 | config = { 104 | buildInputs = with pkgs; [ 105 | config.ocaml.ocamlformat 106 | opam ocaml ocamlPackages.ocp-indent # Tools 107 | m4 gmp libev pkgconfig # Dependencies of some important packages 108 | ]; 109 | vim.vimrc = '' 110 | " This is project wide and not local to ft=ocaml 111 | set makeprg=dune\ build 112 | let g:runtestprg = "dune runtest --auto-promote" 113 | nnoremap F :!dune build @fmt --auto-promote 114 | ''; 115 | activation_script = '' 116 | eval `opam env` 117 | ''; 118 | }; 119 | }; 120 | 121 | in nix-workspaces { 122 | inherit ocaml_env; 123 | 124 | ocamlformat = { 125 | imports = [ ocaml_env ]; 126 | git.remotes = { 127 | origin = "https://github.com/Julow/ocamlformat"; 128 | up = "https://github.com/ocaml-ppx/ocamlformat"; 129 | }; 130 | buildInputs = with pkgs; [ parallel ]; 131 | vim.vimrc = '' 132 | autocmd FileType ocaml set formatprg=dune\ exec\ --\ ocamlformat\ --name\ %\ - 133 | ''; 134 | }; 135 | } 136 | ``` 137 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | 3 | import ./workspaces.nix { inherit pkgs; } 4 | -------------------------------------------------------------------------------- /modules/git.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | conf = config.git; 7 | 8 | esc = escapeShellArg; 9 | 10 | mapAttrsToLines = f: attrs: concatStringsSep "\n" (mapAttrsToList f attrs); 11 | 12 | get_remote = role: remote: 13 | let r = getAttr remote conf.remotes; 14 | in if isAttrs r then getAttr role r else r; 15 | 16 | # Generate the [remote "name"] section of a remote. 17 | mk_remote_conf = name: url: { 18 | "remote \"${name}\"" = { 19 | url = if isAttrs url then url.fetch else url; 20 | pushurl = mkIf (isAttrs url) url.push; 21 | fetch = "+refs/heads/*:refs/remotes/${name}/*"; 22 | }; 23 | }; 24 | 25 | # Write a git config into a file. Argument is a 26 | # attributes of attributes of strings 27 | gen_git_config = filename: conf: 28 | builtins.toFile filename (mapAttrsToLines (section: opts: '' 29 | [${section}] 30 | ${mapAttrsToLines (key: val: " ${key} = ${val}") opts} 31 | '') conf); 32 | 33 | in { 34 | options.git = with types; { 35 | remotes = mkOption { 36 | type = attrsOf (either str (submodule [{ 37 | options = { 38 | fetch = mkOption { type = str; }; 39 | push = mkOption { type = str; }; 40 | }; 41 | }])); 42 | default = { }; 43 | description = '' 44 | Git remotes. When the workspace is activated, new remotes defined here 45 | are added to the Git repository automatically and changed URL are 46 | updated. 47 | Adding a remote is enough to activate this module. The repository is 48 | cloned on the first time the workspace is opened. 49 | ''; 50 | }; 51 | 52 | main_branch = mkOption { 53 | type = nullOr str; 54 | default = null; 55 | description = '' 56 | Defines the 'MAIN' symbolic ref. Also used for the initial checkout. 57 | By default, the default branch is taken from the remote repository 58 | during the initial checkout. This can only work if the 'main_remote' 59 | option is set to a configured remote. 60 | If it is not set at the time of activation, it is guessed. 61 | ''; 62 | }; 63 | 64 | main_remote = mkOption { 65 | type = str; 66 | default = "origin"; 67 | description = "See the 'main_branch' option."; 68 | }; 69 | 70 | gitignore = mkOption { 71 | type = lines; 72 | default = ""; 73 | description = "Local gitignore rules."; 74 | }; 75 | 76 | config = mkOption { 77 | type = attrsOf (attrsOf str); 78 | description = '' 79 | Local git config options. The toplevel attributes represents the 80 | sections and the nested attributes are the config options. 81 | '"remotes.*".url' and similar are set by the 'remotes' option. 82 | ''; 83 | }; 84 | }; 85 | 86 | config = mkIf (conf.remotes != { }) { 87 | buildInputs = with pkgs; [ git ]; 88 | 89 | git.config = { 90 | core.excludesFile = mkIf (conf.gitignore != "") 91 | (builtins.toFile "gitignore" conf.gitignore); 92 | 93 | } // concatMapAttrs mk_remote_conf conf.remotes; 94 | 95 | # If the 'main_remote' option correspond to a configured remote, 96 | # Use 'git clone' if possible, fallback to 'git init' if the 97 | # 'main_remote' option doesn't correspond to a configured remote. 98 | # If the main branch is set to be guessed, it might only be set in the 99 | # 'git clone' branch and won't be set in the fallback branch. 100 | init_script = '' 101 | ${if hasAttr conf.main_remote conf.remotes then '' 102 | git clone --origin=${esc conf.main_remote} ${ 103 | esc (get_remote "fetch" conf.main_remote) 104 | } . 105 | ${if conf.main_branch == null then '' 106 | # Set the 'MAIN' symbolic ref to the HEAD advertised by the remote. 107 | MAIN=$(git symbolic-ref --short HEAD) 108 | git symbolic-ref MAIN "refs/heads/$MAIN" 109 | '' else 110 | ""} 111 | '' else '' 112 | git init ${ 113 | if conf.main_branch != null then 114 | "--initial-branch=${esc conf.main_branch}" 115 | else 116 | "" 117 | } 118 | ''} 119 | git fetch --all --tags --update-head-ok --no-show-forced-updates --force 120 | ''; 121 | 122 | activation_script = '' 123 | ${ 124 | # Remove remotes and ignore rules previously set using 'git remote' and 125 | # '.git/info/exclude' as they take precedence over the new method using an 126 | # included config file. 127 | ""} 128 | # Migrate workspaces using an old format 129 | if ! git config get --local --value="^/nix/store/.*-workspace.git$" include.path &>/dev/null; then 130 | # Remove all remotes 131 | while read name; do 132 | git remote remove "$name" 133 | done < <(git remote) 134 | rm -f .git/info/exclude # Now set through config 135 | fi 136 | 137 | git config set --local --all --value="^/nix/store/.*-workspace.git$" "include.path" ${ 138 | gen_git_config "workspace.git" conf.config 139 | } 140 | 141 | ${ 142 | # If the 'main_branch' option is set, the 'MAIN' symbolic ref is updated to 143 | # point to the specified branch. 144 | # If it is not set, the branch to use is guessed from a list of probable main 145 | # branches. 146 | if conf.main_branch == null then '' 147 | guess_default_branch () 148 | { 149 | local default=$(git config init.defaultBranch) 150 | for guess in "$default" main master trunk; do 151 | if [[ -e .git/refs/heads/$guess ]]; then 152 | git symbolic-ref MAIN "refs/heads/$guess" 153 | return 154 | fi 155 | done 156 | } 157 | 158 | if ! [[ -e .git/MAIN ]]; then guess_default_branch; fi 159 | '' else '' 160 | echo "ref: "${esc "refs/heads/${conf.main_branch}"} > .git/MAIN 161 | ''} 162 | ''; 163 | 164 | }; 165 | } 166 | -------------------------------------------------------------------------------- /modules/github.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, ... }: 2 | 3 | # Automatically set git remotes URL for Github repositories. 4 | # To easily use this module, use a reusable workspace like: 5 | # 6 | # gh = { 7 | # github.origin = "my username"; 8 | # }; 9 | # 10 | # It's also a good place to set 'github.ssh_prefix'. 11 | # Then import in every workspaces that use Github: 12 | # 13 | # imports = [ gh ]; 14 | # 15 | 16 | with lib; 17 | 18 | let 19 | conf = config.github; 20 | 21 | mkUrl = prefix: org: "${prefix}${org}/${config.name}"; 22 | mkHttpUrl = mkUrl "https://github.com/"; 23 | mkSshUrl = mkUrl conf.ssh_prefix; 24 | 25 | mkFetchUrl = if conf.private then 26 | assert assertMsg (conf.ssh_prefix != null) 27 | "'github.ssh_prefix' must be set when 'github.private' is set to 'true'."; 28 | mkSshUrl 29 | else 30 | mkHttpUrl; 31 | mkPushUrl = if conf.ssh_prefix != null then mkSshUrl else mkHttpUrl; 32 | 33 | mkPushPullUrl = org: { 34 | fetch = mkFetchUrl org; 35 | push = mkPushUrl org; 36 | }; 37 | 38 | in { 39 | options = with lib; { 40 | github.origin = mkOption { 41 | type = types.nullOr types.string; 42 | default = null; 43 | description = '' 44 | Set 'git.remotes.origin' to point to the Github repository 45 | '/'. 46 | ''; 47 | }; 48 | 49 | github.up = mkOption { 50 | type = types.nullOr types.string; 51 | default = null; 52 | description = '' 53 | Same as the 'origin' option but for a remote named 'up'. 54 | ''; 55 | }; 56 | 57 | github.extra_remotes = mkOption { 58 | type = types.listOf types.string; 59 | default = [ ]; 60 | description = '' 61 | Extra remotes, named after the user hosting the repository. Useful for forks, for example. 62 | ''; 63 | }; 64 | 65 | github.ssh_prefix = mkOption { 66 | type = types.nullOr types.string; 67 | default = null; 68 | description = '' 69 | Prefix to use for SSH urls, which are used for 'push' url. If this is 70 | unset, only HTTPS urls are used. 71 | ''; 72 | }; 73 | 74 | github.private = mkOption { 75 | type = types.bool; 76 | default = false; 77 | description = "Use the SSH url for both push and fetch."; 78 | }; 79 | }; 80 | 81 | config = { 82 | git.remotes = { 83 | up = lib.mkIf (conf.up != null) (mkFetchUrl conf.up); 84 | origin = lib.mkIf (conf.origin != null) (mkPushPullUrl conf.origin); 85 | } // lib.genAttrs conf.extra_remotes mkPushPullUrl; 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /modules/shell_nix.nix: -------------------------------------------------------------------------------- 1 | { config, pkgs, lib, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | conf = config.shell_nix; 7 | 8 | nixpkgs_incl = if conf.nixpkgs != null then 9 | "-I nixpkgs=${escapeShellArg conf.nixpkgs}" 10 | else 11 | ""; 12 | 13 | # The shell's dependencies are rooted and the derivation is cached to make 14 | # opening shells persistent. Opening a shell is faster and doesn't require 15 | # internet access until the 'shell.nix' file changes. 16 | # https://github.com/NixOS/nix/issues/2208 17 | cached_shell_roots_sh = ''"$HOME/${config.cache_dir}/shell_roots"''; 18 | cached_shell_drv_sh = ''"$HOME/${config.cache_dir}/shell.drv"''; 19 | cached_shell_nix_sh = ''"$HOME/${config.cache_dir}/shell.nix"''; 20 | 21 | in { 22 | options.shell_nix = { 23 | enabled = mkOption { 24 | type = types.bool; 25 | default = false; 26 | description = "Whether use 'shell.nix' in the workspace's tree."; 27 | }; 28 | 29 | nixpkgs = mkOption { 30 | type = types.nullOr types.path; 31 | default = null; 32 | description = "Pinned nixpkgs for evaluating the local shell."; 33 | }; 34 | }; 35 | 36 | config = mkIf conf.enabled { 37 | activation_command = '' 38 | if [[ -e ./shell.nix ]] 39 | then 40 | # Evaluate and build only when 'shell.nix' changes 41 | set -ex 42 | if ! diff ./shell.nix ${cached_shell_nix_sh} &>/dev/null; then 43 | mkdir -p ${cached_shell_roots_sh} 44 | nix-instantiate shell.nix ${nixpkgs_incl} --indirect --add-root ${cached_shell_drv_sh} 45 | nix-store --indirect --add-root ${cached_shell_roots_sh}/result --realise $(nix-store --query --references ${cached_shell_drv_sh}) 46 | cat ./shell.nix > ${cached_shell_nix_sh} 47 | fi 48 | exec nix-shell ${cached_shell_drv_sh} --run ${ 49 | escapeShellArg config.command 50 | } 51 | else ${config.command} 52 | fi 53 | ''; 54 | }; 55 | } 56 | -------------------------------------------------------------------------------- /modules/tools.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, ... }: 2 | 3 | with lib; 4 | 5 | { 6 | options = { 7 | tools = mkOption { 8 | type = types.listOf types.path; 9 | default = [ ]; 10 | description = "Scripts that will added to the PATH."; 11 | }; 12 | 13 | }; 14 | 15 | config = mkIf (config.tools != [ ]) { 16 | buildInputs = let 17 | # Sorted by input path to remove duplicates due to diamond shaped 18 | # imports. 19 | tools = attrValues (listToAttrs (map (p: { 20 | name = toString p; 21 | value = { 22 | name = "bin/${baseNameOf p}"; 23 | path = builtins.path { path = p; }; 24 | }; 25 | }) config.tools)); 26 | drv = (pkgs.linkFarm "tools" tools); 27 | in [ drv ]; 28 | 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /modules/vim.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | # A script that calls 'vim' from the user's env. 7 | # This is the default value for the 'vim.bin' option. 8 | impure_vim = pkgs.writeShellScript "impure-vim" '' 9 | exec vim "$@" 10 | ''; 11 | 12 | # Escaped for bash 13 | viminfo_path_esc = ''"$HOME"/${escapeShellArg config.cache_dir}/viminfo''; 14 | session_path_esc = ''"$HOME"/${escapeShellArg config.cache_dir}/session.vim''; 15 | 16 | vimrc_file = builtins.toFile "vimrc" config.vim.vimrc; 17 | 18 | in { 19 | options = { 20 | vim = { 21 | enable = mkOption { 22 | type = types.bool; 23 | default = false; 24 | description = '' 25 | Enable vim. The 'command' is set to call Vim, with a custom .vimrc 26 | and a session file. 27 | The session file is saved automatically when Vim exists. (eg. with :qa) 28 | ''; 29 | }; 30 | 31 | bin = mkOption { 32 | type = types.path; 33 | default = impure_vim; 34 | description = 35 | "Vim binary to use. The default is to lookup vim from the PATH."; 36 | }; 37 | 38 | vimrc = mkOption { 39 | type = types.lines; 40 | default = ""; 41 | description = "Local vimrc."; 42 | }; 43 | 44 | }; 45 | }; 46 | 47 | config = mkIf (config.vim.enable != "") { 48 | command = '' 49 | exec ${config.vim.bin} -i ${viminfo_path_esc} -S ${vimrc_file} -S ${session_path_esc} 50 | ''; 51 | 52 | activation_script = '' 53 | session_path=${session_path_esc} 54 | if ! [[ -e $session_path ]]; then 55 | echo "let v:this_session = \"$session_path\"" > "$session_path" 56 | fi 57 | 58 | ''; 59 | 60 | vim.vimrc = '' 61 | " Remove some session options to make it work better with automatic sessions 62 | set sessionoptions=blank,help,tabpages,winsize,terminal 63 | 64 | autocmd VimLeave * execute "mksession!" v:this_session 65 | ''; 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /modules/xdg.nix: -------------------------------------------------------------------------------- 1 | { pkgs, config, lib, ... }: 2 | 3 | with lib; 4 | 5 | let 6 | conf = config.xdg.config; 7 | 8 | # All the parent directories of 'conf.files' 9 | files_prefixes = 10 | unique ([ "." ] ++ mapAttrsToList (rel: _: dirOf rel) conf.files); 11 | in { 12 | options = { 13 | xdg.config = { 14 | enable = mkOption { 15 | type = lib.types.bool; 16 | default = false; 17 | description = '' 18 | Set '$XDG_CONFIG_HOME' to a path unique to each workspace. See the 19 | option 'cache_dir'. 20 | ''; 21 | }; 22 | 23 | files = mkOption { 24 | type = lib.types.attrsOf types.path; 25 | default = { }; 26 | description = '' 27 | Static files to link into the xdg config directory. 28 | Attribute name is relative path into the xdg/config directory. 29 | ''; 30 | }; 31 | }; 32 | }; 33 | 34 | config = mkIf conf.enable { 35 | activation_script = '' 36 | export XDG_CONFIG_HOME=$HOME/${escapeShellArg config.cache_dir}/xdg/config 37 | 38 | mkdir -p ${ 39 | concatStringsSep " " 40 | (map (p: ''"$XDG_CONFIG_HOME"/${escapeShellArg p}'') files_prefixes) 41 | } 42 | 43 | ${concatStringsSep "\n" (mapAttrsToList (rel: dst: '' 44 | ln -sf ${escapeShellArg dst} "$XDG_CONFIG_HOME"/${escapeShellArg rel} 45 | '') conf.files)} 46 | ''; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /workspaces.nix: -------------------------------------------------------------------------------- 1 | { pkgs }: 2 | 3 | with pkgs.lib; 4 | 5 | let 6 | eval_modules = modules: 7 | (evalModules { 8 | modules = [{ _module.args = { inherit pkgs; }; }] ++ modules; 9 | }).config; 10 | 11 | base_module = { config, ... }: { 12 | imports = [ 13 | modules/git.nix 14 | modules/github.nix 15 | modules/shell_nix.nix 16 | modules/tools.nix 17 | modules/vim.nix 18 | modules/xdg.nix 19 | ]; 20 | 21 | options = { 22 | name = mkOption { 23 | type = types.str; 24 | default = null; 25 | description = 26 | "Workspace name. Defaults to the attribute name used to define it."; 27 | }; 28 | 29 | init_script = mkOption { 30 | type = types.lines; 31 | default = ""; 32 | description = "Run when the workspace is activated for the first time."; 33 | }; 34 | 35 | activation_script = mkOption { 36 | type = types.lines; 37 | default = ""; 38 | description = "Script run when entering a workspace."; 39 | }; 40 | 41 | command = mkOption { 42 | type = types.str; 43 | default = "${pkgs.bashInteractive}/bin/bash"; 44 | description = "Command to run after activation."; 45 | }; 46 | 47 | buildInputs = mkOption { 48 | type = types.listOf types.package; 49 | default = [ ]; 50 | description = "Workspace dependencies."; 51 | }; 52 | 53 | cache_dir = mkOption { 54 | type = types.str; 55 | default = ".cache/workspaces/${config.name}"; 56 | description = '' 57 | Directory for per-workspace cache, relative to the home directory. 58 | Used to store history files and other unimportant things. 59 | ''; 60 | }; 61 | 62 | activation_command = mkOption { 63 | type = types.str; 64 | default = config.command; 65 | description = 66 | "Command run at the end of the activation script. The default is to run 'command'."; 67 | }; 68 | }; 69 | 70 | config = { 71 | activation_script = '' 72 | export WORKSPACE=${config.name} 73 | export HISTFILE=$HOME/${config.cache_dir}/bash_history 74 | ''; 75 | }; 76 | 77 | }; 78 | 79 | # Options for the '_default' attribute. 80 | global_configuration = { config, ... }: { 81 | options = { 82 | prefix = mkOption { 83 | type = types.str; 84 | default = "$HOME/w"; 85 | description = '' 86 | Base path under which workspaces are located. Can contain bash 87 | substitutions, which will be evaluated when the entry script is 88 | called. 89 | ''; 90 | }; 91 | 92 | defaults = mkOption { 93 | type = with types; oneOf [ attrs (functionTo attrs) ]; 94 | # type = types.deferredModule {}; # Too recent 95 | default = { }; 96 | description = '' 97 | Configuration added to every workspaces. Useful to configure 98 | 'activation_command' or to add basic tools to 'buildInputs'. 99 | ''; 100 | }; 101 | }; 102 | }; 103 | 104 | make_workspace = { defaults, ... }: name: configuration: 105 | let default_name = { name = mkDefault name; }; 106 | in eval_modules [ 107 | base_module 108 | default_name # Base modules 109 | defaults # From global configuration 110 | configuration # User configuration 111 | ]; 112 | 113 | stdenv = pkgs.stdenvNoCC; 114 | 115 | # Generate the 'workspace-init' and 'workspace-activate' script for the 116 | # workspace. 117 | make_drv = w: 118 | stdenv.mkDerivation { 119 | name = strings.sanitizeDerivationName w.name; 120 | 121 | inherit (w) buildInputs; 122 | 123 | passAsFile = [ "init_script" "activation_script" ]; 124 | init_script = '' 125 | #!${pkgs.runtimeShell} 126 | ${w.init_script} 127 | ''; 128 | activation_script = '' 129 | mkdir -p "$HOME/${w.cache_dir}" 130 | ${w.activation_script} 131 | ${w.activation_command} 132 | ''; 133 | 134 | # Similar to 'pkgs.writeShellScriptBin', inlined to avoid generating many 135 | # store paths. 136 | # Some build variables defined by stdenv are hardcoded into the 137 | # activation script to avoid needing 'nix-shell': 'PATH' and some 138 | # variables used by pkg-config, gcc and ld wrappers. 139 | buildPhase = '' 140 | mkdir -p $out/bin 141 | mv $init_scriptPath $out/bin/workspace-init 142 | chmod +x $out/bin/workspace-init 143 | ${stdenv.shell} -n $out/bin/workspace-init 144 | keep_var() { for v in "$@"; do echo "export $v=''\'''${!v}'"; done; } 145 | { 146 | echo "#!${pkgs.runtimeShell}" 147 | echo "PATH='$PATH':\"\$PATH\"" 148 | for v in ''${!NIX_*}; do 149 | if [[ $v = *_FOR_TARGET || $v = *_TARGET_TARGET_* ]]; then 150 | keep_var $v 151 | fi 152 | done 153 | keep_var ''${!PKG_CONFIG_PATH_*} 154 | cat $activation_scriptPath 155 | } > $out/bin/workspace-activate 156 | chmod +x $out/bin/workspace-activate 157 | ${stdenv.shell} -n $out/bin/workspace-activate 158 | ''; 159 | preferLocalBuild = true; 160 | allowSubstitutes = false; 161 | 162 | phases = [ "buildPhase" "fixupPhase" ]; 163 | }; 164 | 165 | # Hard code workspace derivation paths into the script 166 | make_entry_script = { prefix, ... }: 167 | workspaces: 168 | pkgs.writeShellScriptBin "workspaces" '' 169 | declare -A workspaces 170 | workspaces=( 171 | ${ 172 | concatMapStrings (drv: '' 173 | ["${drv.name}"]="${drv}" 174 | '') workspaces 175 | } 176 | ) 177 | # Sorted list of workspaces for use in the 'list' and 'status' commands. 178 | workspaces_names=( 179 | ${concatMapStringsSep " " (drv: ''"${drv.name}"'') workspaces} 180 | ) 181 | PREFIX=${prefix} 182 | ${readFile ./workspaces.sh} 183 | ''; 184 | 185 | in config: 186 | # Entry point. 'config' is a attributes set of workspaces. See 'base_module' 187 | # above for the low-level options and './modules' for modules. 188 | let 189 | global_config = 190 | eval_modules [ global_configuration (config._default or { }) ]; 191 | 192 | workspaces_def = builtins.removeAttrs config [ "_default" ]; 193 | workspaces = 194 | mapAttrsToList (make_workspace global_config) workspaces_def; 195 | workspaces_drv = map make_drv workspaces; 196 | 197 | entry_script = make_entry_script global_config workspaces_drv; 198 | 199 | in entry_script 200 | -------------------------------------------------------------------------------- /workspaces.sh: -------------------------------------------------------------------------------- 1 | # this is not a standalone shell script. 2 | : ${workspaces[@]:?This script is not intended to be executed} 3 | : ${PREFIX:?} ${workspaces_names:?} 4 | 5 | set -e 6 | 7 | open_workspace () 8 | { 9 | local wname="$1" pkg p 10 | p=$PREFIX/$wname 11 | pkg=${workspaces[$wname]:?Workspace $wname not found.} 12 | 13 | if ! [[ -d "$p" ]]; then 14 | mkdir -p "$p" # side effect 15 | # First time opening this workspace, call 'workspace-init' 16 | cd "$p" 17 | "$pkg"/bin/workspace-init 18 | fi 19 | 20 | cd "$p" 21 | "$pkg"/bin/workspace-activate 22 | } 23 | 24 | C_RESET=$'\033[0m' 25 | C_GREEN=$'\033[0;32m' 26 | C_RED=$'\033[0;31m' 27 | C_GREY=$'\033[1;30m' 28 | C_PURPLE=$'\033[0;35m' 29 | 30 | workspace_status () 31 | { 32 | local wname=$1 p=$PREFIX/$1 33 | local prefix="" prefix_color="" status_color status 34 | local first_line line dirty=0 35 | if ! [[ -d $p ]]; then 36 | status=Uninitialized; status_color=$C_GREY 37 | elif ! [[ -d $p/.git ]]; then 38 | status=Initialized; status_color=$C_RESET 39 | elif ! { 40 | # The first line gives the checked-out branch and whether there are 41 | # unpushed changes 42 | read first_line 43 | while read line; do 44 | if ! [[ $line = [?]* ]]; then dirty=1; fi 45 | done 46 | } < <(git -C "$p" status -bs -unormal --no-renames --porcelain=v1); then 47 | status="Error getting git status"; status_color=$C_RED 48 | else 49 | first_line=${first_line#\#\# } 50 | status_color=$C_RESET; prefix="Clean"; prefix_color=$C_GREEN 51 | status="${first_line%%...*}" 52 | if [[ $first_line = *"]" ]]; then # Unpushed changes 53 | status="$status [${first_line##*[}"; status_color=$C_PURPLE; prefix_color=$C_PURPLE 54 | fi 55 | if [[ $dirty -eq 1 ]]; then prefix="Dirty"; prefix_color=$C_RED; fi 56 | fi 57 | printf "$prefix_color%-5s $status_color%s %s$C_RESET\n" \ 58 | "$prefix" "$wname" "$status" 59 | } 60 | 61 | USAGE_OPEN="open " 62 | 63 | cmd=$1 64 | shift 65 | 66 | case "$cmd" in 67 | "open") 68 | wname=${1:?Usage: workspaces $USAGE_OPEN} 69 | open_workspace "$wname" 70 | ;; 71 | 72 | "list") 73 | for wname in "${workspaces_names[@]}"; do 74 | echo "$wname" 75 | done 76 | ;; 77 | 78 | "status") 79 | for wname in "${workspaces_names[@]}"; do 80 | workspace_status "$wname" 81 | done 82 | ;; 83 | 84 | *) 85 | cat <&2 86 | Usage: workspaces { open | list } 87 | 88 | $USAGE_OPEN 89 | Open the specified workspace. A directory in $PREFIX is created if it 90 | doesn't exist, the activation script is run and finally the shell is opened. 91 | 92 | list 93 | List workspaces. 94 | 95 | status 96 | Show status for each workspaces, including Git status. 97 | EOF 98 | ;; 99 | esac 100 | --------------------------------------------------------------------------------