beamPackages.mixRelease: refactor phases into hooks (#563431)

This commit is contained in:
Adam C. Stephens
2026-09-28 17:46:27 +00:00
committed by GitHub
13 changed files with 267 additions and 163 deletions

View File

@@ -28,7 +28,6 @@ beamPackages.mixRelease rec {
nativeBuildInputs = [
git
cmake
];
mixNixDeps = import ./mix.nix {

View File

@@ -82,9 +82,14 @@ makeScopeWithSplicing' {
inherit (pkgs.callPackages ./hooks { })
beamCopySourceHook
beamModuleInstallHook
mixAppConfigPatchHook
mixBuildDirHook
mixCompileHook
mixAppConfigPatchHook
mixDepsCompileHook
mixEscriptSetupHook
mixFodDepsSetupHook
mixNixDepsSetupHook
mixReleaseSetupHook
rebar3CompileHook
rebarDevendorPatchHook
;

View File

@@ -13,4 +13,6 @@ beamCopySourceHook() {
echo "Finished beamCopySourceHook"
}
postPatchHooks+=(beamCopySourceHook)
if [ -z "${dontBeamCopySource-}" ]; then
postPatchHooks+=(beamCopySourceHook)
fi

View File

@@ -13,6 +13,11 @@
meta.license = lib.licenses.mit;
} ./beam-module-install-hook.sh;
mixAppConfigPatchHook = makeSetupHook {
name = "mix-config-patch-hook.sh";
meta.license = lib.licenses.mit;
} ./mix-app-config-patch-hook.sh;
mixBuildDirHook = makeSetupHook {
name = "mix-configure-hook.sh";
meta.license = lib.licenses.mit;
@@ -23,10 +28,30 @@
meta.license = lib.licenses.mit;
} ./mix-compile-hook.sh;
mixAppConfigPatchHook = makeSetupHook {
name = "mix-config-patch-hook.sh";
mixDepsCompileHook = makeSetupHook {
name = "mix-deps-compile-hook.sh";
meta.license = lib.licenses.mit;
} ./mix-app-config-patch-hook.sh;
} ./mix-deps-compile-hook.sh;
mixEscriptSetupHook = makeSetupHook {
name = "mix-escript-setup-hook.sh";
meta.license = lib.licenses.mit;
} ./mix-escript-setup-hook.sh;
mixFodDepsSetupHook = makeSetupHook {
name = "mix-fod-deps-setup-hook";
meta.license = lib.licenses.mit;
} ./mix-fod-deps-setup-hook.sh;
mixNixDepsSetupHook = makeSetupHook {
name = "mix-nix-deps-setup-hook";
meta.license = lib.licenses.mit;
} ./mix-nix-deps-setup-hook.sh;
mixReleaseSetupHook = makeSetupHook {
name = "mix-release-setup-hook.sh";
meta.license = lib.licenses.mit;
} ./mix-release-setup-hook.sh;
rebar3CompileHook = makeSetupHook {
name = "rebar3-compile-hook.sh";

View File

@@ -16,4 +16,6 @@ mixAppConfigPatchHook() {
echo "Finished mixAppConfigPatchHook"
}
prePatchHooks+=(mixAppConfigPatchHook)
if [ -z "${dontMixAppConfigPatch-}" ]; then
prePatchHooks+=(mixAppConfigPatchHook)
fi

View File

@@ -5,9 +5,13 @@
mixBuildDirHook() {
echo "Executing mixBuildDirHook"
local lib dir dest build_dir
mkdir -p _build/"$MIX_BUILD_PREFIX"/lib
while read -r -d ':' lib; do
while IFS= read -r -d ':' lib; do
[ -n "$lib" ] || continue
for dir in "$lib"/*; do
[ -d "$dir" ] || continue
# 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)

View File

@@ -0,0 +1,19 @@
# shellcheck shell=bash
#
# 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.
#
# Phoenix projects for example will need compile.phoenix.
mixDepsCompileHook() {
echo "Executing mixDepsCompileHook"
mix deps.compile --no-deps-check --skip-umbrella-children
echo "Finished mixDepsCompileHook"
}
if [ -z "${dontMixDepsCompile-}" ]; then
postConfigureHooks+=(mixDepsCompileHook)
fi

View File

@@ -0,0 +1,30 @@
# shellcheck shell=bash
mixEscriptBuild() {
echo "Executing mixEscriptBuild"
mix escript.build --no-deps-check
echo "Finished mixEscriptBuild"
}
mixEscriptInstall() {
echo "Executing mixEscriptInstall"
runHook preInstall
mkdir -p "$out/bin"
cp "$escriptBinName" "$out/bin"
runHook postInstall
echo "Finished mixEscriptInstall"
}
if [ -z "${dontMixEscriptBuild-}" ]; then
postBuildHooks+=(mixEscriptBuild)
fi
if [ -z "${dontMixEscriptInstall-}" ] && [ -z "${installPhase-}" ]; then
installPhase=mixEscriptInstall
fi

View File

@@ -0,0 +1,29 @@
# shellcheck shell=bash
mixFodDepsPostUnpack() {
echo "Executing mixFodDepsPostUnpack"
if [ -n "${mixFodDeps-}" ]; then
# Compilation of the dependencies will require that the dependency path is
# writable, thus a copy to the $TEMPDIR is inevitable here.
export MIX_DEPS_PATH="$TEMPDIR/deps"
cp --no-preserve=mode -R "${mixFodDeps}" "$MIX_DEPS_PATH"
fi
echo "Finished mixFodDepsPostUnpack"
}
mixFodDepsPostConfigure() {
echo "Executing mixFodDepsPostConfigure"
if [ -n "${mixFodDeps-}" ]; then
# Symlink deps to build root. Allows for mixFodDeps Phoenix projects to
# find javascript assets.
ln -s "$MIX_DEPS_PATH" ./deps
fi
echo "Finished mixFodDepsPostConfigure"
}
postUnpackHooks+=(mixFodDepsPostUnpack)
postConfigureHooks+=(mixFodDepsPostConfigure)

View File

@@ -0,0 +1,21 @@
# shellcheck shell=bash
#
# Symlink dependency sources. This is needed for projects that require
# access to the source of their dependencies. For example, Phoenix
# projects need javascript assets to build asset bundles.
mixNixDepsPostConfigure() {
echo "Executing mixNixDepsPostConfigure"
local name
for name in "${!mixNixDeps[@]}"; do
if [ -d "${mixNixDeps[$name]}/src" ]; then
mkdir -p deps
ln -sv "${mixNixDeps[$name]}/src" "deps/$name"
fi
done
echo "Finished mixNixDepsPostConfigure"
}
postConfigureHooks+=(mixNixDepsPostConfigure)

View File

@@ -0,0 +1,91 @@
# shellcheck shell=bash
mixReleaseInstallHook() {
echo "Executing mixReleaseInstallHook"
runHook preInstall
mix release ${mixReleaseName:+"$mixReleaseName"} --no-deps-check --path "$out"
runHook postInstall
echo "Finished mixReleaseInstallHook"
}
mixReleaseFixupHook() {
echo "Executing mixReleaseFixupHook"
echo "Removing files for Microsoft Windows"
rm -f "$out"/bin/*.bat
echo "Wrapping programs in $out/bin with their runtime deps"
find "$out/bin/" -type f -executable | while read f; do
wrapProgram "$f" --prefix PATH : "$mixReleaseRuntimePath"
done
# shellcheck disable=SC2144 # this will only work for a single erts
if [ -e "$out"/erts-* ]; then
mixReleaseRemoveErlangReferences
fi
echo "Finished mixReleaseFixupHook"
}
mixReleaseRemoveErlangReferences() {
# ERTS is included in the release, then erlang is not required as a runtime dependency.
# But, erlang is still referenced in some places. To removed references to erlang,
# following steps are required.
echo "Removing references to erlang"
# 1. remove references to erlang from plain text files
for file in $(rg "${erlang}/lib/erlang" "$out" --files-with-matches); do
substituteInPlace "$file" --replace "${erlang}/lib/erlang" "$out"
done
# 2. remove references to erlang from .beam files
#
# No need to do anything, because it has been handled by "deterministic" option specified
# by ERL_COMPILER_OPTIONS.
# 3. remove references to erlang from normal binary files
for file in $(rg "${erlang}/lib/erlang" "$out" --files-with-matches --binary --iglob '!*.beam'); do
echo "removing references to erlang in $file"
# use bbe to substitute strings in binary files, because using substituteInPlace
# on binaries will raise errors
bbe -e "s|${erlang}/lib/erlang|$out|" -o "$file".tmp "$file"
rm -f "$file"
mv "$file".tmp "$file"
done
# References to erlang should be removed from output after above processing.
}
mixReleaseRemoveCookieHook() {
if [ -e "$out/releases/COOKIE" ]; then
echo "Removing $out/releases/COOKIE"
rm "$out/releases/COOKIE"
fi
}
mixReleaseStripDebugHook() {
# Strip debug symbols to avoid hardreferences to "foreign" closures actually
# not needed at runtime, while at the same time reduce size of BEAM files.
erl -noinput -eval 'lists:foreach(fun(F) -> io:format("Stripping ~p.~n", [F]), beam_lib:strip(F) end, filelib:wildcard("'"$out"'/**/*.beam"))' -s init stop
}
if [ -z "${dontMixReleaseFixup-}" ]; then
preFixupHooks+=(mixReleaseFixupHook)
fi
if [ -n "${removeCookie-1}" ]; then
preFixupHooks+=(mixReleaseRemoveCookieHook)
fi
if [ -n "${stripDebug-}" ]; then
postFixupHooks+=(mixReleaseStripDebugHook)
fi
if [ -z "${dontMixReleaseInstall-}" ] && [ -z "${installPhase-}" ]; then
installPhase=mixReleaseInstallHook
fi

View File

@@ -10,4 +10,6 @@ rebarDevendorPatchHook() {
echo "Finished rebarDevendorPatchHook"
}
prePatchHooks+=(rebarDevendorPatchHook)
if [ -z "${dontRebarDevendorPatch-}" ]; then
prePatchHooks+=(rebarDevendorPatchHook)
fi

View File

@@ -12,11 +12,18 @@
ripgrep,
bbe,
makeWrapper,
writableTmpDirAsHomeHook,
coreutils,
gnused,
gnugrep,
gawk,
mixBuildDirHook,
mixCompileHook,
mixDepsCompileHook,
mixEscriptSetupHook,
mixFodDepsSetupHook,
mixNixDepsSetupHook,
mixReleaseSetupHook,
}@inputs:
lib.extendMkDerivation {
@@ -25,7 +32,6 @@ lib.extendMkDerivation {
excludeDrvArgNames = [
"compileFlags"
"erlangCompilerOptions"
"mixNixDeps"
];
extendDrvArgs =
@@ -41,6 +47,7 @@ lib.extendMkDerivation {
mixEnv ? "prod",
mixTarget ? "host",
compileFlags ? [ ],
mixCompileFlags ? compileFlags,
# Build a particular named release.
# see https://hexdocs.pm/mix/1.12/Mix.Tasks.Release.html#content
mixReleaseName ? "",
@@ -106,20 +113,22 @@ lib.extendMkDerivation {
erlang
elixir
hex
git
mixBuildDirHook
]
++
# Mix deps
(builtins.attrValues mixNixDeps)
++
# other compile-time deps
[
findutils
ripgrep
bbe
findutils
git
makeWrapper
];
ripgrep
writableTmpDirAsHomeHook
mixBuildDirHook
mixCompileHook
mixDepsCompileHook
]
++ lib.optionals (escriptBinName != null) [ mixEscriptSetupHook ]
++ lib.optionals (escriptBinName == null) [ mixReleaseSetupHook ]
++ lib.optionals (mixFodDeps != null) [ mixFodDepsSetupHook ]
++ lib.optionals (mixNixDeps != { }) ([ mixNixDepsSetupHook ] ++ (builtins.attrValues mixNixDeps));
buildInputs = [
bashNonInteractive
@@ -158,147 +167,13 @@ lib.extendMkDerivation {
}
// (attrs.env or { });
postUnpack = ''
# Mix and Hex
export MIX_HOME="$TEMPDIR/mix"
export HEX_HOME="$TEMPDIR/hex"
inherit erlang;
# Rebar
export REBAR_GLOBAL_CONFIG_DIR="$TEMPDIR/rebar3"
export REBAR_CACHE_DIR="$TEMPDIR/rebar3.cache"
${lib.optionalString (mixFodDeps != null) ''
# Compilation of the dependencies will require that the dependency path is
# writable, thus a copy to the $TEMPDIR is inevitable here.
export MIX_DEPS_PATH="$TEMPDIR/deps"
cp --no-preserve=mode -R "${mixFodDeps}" "$MIX_DEPS_PATH"
''}
''
+ (attrs.postUnpack or "");
configurePhase =
attrs.configurePhase or ''
runHook preConfigure
# 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.
#
# Phoenix projects for example will need compile.phoenix.
mix deps.compile --no-deps-check --skip-umbrella-children
# Symlink dependency sources. This is needed for projects that require
# access to the source of their dependencies. For example, Phoenix
# projects need javascript assets to build asset bundles.
${lib.optionalString (mixNixDeps != { }) ''
mkdir -p deps
${lib.concatMapAttrsStringSep "\n" (name: dep: ''
dep_path="deps/${name}"
if [ -d "${dep}/src" ]; then
ln -sv ${dep}/src $dep_path
fi
'') mixNixDeps}
''}
# Symlink deps to build root. Similar to above, but allows for mixFodDeps
# Phoenix projects to find javascript assets.
${lib.optionalString (mixFodDeps != null) ''
ln -s "$MIX_DEPS_PATH" ./deps
''}
runHook postConfigure
'';
buildPhase =
attrs.buildPhase or ''
runHook preBuild
mix compile --no-deps-check ${lib.concatStringsSep " " compileFlags}
${lib.optionalString (escriptBinName != null) ''
mix escript.build --no-deps-check
''}
runHook postBuild
'';
installPhase =
attrs.installPhase or ''
runHook preInstall
${
if (escriptBinName != null) then
''
mkdir -p $out/bin
cp ${escriptBinName} $out/bin
''
else
''
mix release ${mixReleaseName} --no-deps-check --path "$out"
''
}
runHook postInstall
'';
postFixup = ''
echo "removing files for Microsoft Windows"
rm -f "$out"/bin/*.bat
echo "wrapping programs in $out/bin with their runtime deps"
for f in $(find $out/bin/ -type f -executable); do
wrapProgram "$f" \
--prefix PATH : ${
lib.makeBinPath [
coreutils
gnused
gnugrep
gawk
]
}
done
''
+ lib.optionalString removeCookie ''
if [ -e $out/releases/COOKIE ]; then
echo "removing $out/releases/COOKIE"
rm $out/releases/COOKIE
fi
''
+ ''
if [ -e $out/erts-* ]; then
# ERTS is included in the release, then erlang is not required as a runtime dependency.
# But, erlang is still referenced in some places. To removed references to erlang,
# following steps are required.
# 1. remove references to erlang from plain text files
for file in $(rg "${erlang}/lib/erlang" "$out" --files-with-matches); do
echo "removing references to erlang in $file"
substituteInPlace "$file" --replace "${erlang}/lib/erlang" "$out"
done
# 2. remove references to erlang from .beam files
#
# No need to do anything, because it has been handled by "deterministic" option specified
# by ERL_COMPILER_OPTIONS.
# 3. remove references to erlang from normal binary files
for file in $(rg "${erlang}/lib/erlang" "$out" --files-with-matches --binary --iglob '!*.beam'); do
echo "removing references to erlang in $file"
# use bbe to substitute strings in binary files, because using substituteInPlace
# on binaries will raise errors
bbe -e "s|${erlang}/lib/erlang|$out|" -o "$file".tmp "$file"
rm -f "$file"
mv "$file".tmp "$file"
done
# References to erlang should be removed from output after above processing.
fi
''
+ lib.optionalString stripDebug ''
# Strip debug symbols to avoid hardreferences to "foreign" closures actually
# not needed at runtime, while at the same time reduce size of BEAM files.
erl -noinput -eval 'lists:foreach(fun(F) -> io:format("Stripping ~p.~n", [F]), beam_lib:strip(F) end, filelib:wildcard("'"$out"'/**/*.beam"))' -s init stop
'';
mixReleaseRuntimePath = lib.makeBinPath [
coreutils
gnused
gnugrep
gawk
];
};
}