kivikakk.ee

I’ve just had the pleasure of reading three interesting pieces of discourse.

First up, Alyson Escalante’s 2016 piece “Gender Nihilism: An Anti-Manifesto”. There is very little about this I have to disagree with.

Then, her own 2018 response to that work, “Beyond Negativity: What Comes After Gender Nihilism?”. On the one hand, pushing for a more material analysis is excellent, though I feel like all the work is actually done here by Wittig and this merely restates it

On the other hand, what on earth is this ending? It reads just like any other wishy-washy proclamation, missing only a communism,-now!.88x31.gif to slap on your homepage in solidarity. This anti-anti-manifesto is decidedly a manifesto.

The comments, however, conceal a banger which I nearly missed. “blister” (@destroysound) starts, and then wow, they continue. I don’t trust Medium to continue existing for that much longer — or even the Wayback machine (which Medium very carefully tries to break :) love it) — so I’m reproducing it here in its entirety.

There is very little about this I have to disagree with.


Read more

I use Nix flakes a lot — both in development, and in how I deploy artefacts. (You can see my primary flake’s 17 inputs!)

One thing that has gotten a bit annoying, though, has been keeping the pins somewhat in sync. I regularly end up with a dozen nixpkgs and fenix and (etc. etc. etc.) variants in my Nix store, meaning I’d also have half a dozen different Rust, Erlang, Elixir and whatever other versions installed too. If I dare nix-collect-garbage -d, then working on any given project might give me a surprise as it turns out it had a slightly older pin and I actually need to wait for its special magical Elixir version.

And so on!

Using inputs.X.follows is all good and well when combining them into a unified whole, but I don’t use (and don’t intend to use) global devShells — I want each project to stand on its own. So, instead, I want a way to compare and optionally sync the locked versions from some reference to my projects. For a while I found it was a good enough hack to just cp ~/g/vyx/flake.lock . and then let the next Nix invocation remove all the irrelevant ones … but then I’d introduce a dependency which hitched its own nixpkgs along for the ride, and for Reasons™ that would get called "nixpkgs" in flake.lock, and my actual primary Nixpkgs pin called "nixpkgs_2". For a while I was then accidentally using that reference everywhere. Goodness.

Here’s íqán. It takes a plan file, which specifies the source (Vyx, for me), and then a list of targets along with which inputs should be synced. Here’s an excerpt of a sample run:

$ iqan ./iqan.json
Source: /Users/kivikakk/g/vyx
Target: /Users/kivikakk/g/a1d
-----------------------------
input nixpkgs is synced
input fenix is synced
Target: /Users/kivikakk/g/chog
------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/cmark-gfm-hs
--------------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/comenzar
----------------------------------
input nixpkgs is ahead of source (!)
source: 1751741127 (2025-07-05 18:45:27 UTC)
target: 1752620740 (2025-07-15 23:05:40 UTC)
(S)ync to source, or (I)gnore? s
Target: /Users/kivikakk/g/comrak
--------------------------------
input nixpkgs is behind source
source: 1751741127 (2025-07-05 18:45:27 UTC)
target: 1748437600 (2025-05-28 13:06:40 UTC)
(S)ync to source, or (I)gnore? s
input "fenix" has an original mismatch
source: github:nix-community/fenix
target: github:nix-community/fenix/monthly
(S)ync to source, or (I)gnore? s
[!!!] update flake.nix please!
Target: /Users/kivikakk/g/iqan
------------------------------
input nixpkgs is synced
input fenix is synced
Target: /Users/kivikakk/g/kivikakk
----------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/koino
-------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/kv
----------------------------
input nixpkgs is synced
input fenix is synced
Target: /Users/kivikakk/g/nossa
-------------------------------
input nixpkgs is synced
input fenix is synced
Target: /Users/kivikakk/g/notes
-------------------------------
input nixpkgs is synced
Target: /Users/kivikakk/g/outfoxsync
------------------------------------
input "nixpkgs" has an original mismatch
source: github:NixOS/nixpkgs/nixos-25.05
target: github:NixOS/nixpkgs/nixos-24.11
(S)ync to source, or (I)gnore? s
[!!!] update flake.nix please!
$

