Skip to content

Instantly share code, notes, and snippets.

@lheckemann
Last active March 11, 2024 13:11
Show Gist options
  • Save lheckemann/402e61e8e53f136f239ecd8c17ab1deb to your computer and use it in GitHub Desktop.
Save lheckemann/402e61e8e53f136f239ecd8c17ab1deb to your computer and use it in GitHub Desktop.
Expression for a buildEnv-based declarative user environment.

Expression for a buildEnv-based declarative user environment

This is one way of managing your user profile declaratively.

Alternatives include:

  • an attrset-based nix-env-based environment, installed using nix-env -ir rather than nix-env --set. LnL has an overlay which shows a way of doing this.
  • home-manager, which provides NixOS-like config for your $HOME

Note that this is incompatible with regular imperative use of nix-env, e.g. nix-env -iA nixpkgs.hello. It has the advantage of allowing the installation of multiple outputs of the same package much better than nix-env's builtin profile builder does.

I personally currently use home-manager.

Feature comparison:

Prevents imperative use of nix-env (nix-env -iA nixpkgs.hello)

  • buildEnv ✔️
    • buildEnv takes responsibility for the entire user profile, meaning that nix's builtin env builder cannot modify it
  • attrset ❌
    • the attrset approach leaves building the profile up to nix-env, which allows adding packages ad-hoc (though they will be removed on the next profile build) using nix-env -i
  • home-manager ❌
    • home-manager installs a single buildEnv into the user profile, remaining compatible with imperative/impure nix-env

Management of files outside ~/.nix-profile

  • buildEnv ❌
  • attrset ❌
  • home-manager ✔️

Declarative config like NixOS

  • buildEnv ❌
  • attrset ❌
  • home-manager ✔️

Gives good control over outputs installed

  • buildEnv ✔️
  • attrset ❌
  • home-manager ❓
/*
Save this file to ~/default.nix or your preferred path (make sure to
change the location in the update-profile script if you choose a
different place).
Install using `nix-env -f ~ --set`, from then on use `update-profile`.
*/
{ pkgs ? import <nixpkgs> {}
, name ? "user-env"
}: with pkgs;
buildEnv {
inherit name;
extraOutputsToInstall = ["out" "bin" "lib"];
paths = [
nix # If not on NixOS, this is important!
icewm
pavucontrol
redshift
firefox
(writeScriptBin "update-profile" ''
#!${stdenv.shell}
nix-env --set -f ~ --argstr name "$(whoami)-user-env-$(date -I)"
'')
# Manifest to make sure imperative nix-env doesn't work (otherwise it will overwrite the profile, removing all packages other than the newly-installed one).
(writeTextFile {
name = "break-nix-env-manifest";
destination = "/manifest.nix";
text = ''
throw ''\''
Your user environment is a buildEnv which is incompatible with
nix-env's built-in env builder. Edit your home expression and run
update-profile instead!
''\''
'';
})
# To allow easily seeing which nixpkgs version the profile was built from, place the version string in ~/.nix-profile/nixpkgs-version
(writeTextFile {
name = "nixpkgs-version";
destination = "/nixpkgs-version";
text = lib.version;
})
];
}
@ljdwigney
Copy link

Alternatives include:

  • an attrset-based nix-env-based environment, installed using nix-env -ir rather than nix-env --set. LnL has an overlay which shows a way of doing this.
  • home-manager, which provides NixOS-like config for your $HOME

Sorry if it's a stupid question but what actually are the disadvantages/advantages of these different approaches?

@rendaw
Copy link

rendaw commented Mar 9, 2020

Thanks, this is nice! It doesn't look like with xfce; is needed - that was confusing me, since I didn't want to install xfce.

So to elaborate, to use this:
0. Put this file in ~/default.nix - the instructions say you can put it where you want with whatever name you want but I'm not sure I trust that. The nix-env docs don't explain how just ~ is sufficient either.

  1. Modify the list of icewm through firefox with the packages you want
  2. Run nix-env -f ~ --set
  3. After that install/remove software by modifying this file and then running update-profile

After running the above script nix-env disappeared from my profile... may want to be careful doing this.

Also https://unix.stackexchange.com/questions/332272/name-collision-in-input-nix-expressions-with-nix-env-f recommends against using nix-env -f ~.

@rendaw
Copy link

rendaw commented Mar 9, 2020

Okay, this is what I ended up going with:

$ cat ~/.nix-defexpr/default.nix 
{}: with import <nixpkgs> {}; buildEnv {
  name = "user-env";
  extraOutputsToInstall = ["out" "bin" "lib"];
  paths = [
    nix  # important!
    ...my packages...
  ];
}

If you place the file in the location above it will be automatically used if no other expression is listed, so you can just do nix-env --set. IMO this is much simpler. You need to include nix in paths or else you'll destroy your profile... neither this nor the overlay gist mention this which surprises me, maybe I got something wrong.

@lheckemann
Copy link
Author

@ljdwigney Took a while for me to get there, but I've added a feature comparison.

@lheckemann
Copy link
Author

@rendaw thanks for the hint, I left that with xfce; in by accident.

Re. just ~ being enough: as documented for the --file option at https://nixos.org/nix/manual/#common-options, if nix is given a directory it will look for default.nix inside. As the comment saying you can put it anywhere says, you need to adjust the update-profile script to pass the new path if you do put it elsewhere.

Re. ~ making sense at all: if default.nix exists, nix will not touch any subdirectories.

Re. .nix-defexpr: that command is indeed quite pretty! The mechanism is awful though, nix-env is the only nix command that uses it and the search behaviour can be rather unexpected. This is why things like these profile management tools exist in the first place, because nix-env doesn't do a very good job by itself… Anyway, don't let my opinion that nix-defexpr shouldn't exist stop you from using it ;)

