Adrianistán

Primeros pasos con Nix: un Linux más funcional

25/01/2020

Como ya sabéis, me encanta experimentar cosas alternativas dentro de la informática. Cosas que quizá no sean populares ni vayan a serlo, pero aportan un punto de vista diferente. Es por eso que en este blog se habla de Prolog, hablamos de web semántica, de Rust y se habla de Haiku. Porque para ver el típico framework hecho en Java ya tienes cientos de blogs por ahí. En este blog intento hablar de cosas de las que poca gente habla. Hoy nos toca hablar de Nix, un gestor de paquetes funcional, y base de NixOS, una distribución Linux muy interesante. Sin embargo aquí solo nos centraremos en Nix.

Nix es un gestor de paquetes, al igual que pacman en Arch Linux, apt en Debian o yum en RedHat. Si solo fuese un sistema más, no tendría interés, no solo hay estos tres, hay muchos más. Pero Nix ofrece algo diferente. Nix es funcional. Nix no mantiene un estado global del sistema y nos permite describir entornos de forma declarativa. Nix permite hacer rollbacks instantáneos (incluso varias versiones). Nix es además un lenguaje de programación, similar a Haskell (perezoso, puro, con currificación, pero con tipado dinámico). Nix permite tener instaladas a la vez varias versiones de un mismo paquete, sin provocar conflictos. En la gran mayoría de sistemas de paquetes, el funcionamiento es sencillo: se desempaqueta, reemplazando los ficheros si los hubiese, dentro del sistema de carpetas global. En Nix, se gestiona un PATH de forma dinámica que nos permite acceder a los paquetes residentes en /nix. Pero no nos adelantemos, vayamos poco a poco.

Instalando Nix

Hay dos formas de instalar Nix. Una es instalarse NixOS, la distro diseñada para trabajar con Nix de forma nativa. Pero también podemos instalar Nix en cualquier otra distro Linux y en macOS. Para ello deberemos ejecutar el siguiente comando:


curl https://nixos.org/nix/install | sh

Esto nos instalará Nix para nuestro usuario, aunque necesitará permisos de root. Por defecto crea un perfil. En Nix podemos tener infinitos perfiles. Cada perfil es un entorno, con paquetes diferentes que no interfieren entre sí. Podemos pensar en ello como si fuesen contenedores o máquinas virtuales, pero en realidad son simplemente colecciones de enlaces simbólicos al Nix Store, la carpeta donde se guardan todos los paquetes (por defecto /nix/store). Cada perfil carga ciertos programas en el PATH, así como sus librerías.

Para cargar un perfil y usarlo desde Bash/Zsh tendremos que ejecutar el script nix.sh correspondiente:


source ~/.nix-profile/etc/profile.d/nix.sh

También lo podemos añadir a nuestro fichero .bashrc o equivalente para que no tengamos que hacerlo manualmente

.

Instalar, borrar y actualizar paquetes

El comando principal para manipular los paquetes dentro de los perfiles es nix-env. Para ver los paquetes que tenemos disponibles para instalar ejecutamos:


nix-env -qa

Para acordarse mejor, qa significa "query all". Si queremos ver los paquetes que tenemos instalados en el perfil, podemos ejecutar solamente


nix-env -q

Que es simplemente query. Para filtrar los paquetes podemos usar expresiones regulares estándar. Por ejemplo:


nix-env -qa "firefox.*"

Busca todos los paquetes disponibles que empiezen por firefox. Para instalar paquetes tenemos el flag i, de install. Vamos a instalar cowsay.


nix-env -i cowsay

Al instalar descarga el paquete y sus dependencias si no existiesen ya, las deja en el Nix Store y genera los enlaces simbólicos para que sean accesibles desde el perfil actual. Aquí pueden surgir varias dudas.

¿Si ya existe un paquete con otra versión que ocurre? Nix es quizá el gestor de paquetes más inteligentes al respecto. Si tienes ya descargada la misma versión exacta debido a otro paquete, se reutiliza, aprovechando el espacio. Sin embargo, Nix también quiere garantizar el funcionamiento de los programas y el determinismo de cuando se construyó el paquete, así que cuando las versiones divergen, se instalan ambas en paralelo. ¡Y ambas versiones de la librería funcionan a la vez en el mismo perfil! Esto se consigue gracias a la forma en la que se construyen los paquetes, que veremos más adelante.

¿Qué es el fichero drv? Es el fichero que almacena una derivación. Cuando realizamos modificaciones de los paquetes en Nix, estamos haciendo derivaciones. Esto nos facilita poder revertir los cambios y hace muy difícil que Nix se corrompa.