It does the job. :)

  • Start with github:cpick/nix-rosetta-builder.

  • Serving suggestion (adjust for your machine):

    nix-rosetta-builder = {
    enable = true;
    cores = 12;
    diskSize = "100GiB";
    memory = "16GiB";
    onDemand = true;
    };

    That’s it! It’ll spin itself up when needed (you might need to retry the build since the first attempt might timeout while it starts up on-demand), and down again after a while of inactivity. Now you can do something like nix build .#packages.x86_64-linux.chog and it’ll Just Work™.

  • Whoops! I lied. You’ll get perhaps a lot of things like this:

    error: build of '/nix/store/9dhn75m3isw55qz29dzw1h8xzs634pim-salchicha-0.4.0.drv' on 'ssh-ng://rosetta-builder' failed: builder for '/nix/store/9dhn75m3isw55qz29dzw1h8xzs634pim-salchicha-0.4.0.drv' failed with exit code 132;
    last 9 log lines:
    > Running phase: unpackPhase
    > unpacking source archive /nix/store/l05szcgqqaxxc9i1hd16ik8kd9vv5cn2-salchicha-0.4.0
    > source root is salchicha-0.4.0
    > Running phase: patchPhase
    > Running phase: updateAutotoolsGnuConfigScriptsPhase
    > Running phase: configurePhase
    > Running phase: buildPhase
    > Compiling 3 files (.ex)
    > /nix/store/qjjpd1310d6qi63pdmjigykcznfi3n4y-stdenv-linux/setup: line 1765: 44 Illegal instruction (core dumped) mix compile --no-deps-check
    For full logs, run:
    nix log /nix/store/9dhn75m3isw55qz29dzw1h8xzs634pim-salchicha-0.4.0.drv
    error: builder for '/nix/store/9dhn75m3isw55qz29dzw1h8xzs634pim-salchicha-0.4.0.drv' failed with exit code 1
    error: build of '/nix/store/3jfv7sghsansgg4sgihqkcsxg925zfrj-jason-1.4.4.drv' on 'ssh-ng://rosetta-builder' failed: builder for '/nix/store/3jfv7sghsansgg4sgihqkcsxg925zfrj-jason-1.4.4.drv' failed with exit code 134;
    last 12 log lines:
    > Running phase: unpackPhase
    > unpacking source archive /nix/store/km205nlk52awji63d5hynwnypdpfqqm3-jason-1.4.4
    > source root is jason-1.4.4
    > Running phase: patchPhase
    > Running phase: updateAutotoolsGnuConfigScriptsPhase
    > Running phase: configurePhase
    > Running phase: buildPhase
    > Compiling 10 files (.ex)
    > no next heap size found: 2305825417165001762, offset 0
    >
    > Crash dump is being written to: erl_crash.dump...done
    > /nix/store/qjjpd1310d6qi63pdmjigykcznfi3n4y-stdenv-linux/setup: line 1765: 44 Aborted (core dumped) mix compile --no-deps-check
    For full logs, run:
    nix log /nix/store/3jfv7sghsansgg4sgihqkcsxg925zfrj-jason-1.4.4.drv
    error: builder for '/nix/store/3jfv7sghsansgg4sgihqkcsxg925zfrj-jason-1.4.4.drv' failed with exit code 1

Just give me the fix.

Assuming you have pkgs from somewhere:

let
erlang = pkgs.beam_minimal.interpreters.erlang_27;
erlang-jmsingle =
(erlang.override {
patches = [
./nix/erlang_beam_jit_main.cpp.patch
];
}).overrideAttrs
(prev: {
preConfigure = ''
${prev.preConfigure or ""}
configureFlagsArray+=(CFLAGS="-O2 -g -DFORCE_ERTS_JIT_SINGLE_MAP=1")
'';
});
mkPackageWithErlang =
erlang:
let
beamPackages = pkgs.beam_minimal.packagesWith erlang;
elixir = beamPackages.elixir_1_18;
in
pkgs.callPackage ./nix/package.nix {
inherit
beamPackages
erlang
elixir
;
};

