Skip to content

Ambientes de Desenvolvimento no NixOS

A reprodutibilidade do NixOS o torna ideal para a construção de ambientes de desenvolvimento. No entanto, se você está acostumado com outras distribuições (distros), pode encontrar problemas porque o NixOS tem sua própria lógica. Explicaremos brevemente isso a seguir.

Nas seções seguintes, introduziremos como o ambiente de desenvolvimento funciona no NixOS.

Criando um Ambiente de Shell Personalizado com nix shell

A maneira mais simples de criar um ambiente de desenvolvimento é usar o nix shell. O nix shell criará um ambiente de shell com o pacote Nix especificado instalado.

Aqui está um exemplo:

shell
# hello não está disponível
 hello
hello: command not found

# Entrar em um ambiente com os pacotes 'hello' e `cowsay`
 nix shell nixpkgs#hello nixpkgs#cowsay

# hello agora está disponível
 hello
Hello, world!

# cowsay também está disponível
 cowsay "Hello, world!"
 _______
< hello >
 -------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

O nix shell é muito útil quando você apenas quer testar alguns pacotes ou criar rapidamente um ambiente limpo.

Criando um Ambiente de Desenvolvimento

O nix shell é simples e fácil de usar, mas não é muito flexível. Para um ambiente de desenvolvimento mais complexo, precisamos usar pkgs.mkShell e nix develop.

Podemos criar um ambiente de desenvolvimento usando pkgs.mkShell { ... } e abrir um shell Bash interativo desse ambiente de desenvolvimento usando nix develop.

Para ver como o pkgs.mkShell funciona, vamos dar uma olhada em seu código-fonte.

nix
{ lib, stdenv, buildEnv }:

# Uma espécie especial de derivação que se destina apenas a ser consumida pelo
# nix-shell.
{ name ? "nix-shell"
, # uma lista de pacotes a serem adicionados ao ambiente do shell
  packages ? [ ]
, # propaga todas as entradas das derivações fornecidas
  inputsFrom ? [ ]
, buildInputs ? [ ]
, nativeBuildInputs ? [ ]
, propagatedBuildInputs ? [ ]
, propagatedNativeBuildInputs ? [ ]
, ...
}@attrs:
let
  mergeInputs = name:
    (attrs.${name} or [ ]) ++
    (lib.subtractLists inputsFrom (lib.flatten (lib.catAttrs name inputsFrom)));

  rest = builtins.removeAttrs attrs [
    "name"
    "packages"
    "inputsFrom"
    "buildInputs"
    "nativeBuildInputs"
    "propagatedBuildInputs"
    "propagatedNativeBuildInputs"
    "shellHook"
  ];
in

stdenv.mkDerivation ({
  inherit name;

  buildInputs = mergeInputs "buildInputs";
  nativeBuildInputs = packages ++ (mergeInputs "nativeBuildInputs");
  propagatedBuildInputs = mergeInputs "propagatedBuildInputs";
  propagatedNativeBuildInputs = mergeInputs "propagatedNativeBuildInputs";

  shellHook = lib.concatStringsSep "\n" (lib.catAttrs "shellHook"
    (lib.reverseList inputsFrom ++ [ attrs ]));

  phases = [ "buildPhase" ];

  # ......

  # quando a construção distribuída está habilitada, prefira construir localmente
  preferLocalBuild = true;
} // rest)

pkgs.mkShell { ... } é uma derivação (derivation) especial (pacote Nix). Seu name, buildInputs e outros parâmetros são personalizáveis, e shellHook é um parâmetro especial que será executado quando nix develop entrar no ambiente.

Aqui está um flake.nix que define um ambiente de desenvolvimento com o Node.js 18 instalado:

nix
{
  description = "A Nix-flake-based Node.js development environment";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
  };

  outputs = { self , nixpkgs ,... }: let
    # o sistema deve corresponder ao sistema em que você está rodando
    # system = "x86_64-linux";
    system = "x86_64-darwin";
  in {
    devShells."${system}".default = let
      pkgs = import nixpkgs {
        inherit system;
      };
    in pkgs.mkShell {
      # criar um ambiente com nodejs_18, pnpm e yarn
      packages = with pkgs; [
        nodejs_18
        nodePackages.pnpm
        (yarn.override { nodejs = nodejs_18; })
      ];

      shellHook = ''
        echo "node `node --version`"
      '';
    };
  };
}

