mkYarnPackage: ENOENT error from dependency during nix build, but not manual build in dev shell

Hi. I have a couple questions about using mkYarnPackage. I’m trying to nixify
this vscode extension since I had to patch it, and am using a flake.nix. I had to yarn add vsce, which was the only change to packages.json.

From a dev shell with only the yarn dependency, I can run yarn --offline compile and
yarn --offline vsce package --yarn -o command-server.vsix. This builds fine, and I can install the .vsix file into vscode.

The problems come when I try to nix build .. The first problem is I get a ENOENT from
some yazl dependency:

$ yarn run compile
warning You don't appear to have an internet connection. Try the --offline flag to use the cache for registry queries.
$ tsc -p ./
       >  WARNING  Using '*' activation is usually a bad idea as it impacts performance.
       > More info: https://code.visualstudio.com/api/references/activation-events#Start-up
Do you want to continue? [y/N] y
       > node:events:492
       >       throw er; // Unhandled 'error' event
       >       ^
       >
       > Error: ENOENT: no such file or directory, stat '/build/5sg0ck2mfpyccf2pzv1jr133lk15ravx-source/deps/command-server/command-server'
       > Emitted 'error' event on ZipFile instance at:
       >     at /nix/store/ys3bczil4nlvaa7n5hyshrivafs46k84-command-server-modules-0.9.0/node_modules/yazl/index.js:30:26
       >     at FSReqCallback.oncomplete (node:fs:199:21) {
       >   errno: -2,
       >   code: 'ENOENT',
       >   syscall: 'stat',
       >   path: '/build/5sg0ck2mfpyccf2pzv1jr133lk15ravx-source/deps/command-server/command-server'
       > }
       >
       > Node.js v20.10.0

I confirmed that /nix/store/5sg0ck2mfpyccf2pzv1jr133lk15ravx-source doesn’t contain this file. So the easy solution is to create a file deps/command-server/command-server in the project repo, git add it, and then rebuild, so that it gets populated into the build source folder. This builds (although the extension doesn’t work), but I don’t understand why this is happening only during nix build, so am curious if anyone has any insight.

The second issue is that for my buildPhase I had called yarn --offline vsce package --yarn -o command-server.vsix and in my installPhase I had:

mkdir $out
mv command-server.vsix $out

However, this always complains that command-server.vsix doesn’t exist. If I use $PWD/command-server.vsix the error indicates it’s operating from the same /build/... folder above, but if I check it seems the .vsix is not written there.

The only way I’ve been able to solve this is to use echo y | yarn --offline vsce package --yarn -o $out/$pname.vsix from the installPhase, where I’m explicitly telling it to write to $out, in which case I see the file in result.

I’m curious what I’m missing and what the correct way to approach this second problem is. It seems fine to just write directly to $out, but I expected the mv to work and saw other examples online that seemed to use that approach, so would like to know why it fails.

My flake is quite simple:

{
  description = "A Nix-flake-based development environment for command-server";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

  outputs = { self, nixpkgs }:
    let
      supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
      forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
        pkgs = import nixpkgs { inherit system; config.allowUnfree = true; };
      });
    in
    {
      packages = forEachSupportedSystem
        ({ pkgs, ... }:
          let
            attrs = with builtins; fromJSON (readFile ./package.json);
          in
          {
            default = pkgs.mkYarnPackage {
              pname = with attrs; name;
              src = ./.;
              packageJson = ./package.json;
              yarnLock = ./yarn.lock;

              buildPhase = ''
                # yarn tries to create a .yarn file in $HOME. There's probably a
                # better way to fix this but setting HOME to TMPDIR works for now.
                export HOME="$TMPDIR"
                yarn tsc -p ./
              '';

              installPhase = ''
                mkdir $out
                echo y | yarn --offline vsce package --yarn -o $out/$pname.vsix
              '';

              distPhase = "true";

            };
          });

      devShells = forEachSupportedSystem
        ({ pkgs }: {
          default = pkgs.mkShell
            {
              packages = with pkgs;
                [ yarn ];
            };
        });
    };
}

I solved this eventually, so I will summarize for other new nix users who may need to mess around with mkYarnPackage. The main thing I knew was that the error is coming from something called yazl, which is related to zip files, so possibly trying to package the .vsix file (which is just a zip).

       > Error: ENOENT: no such file or directory, stat '/build/5sg0ck2mfpyccf2pzv1jr133lk15ravx-source/deps/command-server/command-server'
       > Emitted 'error' event on ZipFile instance at:

After enabling the pkgs.breakpointHook as a buildInput (which I only just learned about), I entered the build sandbox with cntr and searched for references to command-server using:

find . -iname "*command-server*" 2>/dev/null