Note that -O2 -g are defaults for the Erlang build — we have to specify them again since we’re overriding CFLAGS as a whole.

Here’s the patch:

diff --git a/erts/emulator/beam/jit/beam_jit_main.cpp b/erts/emulator/beam/jit/beam_jit_main.cpp
index 8408b25056..d941532e5f 100644
--- a/erts/emulator/beam/jit/beam_jit_main.cpp
+++ b/erts/emulator/beam/jit/beam_jit_main.cpp
@@ -188,6 +188,8 @@ static JitAllocator *pick_allocator() {
* 64-bit x86 still uses dual-mapped memory as it lacks support for per-
* thread permissions and thus gets unprotected RWX pages with MAP_JIT. */
erts_jit_single_map = 1;
+#elif defined(FORCE_ERTS_JIT_SINGLE_MAP)
+ erts_jit_single_map = 1;
#endif
#if defined(HAVE_LINUX_PERF_SUPPORT)

Now you can expose packages like:

packages = rec {
default = chog;
chog = mkPackageWithErlang erlang;
chog-jmsingle = mkPackageWithErlang erlang-jmsingle;
};

Finally, your cross compile is nix build .#packages.x86_64-linux.chog-jmsingle.

???

Let’s read the erts erl command manual together:

+JMsingle true|false - Enables or disables the use of single-mapped RWX memory for JIT code.

The default is to map JIT:ed machine code into two regions sharing the same physical pages, where one region is executable but not writable, and the other writable but not executable. As some tools, such as QEMU user mode emulation, cannot deal with the dual mapping, this flags allows it to be disabled. This flag is automatically enabled by the +JPperf flag.

Since: OTP 26.0

“Some tools” also includes Rosetta, it turns out. You’ll find some advice on Internet suggesting you use +JPperf with a variety of options to fix this issue, but it’s all cargo-culted — it’s the side-effect of setting +JMsingle true that makes it work. The above patch causes the flag to be set unilaterally.

The solution I present here has downsides:

  • You’re no longer using separate RW/RX mappings, which has (somewhat theoretical) security implications. RIP points to writeable memory!
  • We have to patch Erlang to make this happen consistently! I so wish we didn’t, but I couldn’t figure out a way to get the actual +JMsingle flag to propagate and be set in every single place necessary — namely, every single Erlang/Elixir/etc. invocation. The result is toolchain bloat.
  • The resulting builds therefore differ from those without it.

I use this for testing and building x86_64 artifacts from the comfort of my laptop, but when I actually deploy on x86_64, I build the regular versions on a target machine.

May this help someone!

Oh my goodness. Turns out deps_nix exists, and it obviates not only half of the hacks from yesterday, but also many, many more I’d just accepted as unavoidable, and something I’d have to build some tooling around. Well, looks like someone already has :)

LiveView v1.1 has been released! I like to keep on top of Phoenix’s updates; the ecosystem moves at a reasonable and steady pace, and the direction is in general good. (Not sure I care for colocated hooks yet, but change tracking in comprehensions is extremely welcome, it’s nice to be able to ditch @types/phoenix_live_view, and <.portal> looks neat!)

LazyHTML is a new dependency, and you bet it tries to download pre-compiled dependencies. I never want that, and Nix is so good to me by simply Not Permitting It:

lazy_html> Running phase: unpackPhase
lazy_html> unpacking source archive /nix/store/qlgi3hx2syzhzq2r8l5b8xd42zzwrvb5-lazy_html-0.1.3
lazy_html> source root is lazy_html-0.1.3
lazy_html> Running phase: patchPhase
lazy_html> Running phase: updateAutotoolsGnuConfigScriptsPhase
lazy_html> Running phase: configurePhase
lazy_html> Running phase: buildPhase
lazy_html> ** (File.Error) could not make directory (with -p) "/homeless-shelter/Library/Caches/elixir_make": no such file or directory
lazy_html> (elixir 1.18.4) lib/file.ex:346: File.mkdir_p!/1
lazy_html> (elixir_make 0.9.0) lib/elixir_make/artefact.ex:22: ElixirMake.Artefact.cache_dir/0
lazy_html> (elixir_make 0.9.0) lib/elixir_make/artefact.ex:85: ElixirMake.Artefact.archive_path/3
lazy_html> (elixir_make 0.9.0) lib/mix/tasks/compile.elixir_make.ex:226: Mix.Tasks.Compile.ElixirMake.download_or_reuse_nif/3
lazy_html> (elixir_make 0.9.0) lib/mix/tasks/compile.elixir_make.ex:169: Mix.Tasks.Compile.ElixirMake.run/1
lazy_html> (mix 1.18.4) lib/mix/task.ex:495: anonymous fn/3 in Mix.Task.run_task/5
lazy_html> (mix 1.18.4) lib/mix/tasks/compile.all.ex:117: Mix.Tasks.Compile.All.run_compiler/2
lazy_html> (mix 1.18.4) lib/mix/tasks/compile.all.ex:97: Mix.Tasks.Compile.All.compile/4

Close enough, you get the idea — elixir_make tried to create $HOME/Library/Caches/elixir_make (because this is nix-darwin), and if it had succeeded, would have failed to then actually download anything. How will we figure this out?

tl;dr

Step one: stop LazyHTML from trying to use pre-compiled dependencies

Assuming you’ve produced deps.nix with mix2nix, we patch out the pre-compilation configuration from LazyHTML’s mix.exs:

let
mixNixDeps = import ./deps.nix {
inherit lib beamPackages;
overrides = final: prev: {
lazy_html = prev.lazy_html.overrideAttrs {
patches = [
./lazy_html-mix.exs.patch
];
};
};
};
diff --git a/mix.exs b/mix.exs
index fa0be6f..0f40d16 100644
--- a/mix.exs
+++ b/mix.exs
@@ -24,11 +24,6 @@ defmodule LazyHTML.MixProject do
"LEXBOR_VERSION" => @lexbor_version
}
end,
- # Precompilation
- make_precompiler: {:nif, CCPrecompiler},
- make_precompiler_url: "#{@github_url}/releases/download/v#{@version}/@{artefact_filename}",
- make_precompiler_filename: "liblazy_html",
- make_precompiler_nif_versions: [versions: ["2.16"]]
]
end
@@ -42,7 +37,6 @@ defmodule LazyHTML.MixProject do
[
{:fine, "~> 0.1.0"},
{:elixir_make, "~> 0.9.0"},
- {:cc_precompiler, "~> 0.1", runtime: false},
{:ex_doc, "~> 0.36", only: :dev, runtime: false}
]
end

elixir_make does have a config key, :force_build, but we’d have to patch LazyHTML to include it either way: this build is completely independent of your application, so setting it in your own config won’t do anything at Nix build time, and there’s no environment variable equivalent like RUSTLER_PRECOMPILED_FORCE_BUILD_ALL.

And the result:

lazy_html> Running phase: unpackPhase
lazy_html> unpacking source archive /nix/store/qlgi3hx2syzhzq2r8l5b8xd42zzwrvb5-lazy_html-0.1.3
lazy_html> source root is lazy_html-0.1.3
lazy_html> Running phase: patchPhase
lazy_html> applying patch /nix/store/vk0kvy6ih5nwr75k7yzi8k8nnggycy6w-lazy_html-mix.exs.patch
lazy_html> patching file mix.exs
lazy_html> Running phase: updateAutotoolsGnuConfigScriptsPhase
lazy_html> Running phase: configurePhase
lazy_html> Running phase: buildPhase
lazy_html> make: git: No such file or directory
lazy_html> make: *** [Makefile:50: /private/tmp/nix-build-lazy_html-0.1.3.drv-0/lazy_html-0.1.3/_build/c/third_party/lexbor/2.4.0] Error 127
lazy_html> ** (Mix) Could not compile with "make" (exit status: 2).
lazy_html> You need to have gcc and make installed. Try running the
lazy_html> commands "gcc --version" and / or "make --version". If these programs
lazy_html> are not installed, you will be prompted to install them.
lazy_html>

LazyHTML wraps Lexbor, a C library used for its HTML engine, and here it is trying to download its source at compile-time. (If you make git available to the build, Nix will block its network access.)

Step two: fetch Lexbor’s source ourselves

Let’s declare the version of Lexbor we want, fetch that from GitHub, and then put it where LazyHTML’s Makefile would clone it to. We also check that the version we’ve declared matches the one in LazyHTML’s mix.exs, so we’ll notice if it changes.

(You could try to extract this automatically, but you’ll have to update the hash manually either way. I’ll also skip the step where this fails because the build wants cmake and just give it cmake.)

let
mixNixDeps = import ./deps.nix {
inherit lib beamPackages;
overrides = final: prev: {
lazy_html = prev.lazy_html.overrideAttrs (
prevAttrs:
let
lexborVersion = "2.4.0";
lexbor = pkgs.fetchFromGitHub {
owner = "lexbor";
repo = "lexbor";
tag = "v${lexborVersion}";
hash = "sha256-wsm+2L2ar+3LGyBXl39Vp9l1l5JONWvO0QbI87TDfWM=";
};
in
{
nativeBuildInputs = prevAttrs.nativeBuildInputs ++ (with pkgs; [ cmake ]);
prePatch = ''
# ensure the version is in sync.
grep -q '@lexbor_version "${lexborVersion}"' mix.exs
mkdir -p _build/c/third_party/lexbor
cp -r ${lexbor} _build/c/third_party/lexbor/${lexborVersion}
chmod -R u+w _build/c/third_party/lexbor/${lexborVersion}
'';
patches = [
./lazy_html-mix.exs.patch
];
}
);
};
};

Now we successfully build Lexbor as part of LazyHTML, yay! But then:

lazy_html> [ 99%] Building C object CMakeFiles/lexbor_static.dir/source/lexbor/utils/http.c.o
lazy_html> [ 99%] Building C object CMakeFiles/lexbor_static.dir/source/lexbor/utils/warc.c.o
lazy_html> [100%] Linking C static library liblexbor_static.a
lazy_html> [100%] Built target lexbor_static
lazy_html> make[1]: Leaving directory '/private/tmp/nix-build-lazy_html-0.1.3.drv-0/lazy_html-0.1.3/_build/c/third_party/lexbor/2.4.0/build'
lazy_html> clang++ -shared -fPIC -fvisibility=hidden -std=c++17 -Wall -Wextra -Wno-unused-parameter -Wno-comment -I/nix/store/62pchbdm94l23xcd9c7z76pywli4v9r0-erlang-27.3.4.1/lib/erlang/erts-15.2.7/include -I/private/tmp/nix-build-fine-0.1.2.drv-0/fine-0.1.2/include -I/private/tmp/nix-build-lazy_html-0.1.3.drv-0/lazy_html-0.1.3/_build/c/third_party/lexbor/2.4.0/source -O3 -undefined dynamic_lookup -flat_namespace /private/tmp/nix-build-lazy_html-0.1.3.drv-0/lazy_html-0.1.3/c_src/lazy_html.cpp /private/tmp/nix-build-lazy_html-0.1.3.drv-0/lazy_html-0.1.3/_build/c/third_party/lexbor/2.4.0/build/liblexbor_static.a -o /private/tmp/nix-build-lazy_html-0.1.3.drv-0/lazy_html-0.1.3/_build/prod/lib/lazy_html/priv/liblazy_html.so
lazy_html> /private/tmp/nix-build-lazy_html-0.1.3.drv-0/lazy_html-0.1.3/c_src/lazy_html.cpp:3:10: fatal error: 'fine.hpp' file not found
lazy_html> 3 | #include <fine.hpp>
lazy_html> | ^~~~~~~~~~
lazy_html> 1 error generated.
lazy_html> make: *** [Makefile:38: /private/tmp/nix-build-lazy_html-0.1.3.drv-0/lazy_html-0.1.3/_build/prod/lib/lazy_html/priv/liblazy_html.so] Error 1
lazy_html> ** (Mix) Could not compile with "make" (exit status: 2).
lazy_html> You need to have gcc and make installed. Try running the
lazy_html> commands "gcc --version" and / or "make --version". If these programs
lazy_html> are not installed, you will be prompted to install them.
lazy_html>