@lheckemann
Copy link
Author

Oh, last point I missed: re nix being required: sorry, this is often missed by NixOS users because nix is installed system-wide and shouldn't usually be in user profiles there. I'll add it in here :)

@AndersonTorres
Copy link

Hello, people! Sorry by the inconvenience, but how can I use this configuration file with the new Nix Flakes framework?

@lheckemann
Copy link
Author

I don't know, I think that's still pending new profile management tools. For now, use classic nix-env I guess.

@jnetod
Copy link

jnetod commented Nov 29, 2020

@AndersonTorres I tried, without success, to reproduce this with nix profile; but I found a way to use buildenv with flakes (in a certain way), you'll need 3 files, besides buildenv.nix:

# flake.nix

{
  description = "buildenv + flake";

  inputs = {
    nixpkgs.url = github:NixOS/nixpkgs/nixos-20.09;
    flake-compat = {
      url = "github:edolstra/flake-compat";
      flake = false;
    };
  };

  outputs = { self, nixpkgs, flake-compat }: rec {
    system = "x86_64-linux";
    packages.${system}.user-env = import (./buildenv.nix) { pkgs = nixpkgs.legacyPackages.${system}; };
    defaultPackage.x86_64-linux = self.packages.${system}.user-env;
  };
}
# default.nix

(import (
  let
    lock = builtins.fromJSON (builtins.readFile ./flake.lock);
  in fetchTarball {
    url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
    sha256 = lock.nodes.flake-compat.locked.narHash; }
) {
  src =  ./.;
}).defaultNix
# flake.lock

$ nix flake update

Then you can proceed with the installation, just like buildenv.nix instructions.

To update your buildenv you'll need to update your flake.lock first. You could try to put some update logic on update-profile script.

References:

@PanAeon
Copy link

PanAeon commented Aug 3, 2021

@AndersonTorres yet another way to use buildEnv with flakes and nix profile:

 {
  description = "A home flake";
  # original idea https://gist.github.com/suhr/4bb1f8434d0622588b23f9fe13e79973

  inputs = {
    nixpkgs-unstable.url = "nixpkgs/nixpkgs-unstable";
    neuron.url = "github:srid/neuron";
    neuron.inputs.nixpkgs.follows = "nixpkgs-unstable";
  };

  outputs = { self, nixpkgs, nixpkgs-unstable, neuron }: 
    let
      overlay-unstable = final: prev: {
        unstable = import nixpkgs-unstable {
          system = "x86_64-linux";
          config.allowUnfree = true;
        };
      };
    in {
    defaultPackage.x86_64-linux =  with import nixpkgs { system = "x86_64-linux"; overlays = [ overlay-unstable ]; config = { allowUnfree = true; }; }; buildEnv {
      name = "home-env";
      paths = [
      neuron.defaultPackage.x86_64-linux
     
      unstable.neovim
    
      elvish
      mednafen
      firefox-wayland
      
      ...
      ];
    };
  };
}

To install nix profile install .
And to upgrade nix profile upgrade '.*'

@AndersonTorres
Copy link

AndersonTorres commented Aug 5, 2021

@PanAeon many thanks! I have made a quick test and it is working! I will update my files accordingly.

P.S.: for those who may have problem with changing paths like the one below:

$ nix profile install
error: packages '/nix/store/d0wb2gam2pyksz2dsjx6sa83761jrrgi-hello-2.12/bin/hello' and '/nix/store/61n15131n3b9mn7ajvddbyfnv2b3pgjd-hello-2.12/bin/hello' have the same priority 5
; use 'nix-env --set-flag priority NUMBER INSTALLED_PKGNAME' to change the priority of one of the conflicting packages (0 being the highest priority)

The solution taken from NixOS/nix#6143 is just remove the profile before installing it.

@saccarosium
Copy link

Hello,
This gist helped me a lot for build my setup and I will share my config for hopefully helping others.

# flake.nix

{
  description = "my-user-env";

  # inputs are your dependencies
  inputs = {
    nixpkgs.url = "nixpkgs/nixos-unstable"; # where we get the packages
    flake-utils.url = "github:numtide/flake-utils"; # a small utility that helps not hardcode the system architecture. Something like 'x86_64-linux'
  };

  # Here we pass the input to outputs
  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let pkgs = nixpkgs.legacyPackages.${system};
      in
      rec {
        # Here we are importing a file in the same directory of the flake.nix, named 'profile.nix'
        packages.${system}.user-env = import (./profile.nix) { pkgs = nixpkgs.legacyPackages.${system}; };
        defaultPackage = packages.${system}.user-env;
      }
    );
}

ℹ️ NOTE
You have to change <path-to-flake> to the location you want

# profile.nix

{ pkgs ? import <nixpkgs> { }, name ? "user-env" }: with pkgs;

buildEnv {
  inherit name;
  # to get all the symlink we need
  extraOutputsToInstall = [ "out" "man" "lib" ];
  paths = [
    # ... put your packages here
    
    # this will create a script that will rebuild and upgrade your setup. Is using shell script syntax
    (writeScriptBin "nix-rebuild" ''
      #!${stdenv.shell}
      cd <path-to-flake> || exit 1
      nix flake update
      nix profile upgrade '.*'
    '')
    
    # puts in your root the nixpkgs version
    (writeTextFile {
      name = "nixpkgs-version";
      destination = "/nixpkgs-version";
      text = lib.version;
    })

  ];
}

To install your packages you only need to cding in the directory containing the flake and run nix profile install . and run nix-rebuild when you want to update

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment