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-define(LOCAL_HEX_REGISTRY, "registry.ets").
30
31main(Args) ->
32 {ok, RequiredData} = gather_required_data_from_the_environment(Args),
33 ok = bootstrap_libs(RequiredData).
34
35%% @doc
36%% This takes an app name in the standard OTP <name>-<version> format
37%% and returns just the app name. Why? Because rebar doesn't
38%% respect OTP conventions in some cases.
39-spec fixup_app_name(file:name()) -> string().
40fixup_app_name(Path) ->
41 BaseName = filename:basename(Path),
42 case string:split(BaseName, "-") of
43 [Name, _Version] -> Name;
44 Name -> Name
45 end.
46
47
48-spec gather_required_data_from_the_environment([string()]) -> {ok, #data{}}.
49gather_required_data_from_the_environment(_) ->
50 {ok, #data{ version = guard_env("version")
51 , erl_libs = os:getenv("ERL_LIBS", [])
52 , root = code:root_dir()
53 , name = guard_env("name")}}.
54
55-spec guard_env(string()) -> string().
56guard_env(Name) ->
57 case os:getenv(Name) of
58 false ->
59 stderr("Expected Environment variable ~s! Are you sure you are "
60 "running in a Nix environment? Either a nix-build, "
61 "nix-shell, etc?~n", [Name]),
62 erlang:halt(1);
63 Variable ->
64 Variable
65 end.
66
67-spec bootstrap_libs(#data{}) -> ok.
68bootstrap_libs(#data{erl_libs = ErlLibs}) ->
69 io:format("Bootstrapping dependent libraries~n"),
70 Target = "_build/prod/lib/",
71 Paths = string:tokens(ErlLibs, ":"),
72 CopiableFiles =
73 lists:foldl(fun(Path, Acc) ->
74 gather_directory_contents(Path) ++ Acc
75 end, [], Paths),
76 lists:foreach(fun (Path) ->
77 ok = link_app(Path, Target)
78 end, CopiableFiles).
79
80-spec gather_directory_contents(string()) -> [{string(), string()}].
81gather_directory_contents(Path) ->
82 {ok, Names} = file:list_dir(Path),
83 lists:map(fun(AppName) ->
84 {filename:join(Path, AppName), fixup_app_name(AppName)}
85 end, Names).
86
87%% @doc
88%% Makes a symlink from the directory pointed at by Path to a
89%% directory of the same name in Target. So if we had a Path of
90%% {`foo/bar/baz/bash`, `baz`} and a Target of `faz/foo/foos`, the symlink
91%% would be `faz/foo/foos/baz`.
92-spec link_app({string(), string()}, string()) -> ok.
93link_app({Path, TargetFile}, TargetDir) ->
94 Target = filename:join(TargetDir, TargetFile),
95 ok = make_symlink(Path, Target).
96
97-spec make_symlink(string(), string()) -> ok.
98make_symlink(Path, TargetFile) ->
99 file:delete(TargetFile),
100 ok = filelib:ensure_dir(TargetFile),
101 io:format("Making symlink from ~s to ~s~n", [Path, TargetFile]),
102 ok = file:make_symlink(Path, TargetFile).
103
104%% @doc
105%% Write the result of the format string out to stderr.
106-spec stderr(string(), [term()]) -> ok.
107stderr(FormatStr, Args) ->
108 io:put_chars(standard_error, io_lib:format(FormatStr, Args)).