Overriding A Derivation Provided In A nixpkgs Module

I maintain the Drupal service module, and I’m writing some documentation for common use cases.

There are a few different ways people manage the file structure for their Drupal projects. The upstream source organizes all of the files in the root directory, whereas most people put their web-accessible code in a weboroot directory, like /web.

The issue is that the service module in nixpkgs builds a new derivation on top of the official upstream package, and some of the operations in the installPhase and postInstall hook are kind of specific to that implementation. In other words, nix fails to build user packages because of the slight difference in file structure.

It would be great to instruct folks on how to override (or hook into) those parts of the module so they can adapt it to their needs.

The module-made derivation (defined in a let/in block) is here (link):

  pkg =
    hostName: cfg:
    stdenv.mkDerivation (finalAttrs: {
      pname = "drupal-${hostName}";
      name = "drupal-${hostName}";
      src = cfg.package;

      installPhase = ''
        runHook preInstall

        mkdir -p $out
        cp -r * $out/

        runHook postInstall
      '';

      postInstall = ''
        ln -s ${cfg.filesDir} $out/share/php/${cfg.package.pname}/sites/default/files
        ln -s ${cfg.stateDir}/sites/default/settings.php $out/share/php/${cfg.package.pname}/sites/default/settings.php
        ln -s ${cfg.modulesDir} $out/share/php/${cfg.package.pname}/modules
        ln -s ${cfg.themesDir} $out/share/php/${cfg.package.pname}/themes
      '';
    });

A sample config featuring a custom Drupal package (called “sector”) is here:

{ pkgs, config, ... }:
let
  sector = pkgs.php.buildComposerProject2 (finalAttrs: {
    pname = "sector";
    version = "10.0.9-d3b730d660";


    src = builtins.fetchGit {
      url = "https://codeberg.org/abmurrow/example-sector/";
      rev = "d3b730d660d8b206a0ba3d65f823ca594ad76a19";
    };

    vendorHash = "sha256-ZotmPP9RqG+HrXvc6dH11i07FxcTcfPKkHpwq9l1MMk=";
    composerNoPlugins = false;
    composerStrictValidation = false;

    installPhase = ''
      runHook preInstall

      mkdir -p $out/share/php/sector
      cp -r * $out/share/php/sector

      runHook postInstall
    '';

    postInstall = ''
      ln -s /var/lib/drupal/sector.local/sites/default/files $out/share/php/sector/web/sites/default
     '';
  });
in {
   services.drupal = {
     enable = true;
    sites = {
      "sector.local" = {
        enable = true;
        package = sector;
      };
    };
   };

  networking.hosts = {
    "127.0.0.1" = ["localhost" "sector.local"];
  };
}

Implementing installPhase and postInstall on the user’s custom package is fine for making that package buildable, but the system chokes on this second derivation.

How do I either:

  1. (as an end user) override the module derivation
  2. (as an end user) modify or hook into this derivation’s phases and hooks (i.e. from configuration.nix), or
  3. (as a maintainer) alter this module in such a way as to allow it to be overridden by a user’s own package implementation (I have some notion on how to do this, but I wonder if there is an accepted pattern).
1 Like

This is all faintly horrifying, but I guess that’s PHP for you? I don’t know, this whole linking non-store paths into a store output thing strikes me as very unidiomatic, but maybe this is a hard-won compromise against even worse alternatives.

But anyway, to the extent of my understanding, why wouldn’t you document that the service module expects Drupal content to be in share/php/${name}, and therefore the user who writes your sector derivation should write cp -r web/* $out/share/php/sector instead of cp -r *?

I agree with you on the store and non-store linking, but it is one of those weird edge cases where the web server expects that some of its package code should be modifiable, and other parts shouldn’t. The entire web application is ultimately being run out of nix store, so, the compromise I’ve found to make it so it the application can change some parts of itself (like file uploads, cached JS, and altering the settibgs files at install time) is to link user generated content (and variable files) up to the store in the way depicted. There might be another way to do it, admittedly. I’d eventually like to make this a deployment-only tool, and remove the need for needing any variable files other than file uploads and the settings file (which Drupal insists on being writeable by the web server group).

As for the second part, the content in /web is only a portion of the Drupal code needs yo make the server work. Just doing cp -r /web … would omit some important scaffolding tools and the composer.json, which is required to install the third party code.

So, ultimately I’m trying to figure out what tools the end user needs to overcome the assumptions that the service module is making about the package its getting, whether stock or custom.

EDIT: Or, to that point, I could change the module’s assumptions and make it more flexible, or more specific to a specific kind of solution, as needed.

Hmm. In that case, maybe add a per-site setting pathToPublicRoot (or something; you probably can pick a better name than I could) that defaults to "share/php/${cfg.pname}", and use that in the postInstall phase of the internal derivation (and your state-init service)? And users can set it to be "share/php/sector/web", for example?

I do think we can probably make this linking thing a bit cleaner, but one problem at a time. :slightly_smiling_face:

1 Like

Hm… OK that’s a cool way to do it, and it starts to set up a nice way to drop the linking issue. I already provide an option that does what youre describing called stateDir

It would just need a different default.

I think this makes it pretty clear the module has to change one way or another because it is a tad too inflexible.

I’m also wondering if I can’t instruct the webserver to patch over the fact that the user uploaded files aren’t in the same place as the source code and that it needs to resolve that on behalf of the application. That also works towards dropping the symlinks.

You’ve given me a lot to think about! I’ll need to crack open nixpkgs and do some experimenting.

Yeah, I was wondering things along those lines. If the server can’t do that, then maybe you have your init service cp -rs the user’s package entirely into /var and then modify that in place as needed.

Alternatively you could do something clever with bubblewrap — I’m a big fan of bwrap --overlay (don’t act surprised you guys 'cause I wrote it), but that might be more power than is needed. Depends on how cooperative PHP is, maybe.

Just some more things to try as you experiment. Good luck!

1 Like