1#!/usr/bin/env escript
2%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*-
3%%! -smp enable
4%%% ---------------------------------------------------------------------------
5%%% @doc
6%%% The purpose of this command is to prepare a mix project so that mix
7%%% understands that the dependencies are all already installed. If you want a
8%%% hygienic build on nix then you must run this command before running mix. I
9%%% suggest that you add a `Makefile` to your project and have the bootstrap
10%%% command be a dependency of the build commands. See the nix documentation for
11%%% more information.
12%%%
13%%% This command designed to have as few dependencies as possible so that it can
14%%% be a dependency of root level packages like mix. To that end it does many
15%%% things in a fairly simplistic way. That is by design.
16%%%
17%%% ### Assumptions
18%%%
19%%% This command makes the following assumptions:
20%%%
21%%% * It is run in a nix-shell or nix-build environment
22%%% * that all dependencies have been added to the ERL_LIBS
23%%% Environment Variable
24
25-record(data, {version
26 , erl_libs
27 , root
28 , name
29 , registry_snapshot}).
30-define(LOCAL_HEX_REGISTRY, "registry.ets").
31
32main(Args) ->
33 {ok, RequiredData} = gather_required_data_from_the_environment(Args),
34 ok = bootstrap_libs(RequiredData).
35
36%% @doc
37%% This takes an app name in the standard OTP <name>-<version> format
38%% and returns just the app name. Why? Because rebar doesn't
39%% respect OTP conventions in some cases.
40-spec fixup_app_name(file:name()) -> string().
41fixup_app_name(Path) ->
42 BaseName = filename:basename(Path),
43 case string:tokens(BaseName, "-") of
44 [Name, _Version] -> Name;
45 Name -> Name
46 end.
47
48
49-spec gather_required_data_from_the_environment([string()]) -> {ok, #data{}}.
50gather_required_data_from_the_environment(_) ->
51 {ok, #data{ version = guard_env("version")
52 , erl_libs = os:getenv("ERL_LIBS", [])
53 , root = code:root_dir()
54 , name = guard_env("name")
55 , registry_snapshot = guard_env("HEX_REGISTRY_SNAPSHOT")}}.
56
57-spec guard_env(string()) -> string().
58guard_env(Name) ->
59 case os:getenv(Name) of
60 false ->
61 stderr("Expected Environment variable ~s! Are you sure you are "
62 "running in a Nix environment? Either a nix-build, "
63 "nix-shell, etc?~n", [Name]),
64 erlang:halt(1);
65 Variable ->
66 Variable
67 end.
68
69-spec bootstrap_libs(#data{}) -> ok.
70bootstrap_libs(#data{erl_libs = ErlLibs}) ->
71 io:format("Bootstrapping dependent libraries~n"),
72 Target = "_build/prod/lib/",
73 Paths = string:tokens(ErlLibs, ":"),
74 CopiableFiles =
75 lists:foldl(fun(Path, Acc) ->
76 gather_directory_contents(Path) ++ Acc
77 end, [], Paths),
78 lists:foreach(fun (Path) ->
79 ok = link_app(Path, Target)
80 end, CopiableFiles).
81
82-spec gather_directory_contents(string()) -> [{string(), string()}].
83gather_directory_contents(Path) ->
84 {ok, Names} = file:list_dir(Path),
85 lists:map(fun(AppName) ->
86 {filename:join(Path, AppName), fixup_app_name(AppName)}
87 end, Names).
88
89%% @doc
90%% Makes a symlink from the directory pointed at by Path to a
91%% directory of the same name in Target. So if we had a Path of
92%% {`foo/bar/baz/bash`, `baz`} and a Target of `faz/foo/foos`, the symlink
93%% would be `faz/foo/foos/baz`.
94-spec link_app({string(), string()}, string()) -> ok.
95link_app({Path, TargetFile}, TargetDir) ->
96 Target = filename:join(TargetDir, TargetFile),
97 ok = make_symlink(Path, Target).
98
99-spec make_symlink(string(), string()) -> ok.
100make_symlink(Path, TargetFile) ->
101 file:delete(TargetFile),
102 ok = filelib:ensure_dir(TargetFile),
103 io:format("Making symlink from ~s to ~s~n", [Path, TargetFile]),
104 ok = file:make_symlink(Path, TargetFile).
105
106%% @doc
107%% Write the result of the format string out to stderr.
108-spec stderr(string(), [term()]) -> ok.
109stderr(FormatStr, Args) ->
110 io:put_chars(standard_error, io_lib:format(FormatStr, Args)).