Crie uma pasta vazia, salve a configuração acima como flake.nix e, em seguida, execute nix develop (ou, mais precisamente, você pode usar nix develop .#default), A versão atual do nodejs será exibida, e agora você pode usar node, pnpm e yarn sem problemas.

Usando zsh/fish/... em vez de bash

O pkgs.mkShell usa bash por padrão, mas você também pode usar zsh ou fish adicionando exec <your-shell> no shellHook.

Aqui está um exemplo:

nix
{
  description = "A Nix-flake-based Node.js development environment";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
  };

  outputs = { self , nixpkgs ,... }: let
    # o sistema deve corresponder ao sistema em que você está rodando
    # system = "x86_64-linux";
    system = "x86_64-darwin";
  in {
    devShells."${system}".default = let
      pkgs = import nixpkgs {
        inherit system;
      };
    in pkgs.mkShell {
      # criar um ambiente com nodejs_18, pnpm e yarn
      packages = with pkgs; [
        nodejs_18
        nodePackages.pnpm
        (yarn.override { nodejs = nodejs_18; })
        nushell
      ];

      shellHook = ''
        echo "node `node --version`"
        exec nu
      '';
    };
  };
}

Com a configuração acima, o nix develop entrará no ambiente REPL do nushell.

Criando um Ambiente de Desenvolvimento com pkgs.runCommand

A derivação criada pelo pkgs.mkShell não pode ser usada diretamente, mas deve ser acessada via nix develop.

É realmente possível criar um wrapper de shell contendo os pacotes necessários via pkgs.stdenv.mkDerivation, que pode então ser executado diretamente no ambiente.

Usar mkDerivation diretamente é um pouco complicado, e o Nixpkgs fornece algumas funções mais simples para nos ajudar a criar esses wrappers, como pkgs.runCommand.

Exemplo:

nix
{
  description = "A Nix-flake-based Node.js development environment";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
  };

  outputs = { self , nixpkgs ,... }: let
    # o sistema deve corresponder ao sistema em que você está rodando
    # system = "x86_64-linux";
    system = "x86_64-darwin";
  in {
    packages."${system}".dev = let
      pkgs = import nixpkgs {
        inherit system;
      };
      packages = with pkgs; [
          nodejs_20
          nodePackages.pnpm
          nushell
      ];
    in pkgs.runCommand "dev-shell" {
      # Dependências que devem existir no ambiente de tempo de execução
      buildInputs = packages;
      # Dependências que só devem existir no ambiente de construção
      nativeBuildInputs = [ pkgs.makeWrapper ];
    } ''
      mkdir -p $out/bin/
      ln -s ${pkgs.nushell}/bin/nu $out/bin/dev-shell
      wrapProgram $out/bin/dev-shell --prefix PATH : ${pkgs.lib.makeBinPath packages}
    '';
  };
}

Em seguida, execute nix run .#dev ou nix shell .#dev --command 'dev-shell', Você entrará em uma sessão do nushell, onde poderá usar os comandos node e pnpm normalmente, e a versão do node será a 20.

O wrapper gerado desta forma é um arquivo executável, que na verdade não depende do comando nix run ou nix shell.

Por exemplo, podemos instalar diretamente este wrapper através do environment.systemPackages do NixOS e, em seguida, executá-lo diretamente:

nix
{pkgs, lib, ...}:{

  environment.systemPackages = [
    # Instalar o wrapper no sistema
    (let
      packages = with pkgs; [
          nodejs_20
          nodePackages.pnpm
          nushell
      ];
    in pkgs.runCommand "dev-shell" {
      # Dependências que devem existir no ambiente de tempo de execução
      buildInputs = packages;
      # Dependências que só devem existir no ambiente de construção
      nativeBuildInputs = [ pkgs.makeWrapper ];
    } ''
      mkdir -p $out/bin/
      ln -s ${pkgs.nushell}/bin/nu $out/bin/dev-shell
      wrapProgram $out/bin/dev-shell --prefix PATH : ${pkgs.lib.makeBinPath packages}
    '')
  ];
}