LazyHTML depends on Fine, and if you squint at that last output, you’ll see this argument to clang++:

-I/private/tmp/nix-build-fine-0.1.2.drv-0/fine-0.1.2/include

Fine reports its own include directory in Fine.include_dir/0 like this:

defmodule Fine do
# [...]
@include_dir Path.expand("include")
@doc """
Returns the directory with Fine header files.
"""
@spec include_dir() :: String.t()
def include_dir(), do: @include_dir
end

This resolves the absolute path of the include dir relative to the current working directory at compile time. If you’re building Fine in the same filesystem as its dependent, this works great — Mix changes the cwd to each project’s root as it goes. If not, you’ll have a compiled-in path that may not exist when you try to use it, which is exactly what’s happened above — that’s the Fine sandbox path being used while we’re building in LazyHTML’s sandbox.

Step three: get Fine to report its installed location, not its build-time one

Just one more patch. Just one. I swear. I can stop any time I want.

let
mixNixDeps = import ./deps.nix {
inherit lib beamPackages;
overrides = final: prev: {
lazy_html = prev.lazy_html.overrideAttrs (
prevAttrs:
let
lexborVersion = "2.4.0";
lexbor = pkgs.fetchFromGitHub {
owner = "lexbor";
repo = "lexbor";
tag = "v${lexborVersion}";
hash = "sha256-wsm+2L2ar+3LGyBXl39Vp9l1l5JONWvO0QbI87TDfWM=";
};
in
{
nativeBuildInputs = prevAttrs.nativeBuildInputs ++ (with pkgs; [ cmake ]);
prePatch = ''
# ensure the version is in sync.
grep -q '@lexbor_version "${lexborVersion}"' mix.exs
mkdir -p _build/c/third_party/lexbor
cp -r ${lexbor} _build/c/third_party/lexbor/${lexborVersion}
chmod -R u+w _build/c/third_party/lexbor/${lexborVersion}
'';
patches = [
./lazy_html-mix.exs.patch
];
}
);
fine = prev.fine.overrideAttrs {
patches = [
./fine-lib-fine.ex.patch
];
};
};
};
diff --git a/lib/fine.ex b/lib/fine.ex
index 277b983..22c8d4f 100644
--- a/lib/fine.ex
+++ b/lib/fine.ex
@@ -8,11 +8,9 @@ defmodule Fine do
@moduledoc readme_docs
- @include_dir Path.expand("include")
-
@doc """
Returns the directory with Fine header files.
"""
@spec include_dir() :: String.t()
- def include_dir(), do: @include_dir
+ def include_dir(), do: Application.app_dir(:fine, "include")
end

Instead of resolving something about cwd at compile-time, we resolve the installed application path at runtime. Lovely! 🍴

This now builds successfully! Yay!

And then your application fails bring-up when you’re using the compiled release:

ERROR! the application :lazy_html has a different value set for key :inspect_extra_newline during runtime compared to compile time. Since this application environment entry was marked as compile time, this difference can lead to different behavior than expected:
* Compile time value was set to: true
* Runtime value was not set
To fix this error, you might:
* Make the runtime value match the compile time one
* Recompile your project. If the misconfigured application is a dependency, you may need to run "mix deps.clean lazy_html --build"
* Alternatively, you can disable this check. If you are using releases, you can set :validate_compile_env to false in your release configuration. If you are using Mix to start your system, you can pass the --no-validate-compile-env flag
Runtime terminating during boot ({<<"aborting boot">>,[{'Elixir.Config.Provider',boot,2,[]}]})
Crash dump is being written to: erl_crash.dump...⏎

Oop. Turns out LazyHTML’s config.exs declares a single config var to avoid some behaviour in test:

import Config
# We disable the extra newline in test env, because it breaks doctests.
config :lazy_html, :inspect_extra_newline, config_env() != :test

The actual use of this config has the correct default (since this config isn’t used in your dependent application):

