Como programadores, creo que todos hemos tenido algun encontronazo con los gestores de paquetes, ya sea para instalar un proyecto de python o tener que arreglar algún conflicto de paquetes en Linux. La realidad es que no hay ninguna solución perfecta para la gestión de paquetes de nuestros proyectos, pero Nix es una de las mejores opciones.
En este articulo explicare que es Nix, como lo podemos utilizar para nuestros proyectos y en especifico como lo he usado para este proyecto.
Que es Nix
En realidad Nix no es mas que un gestor de paquetes, con la peculiaridad de que se puede instalar en una gran variedad de sistemas (Distribuciones de Linux, MacOS, Windows con WSL)
La gran diferencia de Nix frente a otros gestores de paquetes es que funciona de una manera declarativa, utilizando expresiones escritas en el lenguaje Nix (al que me referiré como nixlang para poder diferenciarlos mejor) esto nos deja mantener una configuración versionada y estable sin tener que preocuparnos de paquetes huérfanos, dependencias y otros de los grandes problemas de los gestores de paquetes convencionales.
Otra diferencia con otros gestores de paquetes son las generaciones, una manera de mantener varias versiones de nuestros paquetes instaladas para que si una actualización los rompe, podamos volver a una generación anterior, donde las diferentes versiones de estas no tengan conflictos.
Como instalarlo
Para ver todas las opciones de instalación podemos ir a nixos.org, por ejemplo, para instalarlo en Linux podemos ejecutar el siguiente comando:
sh <(curl -L https://nixos.org/nix/install) --daemon
Configuración del sistema con nix
Una de las funciones mas útiles para nuestro día a día de nix es su habilidad de instalar y configurar paquetes de forma declarativa, en esta sección explicare como podemos administrar nuestro sistema con Nix, Home Manager y NixOS.
Administración de paquetes
Para administrar paquetes de una forma declarativa y portable usaremos Home Manager.
{
description = "Home managet test";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs @ {
self,
home-manager,
nixpkgs,
...
}: rec {
homeConfigurations = {
"tu nombre de usuario" = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
home = {
username = "tu nombre de usuario";
stateVersion = "22.11";
packages = with pkgs; [
# Los paquetes que quieras instalar
vlc
discord
];
};
programs.homeManager.enable = true;
};
};
};
}
Para activar nuestra configuración, ejecutaremos:
home-manager switch --flake .
Aunque el flake que hemos creado parezca archivo bastante complejo, pero explicándolo paso a paso entenderemos las partes mas importantes.
Inputs
Las inputs
o entradas son los archivos internos que nuestro flake utilizara, en este caso nixpkgs
que es la colección de oficial paquetes para nix y home-manager
es el modulo de Nix que nos deja configurar nuestro sistema de una forma declarativa en varios sistemas *nix.
Outputs
Los outputs
o salidas son, en este caso las configuraciones de Home Manager que exporta; estas se exportan como homeConfigurations
.
home-manager.lib.homeManagerConfiguration
es la función que genera una configuración de Home Manager con los valores que especificamos. Aquí especificamos los paquetes para nuestra plataforma y los paquetes que queremos instalar.
Configuración de paquetes
Ahora, con una ligera modificación a nuestra configuración, podemos configurar los paquetes instalados.
{
...
homeConfigurations = {
"tu nombre de usuario" = home-manager.lib.homeManagerConfiguration {
...
let
email = "Tu correo electronico";
name = "Tu nombre";
in {
programs.git = {
enable = true;
lfs.enable = true;
extraConfig = {
color.ui = true;
core.editor = "emacs";
credential.helper = "store";
github.user = name;
push.autoSetupRemote = true;
};
userEmail = email;
userName = name;
};
};
};
};
...
}
En esta configuración configuramos e instalamos git; al hacer programs.git.enable
verdadero, automáticamente git se añadirá a los paquetes a instalar.
Utilizando el let {} in {}
podemos especificar una variable local para poder cambiar nuestro correo electronico y contraseña de una manera fácil.
Aquí también habilitamos git-lfs
y especificamos algunas configuraciones extras que automáticamente se traducen de la sintaxis de nix a la del archivo de configuración sin chequearas. En este caso forzamos a que la salida de terminal tenga colores, que el editore que usa, el credential helper, nuestro usuario de git y que se configure automaticamente el remoto para pushear.
Configuración del sistema
Con NixOS podremos configurar nuestro sistema entero de esta manera, ya sea el cargador de arranque, los drivers de video o lo que quieras, pero primero tendremos que hacer unos cambios a nuestro flake para exportar una configuración de NixOS.
{
description = "NixOS config test";
outputs = inputs @ {
self,
home-manager,
nixpkgs,
...
}: rec {
nixosConfigurations = {
"nombre de tu configuración" = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = {inherit inputs;};
modules = [
home-manager.nixosModules.home-manager
{
networking.hostName = "hostname";
system.stateVersion = "24.05";
ollama = {
enable = true;
acceleratiom = "rocm";
};
}
./ollama.nix
];
};
};
};
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
};
}
En nuestro flake actualizado, cambiamos home-manager.lib.homeManagerConfiguration
por nixpkgs.lib.nixosSystem
, que es la función que genera un sistema de NixOS basada en la configuración que le pasamos.
Nuestro primer modulo
Para organizar mejor nuestra configuración, podemos usar módulos, que nos dejan elegir que parte de nuestra configuración queremos utilizar y hacer nuestra configuración configurable. Con un ejemplo, lo entenderemos mejor.
En este ejemplo configuraremos ollama, que es un servicio para correr y descargar LLMs en local.
Para utilizarlo lo guardaremos en el mismo directorio que el flake y lo llamaremos ollama.nix
{
lib,
config,
...
}:
with lib; let
cfg = config.ollama;
in {
options.ollama = {
enable = mkEnableOption "ollama";
models = mkOption {
type = types.listOf types.str;
default = ["mistral-small"];
};
acceleration = mkOption {
type = types.nullOr (
types.enum [
false
"rocm"
"cuda"
]
);
default = null;
};
ui = mkEnableOption "Enable web ui";
};
config = mkIf cfg.enable {
services = {
ollama = {
enable = true;
loadModels = cfg.models;
acceleration = cfg.acceleration;
};
nextjs-ollama-llm-ui.enable = cfg.ui;
};
};
}
Este modulo tiene una sintaxis bastante diferente a los flakes que hemos visto antes, vamos a repasar su sintaxis:
Imports
Vemos que nuestro modulo empieza con un bloque que no habíamos visto antes, haciendo que nuestro flake se vea así:
{
imports
}: {
code
}
En nuestro bloque de imports, importamos lib
, config
y el resto de imports por defecto (...
) para poder usarlos en nuestro modulo. lib
importa muchas funciones y variables útiles para nuestra configuración, y config
que nos deja exportar nuestras opciones de configuración.
options
En este bloque declaramos que ollama
sera la opción de configuración que definiremos, dentro tenemos 4 variables:
enable
Que es un EnableOption
, un booleano para activar el modulo de ollama y por defecto falso. También tiene el comentario de uso "ollama"
.
models
Declara los nombres de los modelos que automáticamente descargará, por lo que su tipo es una lista (listOf
) de strings (str
). Por defecto tendrá el valor ["mistral-small"]
.
acceleration
Si utilizar la aceleración de hardware, en este caso el tipo de datos es mas complejo por que queremos representar mas opciones; puede ser o null
que deja a Nix elegir la aceleración, o puedes forzarlo usando uno de los valores del enum: false
para apagar la aceleración, rocm
para usar la de AMD y cuda
para usar la de Nvidia.
ui
Enciende la interfaz web para usar ollama desde el navegador.
config
En esta parte utilizaremos los valores de las variables declaradas en options
para crear nuestra configuración.
Primero, hacemos un if
con mkIf
para que cfg.enable
encienda nuestra configuración. En todas las demás opciones simplemente le pasamos el valor que configuramos.