¿De dónde se descargan los paquetes? Los repositorios en Nix se llaman channels. Por defecto se usa nixpkg-unstable como repositorio, que es de tipo rolling release, disponiendo de los últimos paquetes siempre. Existen otros, no obstante, principalmente usados por NixOS. Los canales son en principio repositorios de código Nix, que dicen como compilar los paquetes. En la mayoría de los paquetes existen ya versiones compiladas, pero si no las hubiera la instalación procedería a compilar de cero el paquete.

Vamos a usar el programa

Ahora vamos a quitar el programa. Podemos hacer dos cosas. Podemos hacer rollback y restaurar la versión anterior del perfil o podemos realizar una operación de borrado (que genera una nueva derivación).

Para borrar, generando una nueva derivación:


nix-env -e cowsay

Para hacer rollback, podemos ejecutar:


nix-env --rollback

Que nos sirve para ir a la versión inmediatamente anterior. También podemos listar las generaciones y saltar a una de ellas


nix-env --list-generations
nix-env -G NUMERO_GENERATION

Cuando borramos o actualizamos un paquete, no se elimina nada, todo se mantiene, de esta forma podemos hacer rollbacks instantáneos. Pero una vez ya estamos seguros de que no necesitamos ese paquete por si acaso, podemos limpiar las generaciones antiguas donde se usaba. Para ello debemos ejecutar periódicamente el siguiente comando:


nix-collect-garbage
nix-collect-garbage -d

La segunda opción es más agresiva y perderemos la habilidad absoluta de hacer rollback, dejándonos solo con la última generación.

Para actualizar los paquetes usamos este comando:


nix-env -u

Gestionando canales

Los repositorios en Nix se llaman canales. Todas las operaciones con respecto a los canales se realizan con el comando nix-channel. Podemos ver los canales que tenemos con:


nix-channel --list

El canal nixpkgs-unstable es el que podemos encontrar en GitHub: https://github.com/NixOS/nixpkgs/.

Podemos actualizar los canales con update:


nix-channel --update

Para añadir un canal usamos add


nix-channel --add https://nixos.org/channels/channel-name nixos 

El lenguaje Nix

Nix es realmente un lenguaje de programación funcional, especialmente diseñado para manejar la paquetería del sistema, pero completo. Es similar a Haskell, aunque sin tipado estático. No voy a entrar en muchos detalles de este lenguaje, pero vamos a ver algunas cosillas que serán útiles para construir paquetes y manejar ficheros .nix

Podemos acceder a un REPL de Nix escribiendo


nix repl

En Nix todo son expresiones, que se evalúan de forma perezosa como en Haskell. La artimética es sencilla, con la única diferencia de que hay que hay que tener cuidado con la división. El símbolo / significa ruta de archivo, no división. Hay que usar la función builtins.div para dividir.

También existen listas, strings y attribute sets, que son como diccionarios. Para asignar variables usamos la sintaxis let/in, que también existe en Haskell. Básicamente, on let definimos las variables y en in la expresión que las usa.

Para acceder a los atributos de los attribute sets podemos usar la sintaxis punto o usar with para no tener que escribir. Así, estas dos líneas son equivalentes.

En Nix también hay sentencias if, pero al ser expresiones, siempre deben retornar un valor, el else es obligatorio siempre

Las funciones en Nix son anónimas, pero podemos almacenarlas en variables. Admiten varios argumentos, aunque si vienes de Haskell, que sepas que Nix también realiza currificación. Las funciones de Nix también admiten parámetros que sean un attribute set, y los puede descomponer directamente. Además esto nos permite definir argumentos opcionales.

Lo último que debemos saber del lenguaje es el uso de import, que sirve para cargar la expresión resultado de un archivo en otro

Creando paquetes

Una vez conocemos el lenguaje Nix, no es evidente como se crean paquetes. Existen dos formas básicas de crear paquetes: in-tree y out-tree. Los paquetes in-tree son los que componen nixpkgs, y básicamente cada paquete se define por una función que es llamada desde un punto de entrada principal, que es el índice de paquetes. Si queremos subir un paquete a nixpkgs deberemos hacerlos así. Sin embargo, si queremos empaquetar algo que no queremos que llegue a nixpkgs debemos hacerlo de forma que sea independiente, aunque usando algunas funciones de nixpkgs.

Lo primero que vamos a hacer es importar nixpkgs, para poder usar sus utilidades. La mayoría de paquetes se beneficiarán de las funciones prediseñadas. Stdenv tiene funcionalidad genérica, pero útil para programas en C/C++. Para otros lenguajes podemos usar este mismo módulo u otros más específicos (de Rust, de Haskell, ...). La expresión es una llamada a mkDerivation, con ciertos datos, entre ellos un builder, que será un script que construirá la aplicación, el origen de los datos (función fetchurl normalmente) y algunas dependencias, con buildInputs. Podemos pasar cualquier variable de Nix a Bash, simplemente añadiendo más variables a mkDerivation.