if Application.compile_env(:lazy_html, :inspect_extra_newline, true) do
defp separator(), do: line()
else
defp separator(), do: empty()
end

But, because we compiled LazyHTML by itself, that explicit true got compiled in, and the lack of runtime value for it means we crash out.

Step four: declare LazyHTML’s config in our application

In your config.exs:

config :lazy_html, :inspect_extra_newline, true

That’s it!

Egumi? Isn’t that a lot of work?

It sure is. Here’s the PRs I opened this morning to make some of this unnecessary:

LiveView also now supplies its own types (the DefinitelyTyped ones under @types/phoenix_live_view are good but not ~official~!), but not in a way that seems compatible with certain TypeScript moduleResolution configurations, so:

Aside

Technically, a lot of this is on us — we don’t actually need LazyHTML in our build, since it’s marked only: :test — but mix2nix doesn’t exclude it on that basis. phoenix_live_view also declares it as a dependency (albeit with optional: true), but you could do some override shenanigans to excise it entirely.

But: (a) I’ll do what I feel like, and (b) I actually use Floki in my application code, and it has support for a Lexbor-based parser which until now I’ve been unable to use precisely because it entailed all this. Now I can just port my Floki uses to LazyHTML and drop it entirely!

Edit: Fine v0.1.3 no longer includes the includes and it’s all my fault!

Dw bb. Nix gotchu. Get rid of the old patch — our new one is a bit more (and a bit less) dynamic.

fine = prev.fine.overrideAttrs (prevAttrs: {
prePatch = ''
sed -i -e "s|@include_dir Path\.expand(\"c_include\")|@include_dir \"${prevAttrs.src}/c_include\"|" lib/fine.ex
'';
});

include was renamed to c_include — the only reason include was making it into Fine’s application directory was because that’s the directory name used by Erlang .hrl files.

How to solve this? We replace the call to Path.expand/1 with the path to the c_include directory in the source!

I really like knowing this happened right. Let’s decompile the resulting Elixir.Fine.beam file and pull Fine.include_dir/0’s contents:

iex(1)> with {:ok, {Fine, [{:abstract_code, {:raw_abstract_v1, code}}]}} <-
:beam_lib.chunks(~c"/nix/store/nx1bl8jzsg1ns6yfv237spxbd45n9jxs-fine-0.1.3/lib/erlang/lib/fine-0.1.3/ebin/Elixir.Fine.beam", [:abstract_code]),
[body] <- for({:function, _, :include_dir, _, body} <- code, do: body),
do: :erl_prettypr.format(:erl_syntax.form_list(body), ribbon: 100) |> IO.puts()
() -> <<"/nix/store/d54xr3cssgmsj9kwkd04gzvx1jbi2pc3-fine-0.1.3/c_include">>
:ok
iex(2)>

Looks good to me! One question that arises — will this include path definitely be present for all uses of the resulting derivation? It is of course in a different store path — we couldn’t create a store path that referred to itself, after all. I think the answer is no — there’s nothing in the result derivation that tells Nix that the source must stick around.

The answer is to also specify the source derivation in propagatedNativeBuildInputs. What a mouthful. As a quick reviser, for a given derivation, the following definitions generally hold:

  • buildInputs — dependencies that must exist in the runtime environment.
  • nativeBuildInputs — dependencies that must exist in the build environment.
  • propagatedBuildInputs — dependencies that must exist in the runtime environment, and the runtime environment of any downstream user of this package.
  • propagatedNativeBuildInputs — dependencies that must exist in the build environment, and the build environment of any downstream user of this package.

So:

fine = prev.fine.overrideAttrs (prevAttrs: {
propagatedNativeBuildInputs = [prevAttrs.src];
prePatch = ''
sed -i -e "s|@include_dir Path\.expand(\"c_include\")|@include_dir \"${prevAttrs.src}/c_include\"|" lib/fine.ex
'';
});

The resulting derivation now has a file nix-support/propagated-native-build-inputs, which lists the source derivation, matching the path compiled into Elixir.Fine.beam. This should mean that any build that depends on fine will be assured of the include directory’s existence at build-time. :)

Maybe this’ll do!