Current section

Files

Jump to
deps_nix lib deps_nix.ex
Raw

lib/deps_nix.ex

defmodule DepsNix do
alias DepsNix.Packages
alias DepsNix.Util
defmodule Options do
@type t :: %Options{
envs: map(),
github_prefetcher: (String.t(), String.t(), String.t() -> String.t()),
output: String.t(),
app_config_path: String.t() | nil,
include_paths: boolean(),
cwd: String.t()
}
defstruct envs: %{},
github_prefetcher: nil,
output: "deps.nix",
app_config_path: nil,
include_paths: false,
cwd: nil
end
defmodule InvalidGitHubReference do
defexception [:message]
end
@type converger :: (Keyword.t() -> list(Mix.Dep.t()))
@spec run(Options.t(), converger()) ::
{path :: String.t(), output :: String.t()}
def run(opts, converger) do
opts
|> convert_opts()
|> Enum.sort_by(fn
{[env: :test], _packages} -> 0
{[env: :dev], _packages} -> 1
{[env: :prod], _packages} -> 2
end)
|> Enum.flat_map(fn {converger_opts, permitted_names} ->
all_packages_for_env = converger.(converger_opts)
Packages.filter(all_packages_for_env, permitted_names)
|> then(fn packages ->
if opts.include_paths do
packages
else
Packages.reject_paths(packages)
end
end)
end)
|> Enum.reject(&unwanted/1)
|> Enum.sort_by(& &1.app)
|> Enum.uniq_by(& &1.app)
|> Enum.map(&DepsNix.Derivation.from(&1, opts))
|> Enum.join("\n")
|> indent_deps()
|> wrap(opts.output)
end
@spec parse_args(list()) :: Options.t()
def parse_args(args) do
args
|> OptionParser.parse(
strict: [
env: [:string, :keep],
output: :string,
app_config_path: :string,
include_paths: :boolean
]
)
|> to_opts()
end
def github_prefetcher(owner, repo, rev) do
Mix.shell().info("deps_nix: fetching hash for GitHub dependency #{owner}/#{repo}")
with dir <- System.tmp_dir() |> realpath(),
path <- "#{dir}/#{repo}-#{rev}",
{:ok, _deletions} <- File.rm_rf(path),
body <- github_archive(owner, repo, rev),
:ok <- File.write("#{dir}/deps-nix-tarball", body),
{_, 0} <- System.cmd("tar", ["-xf", "#{dir}/deps-nix-tarball"], cd: dir),
nar = ExNar.serialize!(path),
nar_hash = :crypto.hash(:sha256, nar),
hash = "sha256-" <> Base.encode64(nar_hash) do
{hash, find_builder_from_path(path)}
end
end
defp realpath(path) do
for subpath <- Path.split(path), reduce: "/" do
acc ->
new_path = Path.join(acc, subpath)
case File.read_link(new_path) do
{:ok, resolved} -> Path.join("/", resolved)
{:error, _} -> new_path
end
end
end
defp github_archive(owner, repo, rev) do
{:ok, conn} = Mint.HTTP.connect(:https, "codeload.github.com", 443)
{:ok, conn, request_ref} =
Mint.HTTP.request(conn, "GET", "/#{owner}/#{repo}/tar.gz/#{rev}", [], "")
receive_archive(conn, request_ref)
end
defp receive_archive(conn, request_ref, data \\ []) do
receive do
message ->
case Mint.HTTP.stream(conn, message) do
{:ok, conn, responses} ->
case Enum.reduce(responses, data, fn
{:status, ^request_ref, 200}, acc ->
acc
{:status, ^request_ref, 404}, _acc ->
raise InvalidGitHubReference
{:headers, ^request_ref, _headers}, acc ->
acc
{:data, ^request_ref, data}, acc ->
[data | acc]
{:done, ^request_ref}, acc ->
{:ok, acc}
end) do
{:ok, data} ->
Enum.reverse(data)
data ->
receive_archive(conn, request_ref, data)
end
end
end
end
defp find_builder_from_path(path) do
cond do
File.exists?(Path.join(path, "mix.exs")) -> "buildMix"
File.exists?(Path.join(path, "rebar.config")) -> "buildRebar3"
true -> :unknown
end
end
@spec to_opts({list(), any(), any()}) :: Options.t()
defp to_opts({[], _, _}) do
%Options{
cwd: File.cwd!(),
envs: %{"prod" => :all},
github_prefetcher: &github_prefetcher/3
}
end
defp to_opts({opts, _, _}) do
default = to_opts({[], [], []})
%{
default
| envs:
for {:env, env} <- opts, into: default.envs do
case String.split(env, "=", parts: 2) do
[env] ->
{env, :all}
[env, ""] ->
{env, []}
[env, packages] ->
{env, String.split(packages, ",")}
end
end,
include_paths: Keyword.get(opts, :include_paths, false),
app_config_path: Keyword.get(opts, :app_config_path, nil)
}
|> add_output(opts)
end
defp convert_opts(%Options{envs: envs}) do
for {strenv, packages} <- envs do
env = String.to_existing_atom(strenv)
{[env: env], packages}
end
end
defp unwanted(dep) do
dep.app != :heroicons &&
Enum.all?([:app, :compile], fn opt ->
Keyword.fetch(dep.opts, opt) == {:ok, false}
end)
end
defp add_output(%Options{} = options, parsed_args) do
case Keyword.get(parsed_args, :output) do
nil ->
options
output ->
%{options | output: output}
end
end
defp wrap(pkgs, output) do
{
output,
"""
{
lib,
beamPackages,
cmake,
extend,
lexbor,
fetchFromGitHub,
overrides ? (x: y: { }),
overrideFenixOverlay ? null,
pkg-config,
vips,
writeText,
}:
let
buildMix = lib.makeOverridable beamPackages.buildMix;
buildRebar3 = lib.makeOverridable beamPackages.buildRebar3;
workarounds = {
portCompiler = _unusedArgs: old: {
buildPlugins = [ beamPackages.pc ];
};
rustlerPrecompiled =
{
toolchain ? null,
...
}:
old:
let
extendedPkgs = extend fenixOverlay;
fenixOverlay =
if overrideFenixOverlay == null then
import "${
fetchTarball {
url = "https://github.com/nix-community/fenix/archive/6399553b7a300c77e7f07342904eb696a5b6bf9d.tar.gz";
sha256 = "sha256-C6tT7K1Lx6VsYw1BY5S3OavtapUvEnDQtmQB5DSgbCc=";
}
}/overlay.nix"
else
overrideFenixOverlay;
nativeDir = "${old.src}/native/${with builtins; head (attrNames (readDir "${old.src}/native"))}";
fenix =
if toolchain == null then
extendedPkgs.fenix.stable
else
extendedPkgs.fenix.fromToolchainName toolchain;
native =
(extendedPkgs.makeRustPlatform {
inherit (fenix) cargo rustc;
}).buildRustPackage
{
pname = "${old.packageName}-native";
version = old.version;
src = nativeDir;
cargoLock = {
lockFile = "${nativeDir}/Cargo.lock";
};
nativeBuildInputs = [
extendedPkgs.cmake
];
doCheck = false;
};
in
{
nativeBuildInputs = [ extendedPkgs.cargo ];
env.RUSTLER_PRECOMPILED_FORCE_BUILD_ALL = "true";
env.RUSTLER_PRECOMPILED_GLOBAL_CACHE_PATH = "unused-but-required";
preConfigure = ''
mkdir -p priv/native
for lib in ${native}/lib/*
do
dest="$(basename "$lib")"
if [[ "''${dest##*.}" = "dylib" ]]
then
dest="''${dest%.dylib}.so"
fi
ln -s "$lib" "priv/native/$dest"
done
'';
buildPhase = ''
suggestion() {
echo "***********************************************"
echo " deps_nix "
echo
echo " Rust dependency build failed. "
echo
echo " If you saw network errors, you might need "
echo " to disable compilation on the appropriate "
echo " RustlerPrecompiled module in your "
echo " application config. "
echo
echo " We think you need this: "
echo
echo -n " "
grep -Rl 'use RustlerPrecompiled' lib \\
| xargs grep 'defmodule' \\
| sed 's/defmodule \\(.*\\) do/config :${old.packageName}, \\1, skip_compilation?: true/'
echo "***********************************************"
exit 1
}
trap suggestion ERR
${old.buildPhase}
'';
};
elixirMake = _unusedArgs: old: {
preConfigure = ''
export ELIXIR_MAKE_CACHE_DIR="$TEMPDIR/elixir_make_cache"
'';
};
lazyHtml = _unusedArgs: old: {
preConfigure = ''
export ELIXIR_MAKE_CACHE_DIR="$TEMPDIR/elixir_make_cache"
'';
postPatch = ''
substituteInPlace mix.exs \
--replace-fail "Fine.include_dir()" '"${packages.fine}/src/c_include"' \
--replace-fail '@lexbor_git_sha "244b84956a6dc7eec293781d051354f351274c46"' '@lexbor_git_sha ""'
'';
preBuild = ''
install -Dm644 \
-t _build/c/third_party/lexbor/$LEXBOR_GIT_SHA/build \
${lexbor}/lib/liblexbor_static.a
'';
};
};
defaultOverrides = (
final: prev:
let
apps = {
crc32cer = [
{
name = "portCompiler";
}
];
explorer = [
{
name = "rustlerPrecompiled";
toolchain = {
name = "nightly-2025-06-23";
sha256 = "sha256-UAoZcxg3iWtS+2n8TFNfANFt/GmkuOMDf7QAE0fRxeA=";
};
}
];
snappyer = [
{
name = "portCompiler";
}
];
};
applyOverrides =
appName: drv:
let
allOverridesForApp = builtins.foldl' (
acc: workaround: acc // (workarounds.${workaround.name} workaround) drv
) { } apps.${appName};
in
if builtins.hasAttr appName apps then drv.override allOverridesForApp else drv;
in
builtins.mapAttrs applyOverrides prev
);
self = packages // (defaultOverrides self packages) // (overrides self packages);
packages =
with beamPackages;
with self;
{
#{pkgs}
};
in
self
"""
}
end
defp indent_deps("") do
" "
end
defp indent_deps(s) do
"\n#{s}"
|> Util.indent(from: 1)
|> Util.indent(from: 1)
|> Util.indent(from: 1)
end
end