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
fiAfter 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.mjsRepackaging 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.