This led me to see the following the dep folder, where we see there is some invalid symlink. This link corresponds to the file that was being complained about above.

[nixbld@localhost:.]$ ls -al build/9jpv38f3v36hqnm82w99aginlrsynl6p-source/deps/command-server/
total 188
drwxr-xr-x 9 nixbld nixbld  4096  1月 18 04:00 .
drwxr-xr-x 3 nixbld nixbld  4096  1月 18 04:00 ..
drwxr-xr-x 3 nixbld nixbld  4096  1月 18 04:00 .cache
-rw-r--r-- 1 nixbld nixbld   584  1月  1  1970 CHANGELOG.md
lrwxrwxrwx 1 nixbld nixbld    19  1月 18 04:00 command-server -> deps/command-server
-rw-r--r-- 1 nixbld nixbld     0  1月 18 04:00 command-server.vsix
-rw-r--r-- 1 nixbld nixbld    10  1月  1  1970 .envrc
-rw-r--r-- 1 nixbld nixbld   540  1月  1  1970 .eslintrc.json
-rw-r--r-- 1 nixbld nixbld   569  1月  1  1970 flake.lock
-rw-r--r-- 1 nixbld nixbld  2300  1月  1  1970 flake.nix
drwxr-xr-x 2 nixbld nixbld  4096  1月  1  1970 .github
-rw-r--r-- 1 nixbld nixbld    43  1月  1  1970 .gitignore
-rw-r--r-- 1 nixbld nixbld  1076  1月  1  1970 LICENSE
lrwxrwxrwx 1 nixbld nixbld   105  1月 18 04:00 node_modules -> /nix/store/ys3bczil4nlvaa7n5hyshrivafs46k84-command-server-modules-0.9.0/deps/command-server/node_modules
drwxr-xr-x 3 nixbld nixbld  4096  1月 18 04:00 .npm
drwxr-xr-x 3 nixbld nixbld  4096  1月 18 04:00 out
-rwxr-xr-x 1 nixbld nixbld  3652  1月  1  1970 package.json
-rw-r--r-- 1 nixbld nixbld  3760  1月  1  1970 README.md
drwxr-xr-x 3 nixbld nixbld  4096  1月  1  1970 src
-rw-r--r-- 1 nixbld nixbld   584  1月  1  1970 tsconfig.json
-rw-r--r-- 1 nixbld nixbld  2616  1月  1  1970 vsc-extension-quickstart.md
drwxr-xr-x 2 nixbld nixbld  4096  1月  1  1970 .vscode
-rw-r--r-- 1 nixbld nixbld   145  1月  1  1970 .vscodeignore
drwxr-xr-x 3 nixbld nixbld  4096  1月 18 04:00 .yarn
-rw-r--r-- 1 nixbld nixbld 95815  1月  1  1970 yarn.lock
-rw-r--r-- 1 nixbld nixbld    21  1月  1  1970 .yarnrc

There is no deps subfolder above, so the link is invalid. Simply by removing the file in my buildPhase hook, I could prevent the error. I suspect there might be something in
mkYarnPackage’s configurePhase that is doing this automatically as I can see a bunch of special function setting up dependency links, but I need to dig more into what exactly it’s doing to know for sure.

Although that fixed the build problem, that’s not actually enough to get the extension to work. It builds, but it doesn’t actually do anything when loaded. To diagnose this I took a real .vsix file that I had built manually, and compared the contents to one that I built with the flake. It was clear that the folder that the packaging was occurring from was incorrect, as there was no out or node_modules folders, which were in the working extension. Further poking around the build sandbox made me realize that the deps/command-server folder has most of what we want, including out. The main problem is that the node_modules file in that folder is a symlink, and moreover the linked path doesn’t seem to be the one we actually want, as it only contains a @types folder. More digging around made me realize where to find the node_modules folder that actually contains the dependencies that we want.

So the final solution was to something like this:

buildPhase = ''
  # yarn tries to create a .yarn file in $HOME. There's probably a
  # better way to fix this but setting HOME to cwd works for now.
  export HOME="."
  yarn --offline run compile
  # Remove strange non-existent symlink created during packaging
  rm ./deps/command-server/command-server
  rm ./deps/command-server/node_modules
  cp -R ./node_modules ./deps/command-server
  pushd ./deps/command-server
  echo y | yarn --offline vsce package --yarn -o ./$pname.vsix
  popd
'';

I think this is a interesting example of how powerful knowing about using cntr and pkgs.breakpointHook is, as otherwise I think I would have been totally lost.

I did notice some other people do the delete and copy dance for node_modules, such as
nixpkgs/pkgs/applications/office/micropad/default.nix, so I’m guessing maybe what I had to figure out above is normal process for packaging yarn-related things?

I hope that helps someone!

3 Likes