Adicione a configuração acima a qualquer Módulo NixOS, em seguida, implante-a com sudo nixos-rebuild switch, e você poderá entrar no ambiente de desenvolvimento diretamente com o comando dev-shell, que é a característica especial de pkgs.runCommand em comparação com pkgs.mkShell.

Código-fonte relacionado:

Entrar no Ambiente de Construção de Qualquer Pacote Nix

Agora vamos dar uma olhada em nix develop. Primeiro, leia a documentação de ajuda exibida por nix develop --help:

Name
    nix develop - run a bash shell that provides the build environment of a derivation

Synopsis
    nix develop [option...] installable
# ......

Ele nos diz que nix develop aceita um parâmetro installable, o que significa que podemos entrar no ambiente de desenvolvimento de qualquer pacote Nix instalável através dele, não apenas o ambiente criado por pkgs.mkShell.

Por padrão, nix developtentará usar os seguintes atributos nas saídas do flake:

  • devShells.<system>.default
  • packages.<system>.default

Se usarmos nix develop /path/to/flake#<name> para especificar o endereço do pacote flake e o nome da saída do flake, então nix develop tentará os seguintes atributos nas saídas do flake:

  • devShells.<system>.<name>
  • packages.<system>.<name>
  • legacyPackages.<system>.<name>

Agora vamos testá-lo. Primeiro, teste para confirmar que não temos c++, g++ e outros comandos relacionados à compilação no ambiente atual:

shell
ryan in 🌐 aquamarine in ~
 c++
c++: command not found

ryan in 🌐 aquamarine in ~
 g++
g++: command not found

Em seguida, use nix develop para entrar no ambiente de construção do pacote hello no nixpkgs:

shell
# logar no ambiente de construção do pacote `hello`
ryan in 🌐 aquamarine in ~
 nix develop nixpkgs#hello

ryan in 🌐 aquamarine in ~ via ❄️  impure (hello-2.12.1-env)
 env | grep CXX
CXX=g++

ryan in 🌐 aquamarine in ~ via ❄️  impure (hello-2.12.1-env)
 c++ --version
g++ (GCC) 12.3.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

ryan in 🌐 aquamarine in ~ via ❄️  impure (hello-2.12.1-env)
 g++ --version
g++ (GCC) 12.3.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Podemos ver que a variável de ambiente CXX foi definida e que os comandos c++, g++ e outros agora podem ser usados normalmente.

Além disso, também podemos chamar cada fase de construção do pacote hello normalmente:

A ordem de execução padrão de todas as fases de construção de um pacote Nix é: $prePhases unpackPhase patchPhase $preConfigurePhases configurePhase $preBuildPhases buildPhase checkPhase $preInstallPhases installPhase fixupPhase installCheckPhase $preDistPhases distPhase $postPhases

shell
# descompactar o código-fonte
ryan in 🌐 aquamarine in /tmp/xxx via ❄️  impure (hello-2.12.1-env)
 unpackPhase
unpacking source archive /nix/store/pa10z4ngm0g83kx9mssrqzz30s84vq7k-hello-2.12.1.tar.gz
source root is hello-2.12.1
setting SOURCE_DATE_EPOCH to timestamp 1653865426 of file hello-2.12.1/ChangeLog

ryan in 🌐 aquamarine in /tmp/xxx via ❄️  impure (hello-2.12.1-env)
 ls
hello-2.12.1

ryan in 🌐 aquamarine in /tmp/xxx via ❄️  impure (hello-2.12.1-env)
 cd hello-2.12.1/

# gerar Makefile
ryan in 🌐 aquamarine in /tmp/xxx/hello-2.12.1 via ❄️  impure (hello-2.12.1-env)
 configurePhase
