Merge remote-tracking branch 'canonical/next'
[sinan.git] / src / sin_task_escript.erl
blob6989d9e829722108bed7a3a65abc0645c8ad1f80
1 %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
2 %%%---------------------------------------------------------------------------
3 %%% @author Eric Merritt
4 %%% @doc
5 %%% Builds up a escript distributable application (in the sense of a unix application,
6 %%% not an otp application). It looks for an escript directive
7 %%% all of the apps to the system.
8 %%% @end
9 %%% @copyright (C) 2007-2011 Erlware
10 %%%---------------------------------------------------------------------------
11 -module(sin_task_escript).
13 -behaviour(sin_task).
15 -include_lib("sinan/include/sinan.hrl").
17 %% API
18 -export([description/0, do_task/2, format_exception/1]).
20 -define(TASK, escript).
21 -define(DEPS, [build]).
23 %%====================================================================
24 %% API
25 %%====================================================================
27 %% @doc provides a description of the sytem, for help and other reasons
28 -spec description() -> sin_task:task_description().
29 description() ->
31 Desc = "
32 escript Task
33 ============
35 This takes the current project and turns it into an executable
36 [Escript](http://www.erlang.org/doc/man/escript.html).
38 Be aware though that there are significant limitations in escript. These are not
39 limitations of Sinan, but limitations in the built in escript
40 functionality. Your escript may be built off of a single script OR Erlang OTP
41 Applications but not both The system will warn you if you violate these
42 restrictions.
44 Configuration Options
45 ---------------------
47 The escript task allows for a few options in the sinan.config file. The
48 options are specified as follows:
50 {escript, [{Key::atom()), term()}]}.
52 The keys available are:
54 {source, Path::string()}
56 The source command is used if you want to include a single source file as the
57 escript.
59 {emu_args, Args::string()}.
61 {include_apps, [App::term()]}.
63 include_apps is a list of all the apps that are not part of the project that you
64 want to include in the escript. The project apps are all included by default.
66 So a fully configured escript config might look like:
68 {escript,
69 [{source, \"bin/my_cool_escript_file\"},
70 {emu_args, \"-smp disable\"},
71 {include_apps, [kernel, stdlib, my_dep]}.
73 See the escript documentation for details and remember to only pass the
74 script option if you want that to be your escript.",
76 #task{name = ?TASK,
77 task_impl = ?MODULE,
78 bare = false,
79 deps = ?DEPS,
80 example = "escript",
81 short_desc = "Provides a standard erlang escript",
82 desc = Desc,
83 opts = []}.
85 %% @doc Build an escript for this project
86 -spec do_task(sin_config:matcher(), sin_state:state()) -> sin_state:state().
87 do_task(Config, State) ->
88 ProjectDir = sin_state:get_value(project_dir, State),
89 ReleaseApps = sin_state:get_value(release_apps, State),
90 ProjectApps = sin_state:get_value(project_apps, State),
91 BuildDir = sin_state:get_value(build_dir, State),
92 EscriptDir = filename:join([BuildDir, "escript"]),
93 EscriptWorkingDir = filename:join(EscriptDir, ".ez"),
94 ec_file:mkdir_path(EscriptWorkingDir),
95 ReleaseName = sin_state:get_value(release, State),
97 EscriptOptions = Config:match(escript, []),
98 PossibleSourceFile = get_source_file(Config, State, ProjectDir, EscriptOptions),
100 Body =
101 case PossibleSourceFile of
102 [] ->
103 make_archive(Config, State,
104 ReleaseName,
105 EscriptWorkingDir,
106 gather_dirs(Config, State, EscriptWorkingDir,
107 filter_apps(ReleaseApps,
108 EscriptOptions)
110 ProjectApps, []));
111 _ ->
112 sin_log:normal(Config, "With escript you may have source files "
113 "or archive files, but you may not have "
114 "both. Since script files are defined "
115 "I am omiting dependency archives"),
117 end,
119 EscriptTarget = filename:join([EscriptDir, ReleaseName]),
121 EmuArgs =
122 case lists:keyfind(emu_args, 1, EscriptOptions) of
123 {emu_args, ArgList} when is_list(ArgList) ->
124 {emu_args, ArgList};
125 false ->
127 {emu_args, BadArgs} ->
128 sin_log:normal(Config, "emu_args to escript must be a list! not ~p",
129 [BadArgs]),
130 ?SIN_RAISE(State, {bad_emu_args, BadArgs})
131 end,
133 case escript:create(EscriptTarget,
134 lists:flatten([shebang, EmuArgs,
135 PossibleSourceFile, Body])) of
136 ok ->
137 sin_log:normal(Config, "Escript created at ~s",
138 [EscriptTarget]);
139 Error = {error, _} ->
140 sin_log:normal(Config, "Enable to create escript at ~s due to ~p!",
141 [EscriptTarget, Error]),
142 ?SIN_RAISE(State, {unable_to_create_escript, Error})
143 end,
144 %% Execute owner 8#00100, Execute group 8#00010, Execute other 8#00001
145 %% Read owner 8#00400, Read group 8#00040, Read other 8#00004
146 %% Write owner 8#00200, Write group 8#00020, Write other 8#00002
147 %% The mode is the sum of permissions
148 file:change_mode(EscriptTarget, 8#00100 + 8#00010 + 8#00001 +
149 8#00400 + 8#00040 + 8#00004 +
150 8#00200 + 8#00020 + 8#00002),
151 sin_utils:delete_dir(EscriptWorkingDir),
152 State.
154 %% @doc Format an exception thrown by this module
155 -spec format_exception(sin_exceptions:exception()) ->
156 string().
157 format_exception(Exception) ->
158 sin_exceptions:format_exception(Exception).
160 %%====================================================================
161 %%% Internal functions
162 %%====================================================================
164 -spec gather_dirs(sin_config:config(),
165 sin_state:state(), string(), [tuple()], [string()]) ->
166 [string()].
167 gather_dirs(Config, State0, EscriptTargetDir,
168 [#app{name=AppName, vsn=Vsn, path=Path} | T], FileList) ->
169 FileName = erlang:atom_to_list(AppName) ++ "-" ++ Vsn,
170 Target = filename:join(EscriptTargetDir, FileName),
171 ok = ec_file:mkdir_path(Target),
172 State1 = sin_utils:copy_dir(Config, State0, Target, Path),
173 gather_dirs(Config, State1, EscriptTargetDir, T, [FileName | FileList]);
174 gather_dirs(_, _State, _, [], FileList) ->
175 FileList.
177 make_archive(Config, State, ProjectName, CWD, FileList) ->
178 EscriptPath = filename:join([CWD,
179 erlang:atom_to_list(ProjectName) ++ ".ez"]),
180 case zip:create(EscriptPath,
181 FileList,
182 [{cwd, CWD},
183 {compress, all},
184 {uncompress,[".beam",".app"]}]) of
185 {ok, EscriptPath} ->
187 {error, enoent} ->
188 sin_log:normal(Config, "Error trying to write ez archive "
189 "for applications in ~s. This is "
190 "probably due to dot files (.* .#* "
191 "like emacs archive files in the "
192 "application directory. Do a sinan clean,"
193 "clean out the project directory and "
194 "try again", [EscriptPath]),
195 ?SIN_RAISE(State, {error_creating_archive, EscriptPath});
196 Error ->
197 sin_log:normal(Config, "Unknown error (~p) occured while "
198 "trying to write ~s to ~s",
199 [Error, CWD, EscriptPath]),
200 ?SIN_RAISE(State, {error_creating_archive, Error})
201 end,
202 {archive, EscriptPath}.
205 get_source_file(Config, State, ProjectDir, EscriptOptions) ->
206 Sources =
207 lists:foldl(fun({source, SourceFile}, Acc) ->
208 AbsoluteSourcePath = filename:join(ProjectDir,
209 SourceFile),
210 case file:read_file(AbsoluteSourcePath) of
211 {ok, Source} ->
212 [{source, Source} | Acc];
213 Error = {error, _} ->
214 ?SIN_RAISE(State,
215 {unable_to_read_source,
216 AbsoluteSourcePath, Error})
217 end;
218 (_, Acc) ->
220 end, [], EscriptOptions),
221 case Sources of
222 [] ->
224 [Source] ->
225 [Source];
226 _ ->
227 sin_log:normal(Config, "You may only have one source entry in your "
228 "escript directive"),
229 ?SIN_RAISE(State, {multiple_source_entries, Sources})
230 end.
232 filter_apps(Apps, EscriptOptions) ->
233 IncludedApps =
234 case lists:keyfind(include_apps, 1, EscriptOptions) of
235 {include_apps, IApps} ->
236 IApps;
237 _ ->
239 end,
240 lists:filter(fun(#app{name=AppName}) ->
241 lists:member(AppName, IncludedApps)
242 end, Apps).