├── rebar.lock ├── .gitignore ├── rebar.config ├── src ├── reup_app.erl ├── reup_sup.erl ├── reup.app.src └── reup_watcher.erl ├── README.md └── priv └── reup-watcher.sh /rebar.lock: -------------------------------------------------------------------------------- 1 | []. 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | _build 3 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {plugins, [rebar3_hex]}. 2 | {erl_opts, [debug_info]}. 3 | -------------------------------------------------------------------------------- /src/reup_app.erl: -------------------------------------------------------------------------------- 1 | -module(reup_app). 2 | -behaviour(application). 3 | 4 | -export([start/0]). 5 | -export([start/2]). 6 | -export([stop/1]). 7 | 8 | start() -> 9 | application:start(reup). 10 | 11 | start(_Type, _Args) -> 12 | reup_sup:start_link(). 13 | 14 | stop(_State) -> 15 | ok. 16 | -------------------------------------------------------------------------------- /src/reup_sup.erl: -------------------------------------------------------------------------------- 1 | -module(reup_sup). 2 | -behaviour(supervisor). 3 | 4 | -define(CHILD(I, Type, Args), {I, {I, start_link, Args}, permanent, 5000, Type, [I]}). 5 | 6 | -export([start_link/0]). 7 | -export([init/1]). 8 | 9 | start_link() -> 10 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 11 | 12 | init([]) -> 13 | Procs = [?CHILD(reup_watcher, worker, [])], 14 | {ok, {{one_for_one, 100, 1}, Procs}}. 15 | -------------------------------------------------------------------------------- /src/reup.app.src: -------------------------------------------------------------------------------- 1 | {application, reup, [ 2 | {description, "dev watcher that auto compiles and reloads modules"}, 3 | {vsn, "0.1.1"}, 4 | {modules, []}, 5 | {registered, [reup_sup]}, 6 | {applications, [ 7 | kernel, 8 | stdlib 9 | ]}, 10 | {mod, {reup_app, []}}, 11 | {env, []}, 12 | {contributors, ["Richard Jones "]}, 13 | {licenses, ["Apache"]}, 14 | {links, [{"github.com/RJ/erlang-reup", "https://github.com/RJ/erlang-reup"}]} 15 | ]}. 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## the erlang re-up 2 | 3 | **Watches** your erlang source/header files for changes, **recompiles** the 4 | changed modules using the same compiler options as the last compile, and 5 | **reloads** them. 6 | 7 | Conceptually similar to [sync](https://github.com/rustyio/sync) and [active](https://github.com/synrc/active), but works happily on NFS or vbox shared folders. Stateless, because we watch the filesystem by 8 | polling with find, so can handle enormous erlang projects just fine. 9 | 10 | Because it reuses the compile options from last time (provided you 11 | compile with `debug_info`), it will Just Work, regardless of what you use to build your project (rebar2, rebar3, make, etc). 12 | 13 | ### Building 14 | 15 | Add this application as a dev-only dependency to your project. If you're 16 | using rebar3, which I recommend, this should be easy. 17 | 18 | Something, something.. 19 | 20 | application:start(reup). 21 | 22 | ### Options 23 | 24 | If you want reup to compile but not load the modules, set application env: 25 | 26 | `{reload_on_compile, false}` 27 | 28 | 29 | ### Implementation Notes 30 | 31 | Has to work on virtualbox with crappy shared folders, so doesn't use 32 | inotify or fswatcher stuff. Known to work on Ubuntu Linux & OS X. 33 | 34 | The script in `priv/` polls for src changes using some shell gubbins, 35 | and emits changed filenames. 36 | 37 | If a `*.hrl` file changes, it naively emits the filenames of `*.erl` 38 | files that match a grep for the header filename, which tends to work. 39 | -------------------------------------------------------------------------------- /priv/reup-watcher.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | DIR="$1" 3 | if [ ! -d "$DIR" ] 4 | then 5 | >&2 echo "reup error: can't watch missing dir $DIR" 6 | exit 1 7 | fi 8 | # we touch this file, then ask find to find files modified more recently 9 | # two files are used to avoid a touch->(save .erl)->find race condition 10 | MARKER=$(mktemp -t reup.tmp.XXXXXXXXXX) 11 | a=0 12 | b=1 13 | touch "${MARKER}.$a" 14 | touch "${MARKER}.$b" 15 | 16 | function cleanup() { 17 | rm -f "${MARKER}.0" 18 | rm -f "${MARKER}.1" 19 | } 20 | trap cleanup EXIT 21 | 22 | # loop is pumped by erlang process writing to stdin 23 | # so we can control polling speed from erlang without overlapping under load 24 | # 60 second timeout on read, in case erlang exits uncleanly. 25 | while read -t 60 line 26 | do 27 | if [ "$line" = "exit" ] || [ -z "$line" ] 28 | then 29 | exit 0 30 | fi 31 | 32 | if [ "$line" != "pump" ] 33 | then 34 | >&2 echo "reup invalid input line: $line" 35 | exit 2 36 | fi 37 | 38 | touch "${MARKER}.$a" 39 | find "$DIR" -newer "${MARKER}.$b" -type f | while read f 40 | do 41 | if [[ "$f" == *hrl ]]; then 42 | # if a .hrl file is changed, emit all .erl files that mention it 43 | grep -nr "$(basename "$f")\"" "$DIR" | awk -F ':' '{print $1}' 44 | elif [[ "$f" == *erl ]]; then 45 | # else just compile the erl file 46 | echo "$f" 47 | fi 48 | done 49 | # swap i & ii 50 | tmp="$a" 51 | a="$b" 52 | b="$tmp" 53 | # tell erlang we're done, so it can enqueue the next poll 54 | echo "ok" 55 | done 56 | -------------------------------------------------------------------------------- /src/reup_watcher.erl: -------------------------------------------------------------------------------- 1 | -module(reup_watcher). 2 | -behaviour(gen_server). 3 | 4 | -define(POLL_INTERVAL, 2000). 5 | 6 | -define(LOG(S,F), io:format("[reup] " ++ S ++ "\n", F)). 7 | 8 | %% API. 9 | -export([start_link/0, reup_module/1]). 10 | 11 | %% gen_server. 12 | -export([init/1]). 13 | -export([handle_call/3]). 14 | -export([handle_cast/2]). 15 | -export([handle_info/2]). 16 | -export([terminate/2]). 17 | -export([code_change/3]). 18 | 19 | -record(state, { 20 | port 21 | }). 22 | 23 | start_link() -> 24 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 25 | 26 | env(K, Def) -> application:get_env(reup, K, Def). 27 | 28 | %% interval for running find, to check for modified src files 29 | poll_interval() -> env(poll_interval, 2000). 30 | 31 | %% path to watch for src changes, relative to $CWD when erl started 32 | src_dir() -> 33 | case env(src_dir, undefined) of 34 | undefined -> default_src_dir(); 35 | Dir -> Dir 36 | end. 37 | 38 | default_src_dir() -> 39 | {ok, CWD} = file:get_cwd(), 40 | Guesses = [ 41 | %% assuming rebar3 _build//rel/ structure 42 | CWD ++ "/../../../../apps", 43 | %% assuming script from project root, with apps 44 | "./apps", 45 | %% assuming script from project root, with single-otp project 46 | "./src", 47 | CWD 48 | ], 49 | first_valid_dir(Guesses). 50 | 51 | first_valid_dir([]) -> undefined; 52 | first_valid_dir([Dir|Rest]) -> 53 | case filelib:is_dir(Dir) of 54 | true -> Dir; 55 | false -> first_valid_dir(Rest) 56 | end. 57 | 58 | 59 | %% gen_server. 60 | 61 | init([]) -> 62 | SrcDir = src_dir(), 63 | ?LOG("watching for source changes in ~s",[SrcDir]), 64 | Exe = code:priv_dir(reup) ++ "/reup-watcher.sh", 65 | Port = open_port({spawn_executable, Exe}, [ 66 | {args, [SrcDir]}, 67 | {line, 2048}, 68 | use_stdio 69 | ]), 70 | true = link(Port), 71 | self() ! pump, 72 | {ok, #state{port=Port}}. 73 | 74 | handle_call(_Request, _From, State) -> 75 | {reply, ignored, State}. 76 | 77 | handle_cast(Info, State) -> 78 | ?LOG("unhandled cast: ~p",[Info]), 79 | {noreply, State}. 80 | 81 | handle_info(pump, State = #state{port=Port}) -> 82 | erlang:port_command(Port, "pump\n"), 83 | {noreply, State}; 84 | 85 | handle_info({Port, {data, {eol, "ok"}}}, State = #state{port=Port}) -> 86 | erlang:send_after(poll_interval(), self(), pump), 87 | {noreply, State}; 88 | 89 | handle_info({Port, {data, {eol, Line}}}, State = #state{port=Port}) -> 90 | Mod = list_to_atom(filename:basename(Line, ".erl")), 91 | reup_module(Mod), 92 | {noreply, State}; 93 | 94 | handle_info({'EXIT', Port, Reason}, State = #state{port=Port}) -> 95 | ?LOG("port exit ~p", [Reason]), 96 | {stop, {port_exit, Reason}, State}; 97 | 98 | handle_info(Info, State) -> 99 | ?LOG("unhandled reup info: ~p",[Info]), 100 | {noreply, State}. 101 | 102 | terminate(_Reason, #state{port=Port}) -> 103 | (catch port_command(Port, "exit\n")), 104 | (catch port_close(Port)), 105 | ok. 106 | 107 | code_change(_OldVsn, State, _Extra) -> 108 | {ok, State}. 109 | 110 | %% 111 | 112 | mod_info(M) when is_atom(M) -> 113 | %% does this always work? 114 | %% can easily get this info from module_info() if debug_info used. 115 | case code:load_file(M) of 116 | {error, nofile} -> 117 | nofile; 118 | {error, not_purged} -> 119 | loaded_mod_info(M); 120 | {module, M} -> 121 | loaded_mod_info(M) 122 | end. 123 | 124 | loaded_mod_info(M) when is_atom(M) -> 125 | case filename:find_src(M) of 126 | {error, {not_existing, _}} -> 127 | not_existing; 128 | {Src, Opts} -> 129 | {Src, Opts} 130 | end. 131 | 132 | reup_module(M) when is_atom(M) -> 133 | case mod_info(M) of 134 | nofile -> 135 | ?LOG("~s - nofile",[M]), 136 | nofile; 137 | not_existing -> 138 | ?LOG("~s - not_existing",[M]), 139 | not_existing; 140 | {Src, Opts0} -> 141 | ?LOG("~s - compiling...",[M]), 142 | OutDir = filename:dirname(code:which(M)), 143 | Opts = [{outdir, OutDir} | Opts0], 144 | case compile:file(Src, Opts) of 145 | error -> 146 | ?LOG("ERROR: ~s\n~p",[Src,Opts]), 147 | error; 148 | {ok, M} -> 149 | case application:get_env(reup, reload_on_compile, true) of 150 | true -> 151 | code:purge(M), 152 | case code:load_file(M) of 153 | {error, Err} -> 154 | ?LOG("reloaded ERROR -> ~p: ~s",[Err, M]), 155 | nofile; 156 | {module, M} -> 157 | ?LOG("reloaded: ~s",[M]), 158 | maybe_run_tests(M) 159 | end; 160 | _ -> 161 | ok 162 | end 163 | end 164 | end. 165 | 166 | maybe_run_tests(M) when is_atom(M) -> 167 | case erlang:function_exported(M, test, 0) of 168 | true -> 169 | ?LOG("~s:test() ...", [M]), 170 | try M:test() of 171 | ok -> 172 | ?LOG("~s:test() ... PASSED", [M]), 173 | reloaded_test_pass 174 | catch 175 | Reason -> 176 | ?LOG("~s:test() ... FAILED ~p", [M, Reason]), 177 | reloaded_test_fail 178 | end; 179 | false -> 180 | reloaded_no_test 181 | end. 182 | --------------------------------------------------------------------------------