How to build ruby with Nix Flakes?

I’m trying to make a ruby (jekyll) website and using nix to build it. This is my first time using nix flakes, and one of the first times I write nix stuff myself (have used existing ones, and tweaked here and there, but only wrote very very basic ones from scratch myself).

So I created a basic flake.nix with a dev shell that has buildInputs = [ ruby ];, and used that to create my gemfile and stuff. I can use nix develop and then bundle exec jekyll build and that works.

My file structure looks like this:

  • _posts (folder)
  • _config.yaml
  • flake.lock
  • flake.nix
  • Gemfile
  • Gemfile.lock

And my flake.nix currently looks like this:

{
  description = "My Jekyll Website";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-22.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let 
        pkgs = nixpkgs.legacyPackages.${system};
      in with pkgs;
      {
        devShells.default =
          mkShell {
            buildInputs = [ ruby ];
          };

        packages.default =
          stdenv.mkDerivation {
            name = "website";
            src = ./.;
            buildInputs = [ ruby ];
            buildPhase = "echo start && echo `ls` && bundle exec jekyll build";
          };
      }
    );
}

When I run nix.build I get the following output:

❯ nix build
error: builder for '/nix/store/813fpg5lf8d6mh0fjbg95y56gk64xicg-Cooking.drv' failed with exit code 10;
       last 10 log lines:
       > source root is hjaz58fg073by6fihlg68kn9r375jx78-source
       > patching sources
       > configuring
       > no configure script, doing nothing
       > building
       > start
       > flake.lock flake.nix
       > `/homeless-shelter` is not a directory.
       > Bundler will use `/build/bundler20220814-25-3zfowd25' as your home directory temporarily.
       > Could not locate Gemfile or .bundle/ directory
       For full logs, run 'nix log /nix/store/813fpg5lf8d6mh0fjbg95y56gk64xicg-Cooking.drv'.

The full log is:

❯ nix log /nix/store/813fpg5lf8d6mh0fjbg95y56gk64xicg-Cooking.drv
@nix { "action": "setPhase", "phase": "unpackPhase" }
unpacking sources
unpacking source archive /nix/store/v2y1wkshxjr41iz3wh4k78k2bkc1l086-hjaz58fg073by6fihlg68kn9r375jx78-source
source root is hjaz58fg073by6fihlg68kn9r375jx78-source
@nix { "action": "setPhase", "phase": "patchPhase" }
patching sources
@nix { "action": "setPhase", "phase": "configurePhase" }
configuring
no configure script, doing nothing
@nix { "action": "setPhase", "phase": "buildPhase" }
building
start
flake.lock flake.nix
`/homeless-shelter` is not a directory.
Bundler will use `/build/bundler20220814-25-3zfowd25' as your home directory temporarily.
Could not locate Gemfile or .bundle/ directory

So somehow it can see the two flake files, but can’t find any of the other source files.

What am I missing? Or what am I doing wrong?

Note: I found something about a gemset.nix file, but I wanted to first figure out this way that feels it should be pretty straight forward. As I’m only just learning to make nix stuff myself.

Just checking you’ve at least git added your files? Remember that Nix won’t see them unless they’re known to Git.

I did not, I did not realise nix needed that :sweat_smile:

When I do that, my echo does show all the files! So looks like that was the problem.

So then I get an error stating I need to add a bundle install step. So I did that by updating my package:

        packages.default =
          stdenv.mkDerivation {
            name = "Cooking";
            src = ./.;
            buildInputs = [ ruby ];
            buildPhase = ''
              echo start
              echo `ls`
              bundle install
              bundle exec jekyll build
            '';
          };

Which then results in the following error:

❯ nix build
error: builder for '/nix/store/8nyzwbnn8fxqwv3489418wlb516x09kr-Cooking.drv' failed with exit code 23;
       last 10 log lines:
       > no configure script, doing nothing
       > building
       > start
       > 404.html Gemfile Gemfile.lock README.md _config.yml _posts about.markdown flake.lock flake.nix index.markdown
       > `/homeless-shelter` is not a directory.
       > Bundler will use `/build/bundler20220814-25-1ks5f6u25' as your home directory temporarily.
       > Warning: the running version of Bundler (2.1.4) is older than the version that created the lockfile (2.3.19). We suggest you to upgrade to the version that created the lockfile by running `gem install bundler:2.3.19`.
       > There was an error while trying to write to
       > `/homeless-shelter/.local/share/gem/ruby/2.7.0`. It is likely that you need to
       > grant write permissions for that path.
       For full logs, run 'nix log /nix/store/8nyzwbnn8fxqwv3489418wlb516x09kr-Cooking.drv'.

It’s weird that the version of bundler is different in my shell and in the package… And looks like I maybe have to change the location ruby saves gems or something.

Or at this point I maybe have bumped into the limitations of not using the gemset.nix. So maybe I’ll look into that.

Yeah, that looks like some part of Ruby is trying to write to ~/.local/share/gem/ruby/2.7.0. You can probably hack around this by using mktemp to create a temp directory, and then setting $HOME to it - but I don’t know anything about how Ruby works, so this may or may not be too much of a hack.

Ok, so I managed to make some progress.

I updated my flakes.nix to use gemset.nix:

{
  description = "Pablo's Cooking Website";

  inputs = {
    nixpkgs.url = "nixpkgs/nixos-22.05";
    flake-utils.url = "github:numtide/flake-utils";
  };

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

        gems = pkgs.bundlerEnv {
          name = "gems";
          ruby = pkgs.ruby;
          gemfile = ./Gemfile;
          lockfile = ./Gemfile.lock;
          gemset = ./gemset.nix;
        };
      in with pkgs;
      {
        devShells.default =
          mkShell {
            buildInputs = [ gems bundix ];
          };

        packages.default =
          stdenv.mkDerivation {
            name = "Cooking";
            src = self;
            buildInputs = [ gems ];
            buildPhase = ''
              ${gems}/bin/jekyll build
            '';
            installPhase = ''
              mkdir -p $out
              cp -r _site $out/_site
            '';
          };
      }
    );
}

Now when I do nix build it creates a result folder that contains the _site output I expect from Jekyll.

So I think that works.

Still trying to google if I can change it so that the output it _site directly, rather than result/_site, but that’s not a major thing if not possible.

You can’t really change the output directory, because result is actually a symlink to a directory in the Nix store (/nix/store), which you can see if you run readlink result. Storing all build output in the Nix store is pretty essential to how Nix works and to the “contract” that it provides, so I’d say your best option would be to copy _site from result into the root.

Aha that makes sense.
It’s indeed no biggie that it’s in result/_site, so all good :smiley:
Thanks for the explanation!