Search Engine Policies for mainline Firefox on NixOS

Goal and Requirements

Firefox has the functionality to define search engine preferences via policies. Commonly, this is used in commercial IT systems to centrally administrate browser settings. Mozilla decided to lock the functionality behind enterprise builds, see the source. In ESR releases, this is fine and usable - however, I want to use the regular release, which means this needs a patch.

Floorp has a patch, but I mistrust browser forks for various reasons. Most importantly, using a browser fork means I am trusting an additional dev team that does not get notified about critical security vulnerabilities ahead of disclosure. This means any patch I come up with needs to work on mainline Firefox.

Recompiling Firefox on my laptop takes a few hours, and even with zram and ccache swap barely even succeeds with my 32Gb of RAM. While I could try to get community builder access or even upstream the patch to nixpkgs to have a remote build, this is both not ideal. Community builders are meant for community projects, not my personal funky experiments. And upstreaming the patch to nixpkgs, i’d have to explain to Hexa why we should even do the patch. While Hexa is a good diligent maintainer of Firefox in nixpkgs, they are (rightfully) opposed to wild patch stacks, so i’d rather not. This means: the patch needs to be applied without a recompile by patching the policy schema file in the final package.

A success is a solution that is robust enough to survive multiple Firefox updates and rebuilds in less than 5 min.

Naive approach: patching the source (results in recompile)

nixpkgs.overlays = lib.singleton (final: prev: {
  firefox-unwrapped = (prev.firefox-unwrapped.overrideAttrs (old: {
    patches = (old.patches or []) ++ [ ../patches/firefox_search_engines.patch ];
  })).override {
    # optional: ccache support
    stdenv = prev.ccacheStdenv;
    overrideCC = stdenv: compiler: let
      env = prev.ccacheStdenv.override { inherit stdenv; };
    in prev.overrideCC env compiler;
  }
})

This probably works. And technically would be the clean solution, as we’d not be doing any unsupported operations. However, because it invalidates all caches, this will cause a recompile. If you have a private build server or extremely powerful PC, this might be the better solution for you though. The path to the patch likely needs to be adjusted, this is basically just the downloaded patch from the floorp repository linked above.

Patching the wrapper

To not recompile Firefox, this patch has to happen in a package wrapper. Firefox already has a wrapper that is being used. It relies on passthru of the firefox-unwrapped package however, which makes an intermediary wrapper quite bothersome. It is likely possible though, I will leave this as an exercise to the reader. Instead of an intermediary wrapper, I will directly overlay to patch the buildCommand phase of the firefox (wrapper) package.

The plan is simple: take the archive containing schema rules, unpack it, patch it, repack it, and put it back:

nixpkgs.overlays = lib.singleton (final: prev: {
  firefox = prev.firefox.overrideAttrs (old: {
    nativeBuildInputs = (old.nativeBuildInputs or []) ++ (with prev; [ zip unzip gnused ] );
    buildCommand = ''
      export buildRoot="$(pwd)"
    '' + old.buildCommand + ''
      pushd $buildRoot
      unzip $out/lib/firefox/browser/omni.ja -d patched_omni || ret=$?
      if [[ $ret && $ret -ne 2 ]]; then
        echo "unzip exited with unexpected error"
        exit $ret
      fi
      rm $out/lib/firefox/browser/omni.ja
      cd patched_omni
      sed -i 's/"enterprise_only"\s*:\s*true,//' modules/policies/schema.sys.mjs
      zip -0DXqr $out/lib/firefox/browser/omni.ja * # potentially qr9XD
      popd
    '';
  });
})

Now, there is some weirdness going on here! First of, the firefox wrapper enters the $out directory in its build. This is problematic, as we want to unpack the policy schemas from the archive to our build directory where nix will clean them up again. This requires keeping track of the original build directory:

    buildCommand = ''
      export buildRoot="$(pwd)"
    '' + old.buildCommand + ''
      pushd $buildRoot
      ...
      popd
    '';

Now, time to patch the archive! As explained in this blog post, the omni.ja file is just a java jar archive, renamed to .ja because of quirks with windows system restore. Doing a code search through Firefox build scripts, the .ja files are created by gradle. Because these are just regular jar files (even though they don’t say they are!) we can use unzip and zip to unpack them. As documented in this blog post, the unzip utility will successfully unzip the omni.ja archive, but return error code 2. As documented in man unzip in the DIAGNOSTICS section, error code 2 means a generic error in the zipfile format was detected. Processing may have completed successfully anyway; some broken zipfiles created by other archivers have simple work-arounds. This error is fine, it still works - but it requires a workaround. Because the way how nixpkgs implements the buildCommand phase, the workaround needs to pass shellcheck and needs to catch its error immediately:

unzip $out/lib/firefox/browser/omni.ja -d patched_omni || ret=$?
if [[ $ret && $ret -ne 2 ]]; then
  echo "unzip exited with unexpected error"
  exit $ret
fi

After unpacking, we can delete the old omni.ja which is actually just a symlink to the one placed in firefox-unwrapped, and patch the schema file:

rm $out/lib/firefox/browser/omni.ja
cd patched_omni
sed -i 's/"enterprise_only"\s*:\s*true,//' modules/policies/schema.sys.mjs

Repackaging the archive is simple by just using zip -0DXqr (works for me), though some older sources also claim zip -9DXqr to be the correct one (untested).

Applying Policies

Just set programs.firefox.policies.SearchEngines = { ... };, e.g.:

programs.firefox.policies.SearchEngines = {
  Default = "DuckDuckGo";
  Remove = ["Bing" "Google" "Amazon.ca" "eBay"];
};`

Congrats, you are done!

Thanks

Huge thanks to QuadRadical who brought up this topic and was quite capable at Google-fu (duckduck-fu?). Thank you also to K900 for the workaround to catch only a specific error code in a nix build phase, and thank you Hexa for diligently maintaining Firefox. And thank you Niklas, for asking “did you try it?“ when i was about to second-guess my ideas.