configure flags: --prefix=/tmp/xxx/outputs/out --prefix=/tmp/xxx/outputs/out
checking for a BSD-compatible install... /nix/store/02dr9ymdqpkb75vf0v1z2l91z2q3izy9-coreutils-9.3/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /nix/store/02dr9ymdqpkb75vf0v1z2l91z2q3izy9-coreutils-9.3/bin/mkdir -p
checking for gawk... gawk
checking whether make sets $(MAKE)... yes
checking whether make supports nested variables... yes
checking for gcc... gcc
# ......
checking that generated files are newer than configure... done
configure: creating ./config.status
config.status: creating Makefile
config.status: creating po/Makefile.in
config.status: creating config.h
config.status: config.h is unchanged
config.status: executing depfiles commands
config.status: executing po-directories commands
config.status: creating po/POTFILES
config.status: creating po/Makefile

# construir o pacote
ryan in 🌐 aquamarine in /tmp/xxx/hello-2.12.1 via C v12.3.0-gcc via ❄️  impure (hello-2.12.1-env) took 2s
 buildPhase
build flags: SHELL=/run/current-system/sw/bin/bash
make  all-recursive
make[1]: Entering directory '/tmp/xxx/hello-2.12.1'
# ......
ranlib lib/libhello.a
gcc  -g -O2   -o hello src/hello.o  ./lib/libhello.a
make[2]: Leaving directory '/tmp/xxx/hello-2.12.1'
make[1]: Leaving directory '/tmp/xxx/hello-2.12.1'

# rodar o programa construído
ryan in 🌐 aquamarine in /tmp/xxx/hello-2.12.1 via C v12.3.0-gcc via ❄️  impure (hello-2.12.1-env)
 ./hello
Hello, world!

Esse uso é principalmente para depurar o processo de construção de um pacote Nix, ou para executar alguns comandos no ambiente de construção de um pacote Nix.

nix build

O comando nix build é usado para construir um pacote de software e cria um link simbólico (symbolic link) chamado result no diretório atual, que aponta para o resultado da construção.

Aqui está um exemplo:

bash
# Construir o pacote 'ponysay' do flake 'nixpkgs'
nix build "nixpkgs#ponysay"
# Usar o comando 'ponysay' construído
 ./result/bin/ponysay 'hey buddy!'
 ____________
< hey buddy! >
 ------------
     \
      \
       \
       ▄▄  ▄▄
    ▀▄▄▄█▄▄▄▄▄█▄▄▄
   ▀▄███▄▄██▄██▄▄██
  ▄██▄███▄▄██▄▄▄█▄██
 █▄█▄██▄█████████▄██
  ▄▄█▄█▄▄▄▄▄████████
 ▀▀▀▄█▄█▄█▄▄▄▄▄█████
    ▀▄████▄▄▄█▄█▄▄██       ▄▄▄▄▄█▄▄▄
    █▄██▄▄▄▄███▄▄▄██    ▄▄▄▄▄▄▄▄▄█▄▄
    ▀▄▄██████▄▄▄████    █████████████
       ▀▀▀▀▀█████▄▄ ▄▄▄▄▄▄▄▄▄▄██▄█▄▄▀
            ██▄███▄▄▄▄█▄▄▀  ███▄█▄▄▄█▀
            █▄██▄▄▄▄▄████   ███████▄██
            █▄███▄▄█████    ▀███▄█████▄
            ██████▀▄▄▄█▄█    █▄██▄▄█▄█▄
           ███████ ███████   ▀████▄████
           ▀▀█▄▄▄▀ ▀▀█▄▄▄▀     ▀██▄▄██▀█
  ▀▀█

Usando nix profile para gerenciar Ambientes de Desenvolvimento e Ambientes de Entretenimento

nix develop é uma ferramenta para criar e gerenciar múltiplos ambientes de usuário e alternar para diferentes ambientes quando necessário.

Ao contrário do nix develop, o nix profile gerencia o ambiente de sistema do usuário, em vez de criar um ambiente de shell temporário. Portanto, é mais compatível com IDEs como JetBrains IDE/VSCode e não terá o problema de não conseguir usar o ambiente de desenvolvimento configurado na IDE.

TODO: Isso está em andamento. Precisa ser revisado.

Outros Comandos

Existem outros comandos como nix flake init, que você pode explorar em New Nix Commands. Para informações mais detalhadas, por favor, consulte a documentação.

Referências