Grpc-tools in node programs tries to access internet while building

Ok, so I finally understand what’s happening: grpc-tools is originally a C++ program, but it was packaged in nodejs with a trivial program that downloads pre-built C++ binaries. Nix does not like this, so they packaged grpc-tools themself starting from the C++ code directly. So my first trick was to remove grpc-tools from the package.json and include grpc-tools in the native build inputs. The second trick was to realize that nix forgot to also create the binary packages created by npm (these are trivial wrappers grpc-node/packages/grpc-tools/bin/protoc.js at 179dbfaeccc19bce786788a6b8e986990ab51329 · grpc/grpc-node · GitHub), so I manually added them like:

diff --git a/pkgs/by-name/gr/grpc-tools/package.nix b/pkgs/by-name/gr/grpc-tools/package.nix
index 5bf09b7f0b94..221727e5296a 100644
--- a/pkgs/by-name/gr/grpc-tools/package.nix
+++ b/pkgs/by-name/gr/grpc-tools/package.nix
@@ -25,6 +25,17 @@ stdenv.mkDerivation rec {
   installPhase = ''
     install -Dm755 -t $out/bin grpc_node_plugin
     install -Dm755 -t $out/bin deps/protobuf/protoc
+    # The node script creates two additional binaries that just forward their inputs to the above programs,
+    # but it seems unnecessary to install node etc just for this. So let's fake it using regular bash.
+    # https://github.com/grpc/grpc-node/blob/179dbfaeccc19bce786788a6b8e986990ab51329/packages/grpc-tools/package.json#L19-L21
+    cat >$out/bin/grpc_tools_node_protoc <<EOL
+    #/usr/bin/env bash
+    $out/bin/protoc --plugin=protoc-gen-grpc=$out/bin/grpc_node_plugin "\$@"
+    EOL
+    chmod +x $out/bin/grpc_tools_node_protoc
+    # grpc_tools_node_protoc_plugin seems to literally be the same as grpc_node_plugin
+    # except with a node wrapper
+    ln -s $out/bin/grpc_node_plugin $out/bin/grpc_tools_node_protoc_plugin
   '';

So I finally managed to get a fully working robotframework-browser packaged purely in nix:

{
  lib,
  stdenv,
  buildPythonPackage,
  fetchPypi,
  pythonOlder,
  robotframework,
  robotframework-pythonlibcore,
  robotframework-assertion-engine,
  grpcio,
  protobuf,
  overrides,
  click,
  seedir,
  wrapt,
  npmHooks,
  nodejs_latest,
  fetchNpmDeps,
  fetchFromGitHub,
  node-pre-gyp,
  grpc-tools,
  grpcio-tools, # needed to generate --grpc_python_out + imported
  # For inv build
  invoke, # cmake-like for python
  mypy-protobuf,
  robotstatuschecker,
  pytest,
  beautifulsoup4,
  # To provide the binaries
  playwright-driver,
}:
let
  version = "19.6.0";
  pname = "robotframework-browser";
  src_orig = fetchFromGitHub {
    owner = "MarketSquare";
    repo = "robotframework-browser";
    rev = "v${version}";
    hash = "sha256-kOifD+Fa1WCYbNZSRX89FlZio9oJ7Rh6aXQGVmVPXRI=";
  };
  src = stdenv.mkDerivation {
    inherit pname;
    inherit version;
    src = src_orig;
    patches = [
      # Cf details on the above identical patch
      ./pin_playwright_to_nix_playwright_driver_version.diff
    ];
    buildPhase = "";
    installPhase = ''
      mkdir -p $out
      cp -Ra . $out
      cat $out/package.json
    '';
  };
in
buildPythonPackage rec {
  inherit pname;
  inherit version;

  disabled = pythonOlder "3.9";

  nativeBuildInputs = [
    #importNpmLock.npmConfigHook
    npmHooks.npmConfigHook
    # npmHooks.npmInstallHook
    nodejs_latest
    node-pre-gyp
    grpc-tools
    # For inv build
    invoke # cmake-like for python
    grpcio-tools # needed to generate --grpc_python_out
    mypy-protobuf
    robotstatuschecker
    pytest
    beautifulsoup4
  ];

  npmDeps = fetchNpmDeps {
    inherit src;
    hash = "sha256-5EYZ5t24aPWM12nGbBBu1nwb5QS27eWPCXT76JMIpoA="; # after patch
  };

  inherit src;

  # We fake a run of rfbrowser init
  # + grpc_tools_node_protoc already includes the plugin and is not runnable via npm
  # since nix directly builds it from C++ sources to avoid to use pre-build binaries
  # downloaded by npm
  patchPhase = ''
    runHook prePatch
    substituteInPlace ./Browser/playwright.py \
      --replace-fail '(installation_dir / "node_modules").is_dir()' 'True'
    substituteInPlace ./tasks.py \
      --replace-fail 'c.run("pip install -U pip")' 'return' \
      --replace-fail 'npm run grpc_tools_node_protoc' 'grpc_tools_node_protoc' \
      --replace-fail ' -- ' ' '
    substituteInPlace ./package.json \
      --replace-fail '"grpc-tools": "^1.13.0",' ' '
    sed -i '/--plugin=protoc-gen-grpc/d' ./tasks.py
    runHook postPatch
  '';

  dependencies = [
    robotframework
    robotframework-pythonlibcore
    robotframework-assertion-engine
    grpcio
    grpcio-tools
    protobuf
    overrides
    click
    seedir
    wrapt
  ];

  preBuild = ''
    inv build -d -e
  '';

  # makeWrapperArgs can't be used (not a script here but a library)
  postInstall = ''
    cp -r node_modules $out/lib
    cat >> $out/lib/python3.13/site-packages/Browser/__init__.py <<EOF
    import os
    if not "PLAYWRIGHT_BROWSERS_PATH" in os.environ:
        os.environ["PLAYWRIGHT_BROWSERS_PATH"] = "${playwright-driver.browsers}"
    if not "PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS" in os.environ:
        os.environ["PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS"] = "true"
    EOF
  '';

  meta = with lib; {
    description = "Robot Framework Browser library powered by Playwright. ";
    homepage = "https://robotframework-browser.org/";
    license = licenses.asl20;
    maintainers = with maintainers; [ tobiasBora ];
  };

}

Next step is to see if I can get a cleaner solution to synchronize the version of pkgs.playwright-driver.browsers and the playwright version used in robotframework… and do a PR eventually!