지피지기 백전백퇴

Nix flake에서 Python 환경 관리하기


목표

Node와 Python으로 짠 스크립트가 어지럽게 널려있는 환경에서 두세번 스크립트 실행을 실패하면서 pip install ... 혹은 npm install ...을 하다보니, 왠지 그냥 nix run .#asdf args...로 스크립트를 실행할 수 있으면 조금이나마 편하지 않을까…? 라는 생각을 하게 되었다. 일단 node 스크립트는 잠깐 제쳐두고, python 스크립트를 바로 실행할 수 있는 flake 환경을 만들어보았다.

문제 1 - 스크립트의 의존성 관리

그냥 requirements.txt 파일로 관리하던 의존성을 어딘가 nix가 알아먹을 수 있는 포맷으로 옮겨야 하는데, 이거 하자고 flake.nix에 또다시 의존성 목록을 쓰는 건… 배보다 배꼽이 크다는 느낌이다. 절충해서 의존성 목록을 pyproject.toml 파일에 넣고, 해당 파일을 읽어서 의존성을 관리할 수 있는 pyproject-nix flake를 사용해보기로 했다.

문제 2 - nixpkgs에 없는 의존성

다만 모든 pypi 패키지를 사용할 수 있는 게 아니라, nixpkgs에 등록된 파이썬 패키지만 쓸 수 있다는 문제가 있었다. 몇몇 아주 오래되고 인기 없는 패키지의 경우 nixpkgs에 없어서, 내가 직접 등록하던가 아니면 로컬에서 바로 패키지를 빌드해서 써야 할 필요가 있었다.

결론

되면 한다.

{
  description = "A flake for python package";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
    flake-utils.url = "github:numtide/flake-utils";
    pyproject-nix.url = "github:pyproject-nix/pyproject.nix";
    pyproject-nix.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs =
    {
      self,
      nixpkgs,
      flake-utils,
      pyproject-nix,
    }:
    flake-utils.lib.eachDefaultSystem (
      system:
      let
        pkgs = nixpkgs.legacyPackages.${system};

        # Python 패키지 googledrivedownloader를 따로 빌드한다.
        googledrivedownloader = pkgs.python3Packages.buildPythonPackage rec {
          pname = "googledrivedownloader";
          version = "1.1.0";
          format = "pyproject"; # 이 패키지는 setup.py 대신 pyproject.toml을 사용한다.

          propagatedBuildInputs = with pkgs.python3Packages; [ # 이 패키지가 의존하는 패키지들
            hatchling
            requests
          ];

          src = pkgs.python3Packages.fetchPypi {
            inherit version;
            inherit pname;
            hash = "sha256-EAkg606QabSLwvBEXt8uvFKBS8jUEDhdCyyQLK1kzJs="; # 해시는 빌드 과정에서 알려준다.
          };
        };

        # Python 패키지 pimento를 따로 빌드한다.
        pimento = pkgs.python3Packages.buildPythonPackage rec {
          pname = "pimento";
          version = "0.5.1";
          # 이번엔 setup.py를 사용하기 때문에 별도의 옵션이 없다.

          src = pkgs.python3Packages.fetchPypi {
            inherit version;
            inherit pname;
            hash = "sha256-psWe9vYH+HzxNjKRpXVGz8uxwbiwUahWIyiDnvxMciY=";
          };
        };

        myPython =
          let
            packageOverrides = self: super: {
              # 새로운 패키지들을 packageOverrides를 통해 추가한다.
              inherit googledrivedownloader;
              inherit pimento;
            };
          in
          pkgs.python3.override {
            inherit packageOverrides;
            self = myPython;
          };

        # pyproject-nix를 사용해서 pyproject.toml을 분석한다.
        project = pyproject-nix.lib.project.loadPyproject {
          projectRoot = ./.;
        };
      in
      {
        devShells.default =
          let
            arg = project.renderers.withPackages { python = myPython; }; # pyproject.toml에서 요구하는 의존성 목록을 제공한다.
            pythonEnv = myPython.withPackages arg; # 해당 의존성 목록을 제공하는 파이썬 환경을 만든다.
          in
          pkgs.mkShell {
            packages = [
              pythonEnv # 해당 파이썬 환경을 바로 사용할 수 있는 shell을 만든다.
            ];
          };

        # flake.nix 포맷용 스크립트
        formatter = pkgs.writeScriptBin "format" ''
          #!${pkgs.bash}/bin/bash
          ${pkgs.nixfmt-rfc-style}/bin/nixfmt flake.nix
        '';
      }
    );
}