let 
    nixpkgs = import  {} ;
in
nixpkgs.stdenv.mkDerivation {
    name = "pcc-1.0.0" ;
    builder = ./builder.sh ;
    src = nixpkgs.fetchurl {
        url = https://github.com/aarroyoc/pcc/archive/3f90d424494f4d1971ea34e66883fdee8a587b1f.zip;
        sha256 = "ec80f0c8af5dc9d6f0fbb691a4132ac8d44e42dd05865e23c80c2e0f0219d56f";
    };
    buildInputs = [ nixpkgs.unzip nixpkgs.bison nixpkgs.flex];
}

o si usamos with para simplificar


with import  {};

stdenv.mkDerivation {
  name = "pcc-1.0.0";
  builder = ./builder.sh;
  src = fetchurl {
    url = https://github.com/aarroyoc/pcc/archive/3f90d424494f4d1971ea34e66883fdee8a587b1f.zip;
    sha256 = "ec80f0c8af5dc9d6f0fbb691a4132ac8d44e42dd05865e23c80c2e0f0219d56f";
  };
  buildInputs = [ unzip bison flex ];
}

Y el builder.sh contiene:


source $stdenv/setup

unzip $src
cd pcc-*
make
mkdir -p $out/bin
cp pcc $out/bin/

Dentro del script hay que cumplir varias normas. La primera línea, si usamos stdenv, debe ser llamar al setup. La variable src es la que hemos definido como resultado de fetchurl y la variable out representa la estructura que va a tener el paquete.

Con Nix podemos importar otros paquetes como base y personalizarlos obteniendo así paquetes personalizados de forma sencilla y reproducible.

Podemos compilar el paquete con nix-build


nix-build pcc.nix

E instalarlo


nix-env -f pcc.nix -i pcc

Lo que hacemos con nix-env es indicar que use otro archivo nix en vez de nixpkgs y de él, instale pcc.

Construyendo entornos

Con Nix también podemos construir entornos que tengan ciertos paquetes cargados, ideal para documentar el software exacto necesario para trabajar y desplegar programas. Para ello usaremos mkShell


let
  pkgs = import  {};
in

pkgs.mkShell {
    name = "python-datascience";
    buildInputs = with pkgs; [
        python38
        python38Packages.numpy
        python38Packages.scikitlearn
        python38Packages.scipy
        python38Packages.matplotlib
    ];
}

Y lo cargamos


nix-shell datascience.nix

Y ya tendríamos un entorno para ejecutar nuestros scripts de NumPy, SciPy y Sklearn. Estos shells se construyen como si fuesen paquetes y también se pueden distribuir. Si faltase algún paquete de Python, podríamos añadirlo fácilmente a Nix. Por ejemplo, suponiendo que no tuviésemos paquete para requests, usaríamos las funciones de Nix para Python


let
  pkgs = import  {};
  requests = pkgs.python38.pkgs.buildPythonPackage rec {
      pname = "requests";
      version = "2.22.0";
      src = pkgs.python38.pkgs.fetchPypi {
          inherit pname version;
          sha256 = "1d5ybh11jr5sm7xp6mz8fyc7vrp4syifds91m7sj60xalal0gq0i";
      };
      doCheck = false;
      buildInputs = with pkgs; [
          python38
          python38Packages.chardet
          python38Packages.idna
          python38Packages.urllib3
      ];
  };
in

pkgs.mkShell {
    name = "python-datascience";
    buildInputs = with pkgs; [
        python38
        python38Packages.numpy
        python38Packages.scikitlearn
        python38Packages.scipy
        python38Packages.matplotlib
        requests
    ];
}

Conclusión

Nix es un gestor de paquetes muy potente, ofrece mejoras respecto a apt, yum, dnf, etc y se acerca mucho a Docker y otros sistemas modernos. Es puramente funcional, que se refleja en una curva de aprendizaje inicial más elevada. A priori no parece que ninguna gran distro que quiera pasarse a Nix, pero se puede instalar de forma paralela en tu Linux o Mac. Si quieres ir un paso más allá, NixOS utiliza el lenguaje Nix para todavía más partes de la administración del sistema y existen proyectos como home-manager que te permiten configurar entornos completos en Nix de una forma más extensa que solo decidir los paquetes que va a disponer.

Tags: nixos programacion linux tutorial nix debian python