Merge remote-tracking branch 'canonical/next'
[sinan.git] / src / sin_task_release.erl
blob8518a99f7ff6c2d04baab3938132ab03989f1df6
1 %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
2 %%%---------------------------------------------------------------------------
3 %%% @author Eric Merritt
4 %%% @doc
5 %%% Builds the *.script and *.boot from the project rel file.
6 %%% @end
7 %%% @copyright (C) 2006-2011 Erlware
8 %%%---------------------------------------------------------------------------
9 -module(sin_task_release).
11 -behaviour(sin_task).
13 -include_lib("sinan/include/sinan.hrl").
15 %% API
16 -export([description/0, do_task/2, format_exception/1]).
18 -define(TASK, release).
19 -define(DEPS, [build]).
21 %%====================================================================
22 %% API
23 %%====================================================================
25 %% @doc describes this task to the system
26 -spec description() -> sin_task:task_description().
27 description() ->
29 Desc = "
30 release Task
31 ============
33 This command creates all the artifacts needed to start the current
34 project as an otp release. This creates the `*.rel`, `*.boot` and `*.script`
35 files into the output area of the project.
37 Those files maybe found at: `<build-area>/<release-name>/releases/<release-vsn>`
39 To understand what those files are and why they are important you should check
40 the erlang documentation on releases.
42 Configuration Options
43 ---------------------
45 ### Application Load Types
47 This configuration allows you to specify the load type for the application
48 specified in `AppName` for the release. (Again review the release information
49 for details).
51 {types, [{AppName1::atom(), RelType::atom()},
52 {AppName2::atom(), RelType::atom()}]}.
54 ### Script Args
56 You may also add additional options to the sys_tools:make_script
57 call. The one you probably want to add most is the local option, but you
58 may add any valid option. You do this by adding the following call to
59 your sinan.config.
61 {script_args, [term()]}.
63 Not that the value for script_args must always be a list
65 ### Additional Release Dirs
67 By default the release only includes directories directly relevant to the erlang
68 otp release. It is common to want to include additional directories. You can do
69 that with the following configuration.
71 {include_dirs, [string()]}.
73 ### Including The Erts Dir
75 This is a boolean that indicates to the system whether or not you want the
76 Erlang runtime system included in the tarball. This allows you to distribute
77 the vm with your release but has the drawback of turning your tarball into a
78 platform specific thing.
80 {include_erts, boolean()}.",
82 #task{name = ?TASK,
83 task_impl = ?MODULE,
84 bare = false,
85 deps = ?DEPS,
86 example = "release",
87 short_desc = "Creates an otp release for the system",
88 desc = Desc,
89 opts = []}.
91 %% @doc create an otp release
92 -spec do_task(sin_config:matcher(), sin_state:state()) -> sin_state:state().
93 do_task(Config, State0) ->
94 BuildDir = sin_state:get_value(build_dir, State0),
95 ReleaseDir = sin_state:get_value(release_dir, State0),
96 ReleaseName = sin_state:get_value(release, State0),
97 Version = sin_state:get_value(release_vsn, State0),
98 ReleaseInfo = generate_rel_file(Config, State0, ReleaseDir,
99 ReleaseName, Version),
100 State1 = sin_state:store(rel, ReleaseInfo, State0),
101 copy_include_dirs(Config, State1, BuildDir),
102 copy_apps(Config, State1),
103 create_bin_file(State1, BuildDir,
104 ReleaseName, Version, get_erts_info()),
105 copy_or_generate_sys_config_file(Config, ReleaseDir, Version),
106 include_erts(Config, State1, BuildDir),
107 make_boot_script(State1, Config, ReleaseInfo),
108 launch_if_required(Config, BuildDir, ReleaseName, Version),
109 State1.
111 %% @doc Format an exception thrown by this module
112 -spec format_exception(sin_exceptions:exception()) ->
113 string().
114 format_exception(?SIN_EXEP_UNPARSE(_, release_script_generation_error, Description)) ->
115 Description;
116 format_exception(Exception) ->
117 sin_exceptions:format_exception(Exception).
119 %%====================================================================
120 %% Internal functions
121 %%====================================================================
123 %% @doc Generate release information from info available in the project.
124 -spec generate_rel_file(sin_config:config(), sin_state:state(), string(),
125 string(), string()) ->
126 {ReleaseFile::string(), ReleaseInfo::term()}.
127 generate_rel_file(Config, State, ReleaseDir, Name, Version) ->
128 ReleaseName = sin_state:get_value(release, State),
129 ReleaseVsn = sin_state:get_value(release_vsn, State),
131 RootDir = sin_state:get_value(project_dir, State),
133 Release =
134 case sin_release:get_release(State, RootDir, ReleaseName,
135 ReleaseVsn) of
136 no_file ->
137 Erts = get_erts_info(),
138 Deps0 =
139 process_deps(Config:match(types, []),
140 sin_state:get_value(release_runtime_deps, State) ++
141 sin_state:get_value(project_apps, State), []),
142 Deps1 = lists:map(fun({App, AppVersion}) ->
143 {App, AppVersion}
144 end, Deps0),
146 {release, {erlang:atom_to_list(Name), Version}, {erts, Erts},
147 lists:sort(Deps1)};
148 RelInfo ->
149 RelInfo
150 end,
151 {save_release(Config, State, ReleaseDir, Name, Version, Release), Release}.
153 %% @doc Process the dependencies into a format useful for the rel depends area.
154 process_deps(Types, [#app{name=App, vsn=Vsn} | T], Acc) ->
155 case lists:keyfind(App, 1, Types) of
156 {App, Type} ->
157 process_deps(Types, T, [{App, Vsn, list_to_atom(Type)} | Acc]);
158 _ ->
159 process_deps(Types, T, [{App, Vsn} | Acc])
160 end;
161 process_deps(_, [], Acc) ->
162 Acc.
164 %% @doc Save the release terms to a releases file for later use by the system.
165 -spec save_release(sin_config:config(), sin_state:state(), string(),
166 string(), string(), term()) ->
167 {Location::string(), RelBase::string()}.
168 save_release(Config, State, RelDir, Name, Version, RelInfo) ->
169 Location = filename:join(RelDir, Version),
170 filelib:ensure_dir(filename:join([Location, "tmp"])),
171 Relbase = filename:join([Location, Name]),
172 Relf = lists:flatten([Relbase, ".rel"]),
173 case file:open(Relf, write) of
174 {error, _} ->
175 sin_log:normal(Config, "Couldn't open ~s for writing. Unable to "
176 "write release information",
177 [Relf]),
178 ?SIN_RAISE(State,
179 unable_to_write_rel_info);
180 {ok, IoDev} ->
181 io:format(IoDev, "~p.", [RelInfo]),
182 file:close(IoDev)
183 end,
184 {Location, Relbase}.
186 %% @doc Get the system erts version.
187 -spec get_erts_info() -> ErtsVersion::string().
188 get_erts_info() ->
189 erlang:system_info(version).
191 %% @doc Gather up the path information and make the boot/script files.
192 -spec make_boot_script(sin_state:state(), sin_config:config(), {{string(), string()}, term()}) ->
194 make_boot_script(State, Config, {{Location, File}, {release, {Name, _}, _, _}}) ->
195 Options = [{path, [Location | get_code_paths(State)]},
196 no_module_tests, silent] ++ Config:match(script_args, []),
197 case make_script(Name, File, Location, Options) of
198 ok ->
200 error ->
201 ?SIN_RAISE(State, release_script_generation_error);
202 {ok, _, []} ->
204 {ok,Module,Warnings} ->
205 Detail = lists:flatten(Module:format_warning(Warnings)),
206 ?SIN_RAISE(State, release_script_generation_error,
207 "~s~n", [Detail]);
208 {error,Module,Error} ->
209 Detail = lists:flatten(Module:format_error(Error)),
210 ?SIN_RAISE(State, release_script_generation_error,
211 "~s~n", [Detail])
212 end.
214 -spec make_script(string(), string(), string(), [term()]) ->
215 ok | error | {ok, term(), []} |
216 {ok, atom(), [term()]} |
217 {ok, atom(), [term()]}.
218 make_script(Name, File, Location, Options) ->
219 %% Erts 5.9 introduced a non backwards compatible option to
220 %% erlang this takes that into account
221 Erts = erlang:system_info(version),
222 case Erts == "5.9" orelse ec_string:compare_versions(Erts, "5.9") of
223 true ->
224 systools_make:make_script(Name,
225 File, [no_warn_sasl,
226 {outdir, Location} | Options]);
227 _ ->
228 systools_make:make_script(Name,
229 File, [{outdir, Location} | Options])
230 end.
232 %% @doc copy config/sys.config or generate one to releases/VSN/sys.config
233 -spec copy_or_generate_sys_config_file(sin_config:config(), string(),
234 string()) ->
236 copy_or_generate_sys_config_file(Config, RelDir, Version) ->
237 RelSysConfPath = filename:join([RelDir, Version, "sys.config"]),
238 case Config:match(config_dir, undefined) of
239 undefined ->
240 generate_sys_config_file(RelSysConfPath);
241 ConfigDir ->
242 ConfSysConfPath = filename:join([ConfigDir, "sys.config"]),
243 case filelib:is_regular(ConfSysConfPath) of
244 false ->
245 generate_sys_config_file(RelSysConfPath);
246 true ->
247 file:copy(ConfSysConfPath, RelSysConfPath)
249 end.
251 %% @doc write a generic sys.config to the path RelSysConfPath
252 -spec generate_sys_config_file(string()) -> ok.
253 generate_sys_config_file(RelSysConfPath) ->
254 {ok, Fd} = file:open(RelSysConfPath, [write]),
255 io:format(Fd,
256 "%% Thanks to Ulf Wiger at Ericcson for these comments:~n"
257 "%%~n"
258 "%% This file is identified via the erl command line option -config File.~n"
259 "%% Note that File should have no extension, e.g.~n"
260 "%% erl -config .../sys (if this file is called sys.config)~n"
261 "%%~n"
262 "%% In this file, you can redefine application environment variables.~n"
263 "%% This way, you don't have to modify the .app files of e.g. OTP applications.~n"
264 "[].~n", []),
265 file:close(Fd).
269 %% @doc Generates the correct set of code paths for the system.
270 -spec get_code_paths(sin_state:state()) -> sin_config:config().
271 get_code_paths(State) ->
272 [filename:join([Path, "ebin"]) ||
273 #app{path=Path} <-
274 sin_state:get_value(release_runtime_deps, State) ++
275 sin_state:get_value(project_apps, State)].
277 %% @doc Optionally add erts directory to release, if defined.
278 -spec include_erts(sin_config:config(), sin_state:state(), string()) -> ok.
279 include_erts(Config, State, ReleaseRootDir) ->
280 case Config:match(include_erts, false) of
281 true ->
282 ErtsDir = sin_utils:get_erts_dir(),
283 sin_utils:copy_dir(Config, State, ReleaseRootDir, ErtsDir, [keep_parent]);
284 _ ->
286 end.
288 copy_apps(Config, State) ->
289 LibDir = sin_state:get_value(apps_dir, State),
290 Apps = sin_state:get_value(release_runtime_deps, State),
291 lists:foreach(fun(#app{path=Path}) ->
292 %% Don't copy it if it already exists in the libdir
293 AppName = filename:basename(Path),
294 case sin_utils:file_exists(State, filename:join(LibDir, AppName)) of
295 false ->
296 sin_utils:copy_dir(Config, State, LibDir, Path, [keep_parent]);
297 true ->
300 end, Apps).
302 copy_include_dirs(Config, State, BuildDir) ->
303 case Config:match(include_dirs, undefined) of
304 undefined ->
306 RequiredDirs ->
307 lists:foreach(fun(File) ->
308 case sin_utils:file_exists(State, File) of
309 true ->
310 sin_utils:copy_dir(Config, State, BuildDir,
311 File, [keep_parent]);
312 false ->
315 %% Bin is always included by default
316 end, ["bin" | RequiredDirs])
317 end.
319 create_bin_file(State, BuildDir, RelName, RelVsn, ErtsVsn) ->
320 BinDir = filename:join([BuildDir, "bin"]),
321 filelib:ensure_dir(filename:join(BinDir, "tmp")),
322 VsnRel = filename:join(BinDir, erlang:atom_to_list(RelName) ++ "-" ++ RelVsn),
323 BareRel = filename:join(BinDir, erlang:atom_to_list(RelName)),
324 StartFile = bin_file_contents(RelName, RelVsn, ErtsVsn),
325 case sin_utils:file_exists(State, VsnRel) of
326 false ->
327 ok = file:write_file(VsnRel, StartFile),
328 ok = file:change_mode(VsnRel, 8#777);
329 _ ->
331 end,
333 case sin_utils:file_exists(State, BareRel) of
334 false ->
335 ok = file:write_file(BareRel, StartFile),
336 ok = file:change_mode(BareRel, 8#777);
337 _ ->
339 end.
342 launch_if_required(Config, BuildDir, RelName, RelVsn) ->
343 BinDir = filename:join([BuildDir, "bin"]),
344 VsnRel = filename:join(BinDir, erlang:atom_to_list(RelName) ++ "-" ++ RelVsn),
345 AdditionalArgs = Config:match(additional_args),
346 case AdditionalArgs of
347 ["start"] ->
348 execute(VsnRel);
349 _ ->
351 end.
353 execute(Cmd) ->
354 Port = open_port({spawn, Cmd}, [ exit_status, binary ] ),
355 wait(Port).
357 wait(Port) ->
358 receive
359 {Port, {data, BinData}} ->
360 io:format("~s~n", [BinData]),
361 wait(Port);
362 {Port, {exit_status, Status}} ->
363 io:format("exit_code: ~p~n", [Status]);
364 {Port, exit} ->
366 end.
368 bin_file_contents(RelName, RelVsn, ErtsVsn) ->
370 [<<"#!/bin/sh
372 set -e
374 SCRIPT_DIR=`dirname $0`
375 RELEASE_ROOT_DIR=`cd $SCRIPT_DIR/.. && pwd`
376 REL_NAME=">>, erlang:atom_to_list(RelName), <<"
377 REL_VSN=">>, RelVsn, <<"
378 ERTS_VSN=">>, ErtsVsn, <<"
379 ERTS_DIR=
380 SYS_CONFIG=
381 ROOTDIR=
383 find_erts_dir() {
384 local erts_dir=$RELEASE_ROOT_DIR/erts-$ERTS_VSN
385 if [ -d \"$erts_dir\" ]; then
386 ERTS_DIR=$erts_dir;
387 ROOTDIR=$RELEASE_ROOT_DIR
388 else
389 local erl=`which erl`
390 local erl_root=`$erl -noshell -eval \"io:format(\\\"~s\\\", [code:root_dir()]).\" -s init stop`
391 ERTS_DIR=$erl_root/erts-$ERTS_VSN
392 ROOTDIR=$erl_root
397 find_sys_config() {
398 local possible_sys=$RELEASE_ROOT_DIR/releases/$REL_VSN/sys.config
399 if [ -f \"$possible_sys\" ]; then
400 SYS_CONFIG=\"-config $possible_sys\"
404 find_erts_dir
405 find_sys_config
407 export ROOTDIR=$RELEASE_ROOT_DIR
408 export BINDIR=$ERTS_DIR/bin
409 export EMU=beam
410 export PROGNAME=erl
411 export LD_LIBRARY_PATH=$ERTS_DIR/lib
413 REL_DIR=$RELEASE_ROOT_DIR/releases/$REL_VSN
415 $BINDIR/erlexec $SYS_CONFIG -boot $REL_DIR/$REL_NAME $@
416 ">>].