beamPackages.buildMix: move to fixed point builder and hooks (#475224)

This commit is contained in:
Adam C. Stephens
2026-02-14 02:07:23 +00:00
committed by GitHub
11 changed files with 216 additions and 158 deletions
+63 -133
View File
@@ -1,55 +1,50 @@
{
stdenv,
writeText,
elixir,
erlang,
hex,
beamCopySourceHook,
beamModuleInstallHook,
mixBuildDirHook,
mixCompileHook,
mixAppConfigPatchHook,
lib,
stdenv,
writeText,
}:
{
name,
version,
src,
buildInputs ? [ ],
nativeBuildInputs ? [ ],
erlangCompilerOptions ? [ ],
# Deterministic Erlang builds remove full system paths from debug information
# among other things to keep builds more reproducible. See their docs for more:
# https://www.erlang.org/doc/man/compile
erlangDeterministicBuilds ? true,
beamDeps ? [ ],
propagatedBuildInputs ? [ ],
postPatch ? "",
compilePorts ? false,
meta ? { },
enableDebugInfo ? false,
mixEnv ? "prod",
mixTarget ? "host",
removeConfig ? true,
# A config directory that is considered for all the dependencies of an app, typically in $src/config/
# This was initially added, as some of Mobilizon's dependencies need to access the config at build time.
appConfigPath ? null,
...
}@attrs:
lib.extendMkDerivation {
constructDrv = stdenv.mkDerivation;
excludeDrvArgNames = [
"mixEnv"
];
extendDrvArgs =
finalAttrs:
{
beamDeps ? [ ],
mixEnv ? "prod",
mixTarget ? "host",
assert appConfigPath != null -> removeConfig;
# Allow passing compile time config instead of an empty config
appConfigPath ? null,
let
shell =
drv:
stdenv.mkDerivation {
name = "interactive-shell-${drv.name}";
buildInputs = [ drv ];
};
enableDebugInfo ? false,
erlangCompilerOptions ? [ ],
# Deterministic Erlang builds remove full system paths from debug information
# among other things to keep builds more reproducible. See their docs for more:
# https://www.erlang.org/doc/man/compile
erlangDeterministicBuilds ? true,
...
}@args:
{
name = "erlang${erlang.version}-${args.name}-${finalAttrs.version}";
pkg =
self:
stdenv.mkDerivation (
attrs
// {
name = "${name}-${version}";
inherit version src;
env = {
ERL_COMPILER_OPTIONS =
let
options = erlangCompilerOptions ++ lib.optionals erlangDeterministicBuilds [ "deterministic" ];
in
"[${lib.concatStringsSep "," options}]";
MIX_ENV = mixEnv;
MIX_TARGET = mixTarget;
@@ -57,103 +52,38 @@ let
MIX_DEBUG = if enableDebugInfo then 1 else 0;
HEX_OFFLINE = 1;
__darwinAllowLocalNetworking = true;
ERL_COMPILER_OPTIONS =
let
options = erlangCompilerOptions ++ lib.optionals erlangDeterministicBuilds [ "deterministic" ];
in
"[${lib.concatStringsSep "," options}]";
LANG = if stdenv.hostPlatform.isLinux then "C.UTF-8" else "C";
LC_CTYPE = if stdenv.hostPlatform.isLinux then "C.UTF-8" else "UTF-8";
# add to ERL_LIBS so other modules can find at runtime.
# http://erlang.org/doc/man/code.html#code-path
# Mix also searches the code path when compiling with the --no-deps-check flag
setupHook = attrs.setupHook or writeText "setupHook.sh" ''
addToSearchPath ERL_LIBS "$1/lib/erlang/lib"
'';
# some hooks need name-version, but we've overridden name above for the nix package
beamModuleName = args.name;
};
buildInputs = buildInputs ++ [ ];
nativeBuildInputs = nativeBuildInputs ++ [
elixir
hex
];
propagatedBuildInputs = propagatedBuildInputs ++ beamDeps;
__darwinAllowLocalNetworking = true;
configurePhase =
attrs.configurePhase or ''
runHook preConfigure
# add to ERL_LIBS so other modules can find at runtime.
# http://erlang.org/doc/man/code.html#code-path
# Mix also searches the code path when compiling with the --no-deps-check flag
# This is used by package builders such as mixRelease
setupHook = writeText "setupHook.sh" ''
addToSearchPath ERL_LIBS "$1/lib/erlang/lib"
'';
${./mix-configure-hook.sh}
${lib.optionalString (removeConfig && isNull appConfigPath)
# By default, we don't want to include whatever config a dependency brings; per
# https://hexdocs.pm/elixir/main/Config.html, config is application specific.
''
rm -rf config
mkdir config
''
}
${lib.optionalString (!isNull appConfigPath)
# Some more tightly-coupled dependencies do depend on the config of the application
# they're being built for.
''
rm -rf config
cp -r ${appConfigPath} config
''
}
nativeBuildInputs = (args.nativeBuildInputs or [ ]) ++ [
elixir
hex
runHook postConfigure
'';
beamCopySourceHook
beamModuleInstallHook
mixBuildDirHook
mixCompileHook
mixAppConfigPatchHook
];
buildPhase =
attrs.buildPhase or ''
runHook preBuild
export HEX_HOME="$TEMPDIR/hex"
export MIX_HOME="$TEMPDIR/mix"
mix compile --no-deps-check
runHook postBuild
'';
propagatedBuildInputs = (args.propagatedBuildInputs or [ ]) ++ beamDeps;
installPhase =
attrs.installPhase or ''
runHook preInstall
# This uses the install path convention established by nixpkgs maintainers
# for all beam packages. Changing this will break compatibility with other
# builder functions like buildRebar3 and buildErlangMk.
mkdir -p "$out/lib/erlang/lib/${name}-${version}"
# Some packages like db_connection will use _build/shared instead of
# honoring the $MIX_ENV variable.
for reldir in _build/{$MIX_BUILD_PREFIX,shared}/lib/${name}/{src,ebin,priv,include} ; do
if test -d $reldir ; then
# Some builds produce symlinks (eg: phoenix priv dircetory). They must
# be followed with -H flag.
cp -Hrt "$out/lib/erlang/lib/${name}-${version}" "$reldir"
fi
done
# Copy the source so it can be used by dependent packages. For example,
# phoenix applications need the source of phoenix and phoenix_html to
# build javascript and css assets.
mkdir -p $out/src
cp -r "$src/." "$out/src"
runHook postInstall
'';
# stripping does not have any effect on beam files
# it is however needed for dependencies with NIFs like bcrypt for example
dontStrip = false;
passthru = {
packageName = name;
env = shell self;
inherit beamDeps;
};
}
);
in
lib.fix pkg
passthru = (args.passthru or { }) // {
inherit beamDeps;
};
};
}
+12
View File
@@ -11,6 +11,9 @@ let
callPackageWithScope =
scope: drv: args:
lib.callPackageWith scope drv args;
callPackagesWithScope =
scope: drv: args:
lib.callPackagesWith scope drv args;
mkScope = scope: pkgs // scope;
packages =
@@ -18,6 +21,7 @@ let
let
defaultScope = mkScope self;
callPackage = drv: args: callPackageWithScope defaultScope drv args;
callPackages = drv: args: callPackagesWithScope defaultScope drv args;
in
rec {
inherit callPackage erlang;
@@ -95,6 +99,14 @@ let
# without helper functions buildRebar3 and buildMix.
hex = callPackage ./hex { };
webdriver = callPackage ./webdriver { };
inherit (callPackages ./hooks { })
beamCopySourceHook
beamModuleInstallHook
mixBuildDirHook
mixCompileHook
mixAppConfigPatchHook
;
};
in
makeExtensible packages
@@ -0,0 +1,16 @@
# shellcheck shell=bash
#
# Copy the source so it can be used by mix projects to assemble `deps`
# do this before building to avoid build artifacts but after patching
# to include any user modifications to the source
beamCopySourceHook() {
echo "Executing beamCopySourceHook"
mkdir -p "$out/src"
cp -r "." "$out/src"
echo "Finished beamCopySourceHook"
}
postPatchHooks+=(beamCopySourceHook)
@@ -0,0 +1,25 @@
# shellcheck shell=bash
#
# This uses the install path convention established by nixpkgs maintainers
# for all beam packages. Changing this will break compatibility with other
# builder functions like buildRebar3 and buildErlangMk.
beamModuleInstallHook() {
echo "Executing beamModuleInstallHook"
mkdir -p "$out/lib/erlang/lib/${beamModuleName}-${version}"
for reldir in _build/{$MIX_BUILD_PREFIX,shared}/lib/${beamModuleName}/{src,ebin,priv,include}; do
if test -d "$reldir"; then
# Some builds produce symlinks (eg: phoenix priv directory). They must
# be followed with -H flag.
cp -vHrt "$out/lib/erlang/lib/${beamModuleName}-${version}" "$reldir"
fi
done
echo "Finished beamModuleInstallHook"
}
if [ -z "${dontBeamModuleInstall-}" ] && [ -z "${installPhase-}" ]; then
installPhase=beamModuleInstallHook
fi
@@ -0,0 +1,22 @@
{ makeSetupHook }:
{
beamCopySourceHook = makeSetupHook {
name = "beam-copy-source-hook.sh";
} ./beam-copy-source-hook.sh;
beamModuleInstallHook = makeSetupHook {
name = "beam-module-install-hook.sh";
} ./beam-module-install-hook.sh;
mixBuildDirHook = makeSetupHook {
name = "mix-configure-hook.sh";
} ./mix-build-dir-hook.sh;
mixCompileHook = makeSetupHook {
name = "mix-compile-hook.sh";
} ./mix-compile-hook.sh;
mixAppConfigPatchHook = makeSetupHook {
name = "mix-config-patch-hook.sh";
} ./mix-app-config-patch-hook.sh;
}
@@ -0,0 +1,19 @@
# shellcheck shell=bash
#
# We don't want to include whatever config a dependency brings; per
# https://hexdocs.pm/elixir/main/Config.html, config is application specific.
mixAppConfigPatchHook() {
echo "Executing mixAppConfigPatchHook"
rm -rvf config
# But we still need a way to provide config for deps that need compile time config
if [ ! -z "${appConfigPath}" ]; then
ln -sf "${appConfigPath}" config
fi
echo "Finished mixAppConfigPatchHook"
}
prePatchHooks+=(mixAppConfigPatchHook)
+26
View File
@@ -0,0 +1,26 @@
# shellcheck shell=bash
#
# Symlink all dependencies found in ERL_LIBS since Elixir does not honor ERL_LIBS
mixBuildDirHook() {
echo "Executing mixBuildDirHook"
mkdir -p _build/"$MIX_BUILD_PREFIX"/lib
while read -r -d ':' lib; do
for dir in "$lib"/*; do
# Strip version number for directory name if it exists, so naming of
# all libs matches what mix's expectation.
dest=$(basename "$dir" | cut -d '-' -f1)
build_dir="_build/$MIX_BUILD_PREFIX/lib/$dest"
# Symlink libs to _build so that mix can find them when compiling.
# This is what allows mix to compile the package without searching
# for dependencies over the network.
ln -sv "$dir" "$build_dir"
done
done <<<"$ERL_LIBS:"
echo "Finished mixBuildDirHook"
}
preConfigureHooks+=(mixBuildDirHook)
+20
View File
@@ -0,0 +1,20 @@
# shellcheck shell=bash
mixCompileHook() {
echo "Executing mixCompileHook"
runHook preBuild
local flagsArray=()
concatTo flagsArray mixCompileFlags
mix compile --no-deps-check "${flagsArray[@]}"
runHook postBuild
echo "Finished mixCompileHook"
}
if [ -z "${dontMixCompile-}" ] && [ -z "${buildPhase-}" ]; then
buildPhase=mixCompileHook
fi
@@ -1,18 +1,5 @@
# shellcheck shell=bash
# this hook will symlink all dependencies found in ERL_LIBS
# since Elixir 1.12.2 elixir does not look into ERL_LIBS for
# elixir depencencies anymore, so those have to be symlinked to the _build directory
mkdir -p _build/"$MIX_BUILD_PREFIX"/lib
while read -r -d ':' lib; do
for dir in "$lib"/*; do
# Strip version number for directory name if it exists, so naming of
# all libs matches what mix's expectation.
dest=$(basename "$dir" | cut -d '-' -f1)
build_dir="_build/$MIX_BUILD_PREFIX/lib/$dest"
((MIX_DEBUG == 1)) && echo "Linking $dir to $build_dir"
# Symlink libs to _build so that mix can find them when compiling.
# This is what allows mix to compile the package without searching
# for dependencies over the network.
ln -s "$dir" "$build_dir"
done
done <<< "$ERL_LIBS:"
# Added 2026-02-12 for external users
echo "mix-configure-hook.sh has been removed and is now a setup hook.
Add 'beamPackages.mixBuildDirHook' to 'nativeBuildInputs' instead.
" >&2
exit 1
@@ -16,6 +16,7 @@
gnused,
gnugrep,
gawk,
mixBuildDirHook,
}@inputs:
{
@@ -108,6 +109,7 @@ stdenv.mkDerivation (
elixir
hex
git
mixBuildDirHook
]
++
# Mix deps
@@ -171,8 +173,6 @@ stdenv.mkDerivation (
attrs.configurePhase or ''
runHook preConfigure
${./mix-configure-hook.sh}
# This is needed for projects that have a specific compile step
# the dependency needs to be compiled in order for the task
# to be available.
@@ -186,13 +186,12 @@ stdenv.mkDerivation (
${lib.optionalString (mixNixDeps != { }) ''
mkdir -p deps
${lib.concatMapStringsSep "\n" (dep: ''
dep_name=$(basename ${dep} | cut -d '-' -f2)
dep_path="deps/$dep_name"
${lib.concatMapAttrsStringSep "\n" (name: dep: ''
dep_path="deps/${name}"
if [ -d "${dep}/src" ]; then
ln -s ${dep}/src $dep_path
ln -sv ${dep}/src $dep_path
fi
'') (builtins.attrValues mixNixDeps)}
'') mixNixDeps}
''}
# Symlink deps to build root. Similar to above, but allows for mixFodDeps
+2
View File
@@ -40,6 +40,7 @@ mixRelease rec {
// {
fast_html = prev.fast_html.override {
nativeBuildInputs = [ cmake ];
dontUseCmakeConfigure = true;
};
ex_cldr = prev.ex_cldr.overrideAttrs (old: {
# We have to use the GitHub sources, as it otherwise tries to download
@@ -146,6 +147,7 @@ mixRelease rec {
${lib.getExe nixfmt} pkgs/servers/mobilizon/mix.nix
'';
elixirPackage = beamPackages.elixir;
inherit mixNixDeps;
};
meta = {