Merge remote-tracking branch 'canonical/next'
[sinan.git] / src / sin_task_depends.erl
blobee1058f78ed02220e5b71ec48191b6febd82287f
1 %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
2 %%%---------------------------------------------------------------------------
3 %%% @author Eric Merritt
4 %%% @doc
5 %%% Checks the dependencies in the system. Pulls down latest dependencies if
6 %%% required.
7 %%% @end
8 %%% @copyright (C) 2007-2011 Erlware
9 %%%---------------------------------------------------------------------------
10 -module(sin_task_depends).
12 -behaviour(sin_task).
14 -include_lib("sinan/include/sinan.hrl").
16 %% API
17 -export([description/0,
18 do_task/2,
19 format_exception/1]).
21 -define(TASK, depends).
22 -define(DEPS, []).
24 %%====================================================================
25 %% API
26 %%====================================================================
28 %% @doc provide a description of the system for the caller
29 -spec description() -> sin_task:task_description().
30 description() ->
32 Desc = "
33 depends Task
34 ============
36 This task analyzes all of the dependencies in the project and
37 provides that information to the build state for use by other
38 tasks.
40 Dependencies are taken from three areas.
42 1. Dependencies specified in the `applications` and `included_applications`
43 element in each OTP app metadata file.
44 2. Dependency constraints specified in the projects `sinan.config`
45 3. Dependency constraints in a `dep_constraints` element in the OTP App metadata.
47 `dep_constraints` is a Sinan only addition to the metadata that OTP ignores.
50 Constraining Dependency Versions
51 --------------------------------
53 To constrain the versions of set of dependencies that you rely on you
54 must add a `dep_constraints` tuple to either the [[OTPApplication]]
55 dotapp file or the `sinan.config`. The `dep_constraints` tuple
56 contains a list of dependency constraints for the [[OTPApplication]]
57 in the case of the dotapp or for the entire project in the case of the
58 `sinan.config`. An individual dependency constraint may look as follows.
60 {<app-name>, <version>, <type>}
64 {<app-name>, <version 1>, <version 2>, <type>}
68 {<app-name>, <version>}
70 In the above case type may be one of the following
72 * gte: greater than or equal
73 * lte: less than or equal
74 * lt: less than
75 * gt: greater than
76 * eql: equal
77 * between: between two versions
79 `between` is the specifier that takes two versions. So if we depended
80 on my_app between versions 0.1 and 5.0 we would have the entry:
82 {my_foo, \"0.1\", \"5.0\", between}
84 that would provide the constraints we need.
86 The form `{<app-name>, <version>}` is exactly equivalent to
87 `{<app-name>, <version>, eql}` and is provided as a convenience.
89 Lets look at a complete example used in sinan itself. This is from
90 sinan's `sinan.config`.
93 {dep_constraints,
94 [{cucumberl, \"0.0.4\", gte},
95 {erlware_commons, \"0.6.0\", gte},
96 {getopt, \"0.0.1\", gte}]}.
98 This is specifying the versions for the entire sinan project.
101 #task{name = ?TASK,
102 task_impl = ?MODULE,
103 bare = false,
104 example = "depends",
105 short_desc = "Dependency resolution for the project",
106 deps = ?DEPS,
107 desc = Desc,
108 opts = []}.
110 %% @doc gather all the dependencies for the system
111 -spec do_task(sin_config:config(), sin_state:state()) ->
112 sin_state:state().
113 do_task(Config, State0) ->
114 ProjectApps = sin_state:get_value(project_applist, State0),
115 {State1, {ReleaseApps, RuntimeDeps0, CompiletimeDeps}} =
116 solve_deps(Config, State0, ProjectApps),
118 lists:foreach(fun(#app{path=Path}) ->
119 Ebin = filename:join(Path, "ebin"),
120 ec_file:mkdir_path(Ebin),
121 true = code:add_patha(Ebin)
122 end, CompiletimeDeps),
124 MergedDeps = RuntimeDeps0 ++ CompiletimeDeps,
126 FA = fun (App) ->
127 format_app(Config, App)
128 end,
129 sin_log:verbose(Config, "~ncompile time dependencies:~n"),
130 lists:foreach(FA, CompiletimeDeps),
132 sin_log:verbose(Config, "~nruntime dependencies:~n"),
133 lists:foreach(FA, RuntimeDeps0),
135 sin_log:verbose(Config, "~nproject applications:~n"),
136 lists:foreach(FA, ReleaseApps),
138 sin_state:store([{release_runtime_deps, RuntimeDeps0},
139 {release_compile_deps, CompiletimeDeps},
140 {release_deps, MergedDeps},
141 {project_apps, ReleaseApps},
142 {release_apps, MergedDeps ++ ReleaseApps}],
143 State1).
145 -spec solve_deps(sin_config:config(), sin_state:state(), [atom()]) ->
146 {sin_state:state(), term()}.
147 solve_deps(Config, State0, ProjectApps) ->
148 DefaultConstraints = Config:match(dep_constraints, []),
150 {Apps, ActualSpecs} = remove_excluded(ProjectApps,
151 DefaultConstraints),
153 ResolverState0 = sin_dep_resolver:new(sin_fs_resolver, Config, State0),
154 SolverState0 = sin_dep_solver:new(ResolverState0),
156 {SolverState1, RuntimeDeps0} =
157 get_runtime_deps(Config, State0, SolverState0, Apps, ActualSpecs),
158 {SolverState2, CompiletimeDeps0} =
159 get_compiletime_deps(Config, State0, SolverState1, DefaultConstraints),
161 ResolverState1 = sin_dep_solver:extract_resolver_state(SolverState2),
162 {ReleaseApps1, RuntimeDeps2} =
163 seperate_resolve_deps(ResolverState1, RuntimeDeps0, ProjectApps),
165 CompiletimeDeps1 =
166 lists:foldl(fun({AppName, Vsn}, Acc) ->
167 case in_runtime(AppName, RuntimeDeps2) of
168 true ->
169 Acc;
170 false ->
171 {_, Path} =
172 sin_dep_resolver:resolve(ResolverState1,
173 AppName, Vsn),
174 [#app{name=AppName, vsn=Vsn, path=Path,
175 type=compiletime, project=false} |
176 Acc]
178 end, [], CompiletimeDeps0),
181 State1 =
182 sin_sig:save_sig_info(?MODULE,
183 {ReleaseApps1, RuntimeDeps2, CompiletimeDeps1},
184 State0),
186 {State1, {ReleaseApps1, RuntimeDeps2, CompiletimeDeps1}}.
188 seperate_resolve_deps(ResolverState1, RuntimeDeps0, ProjectApps) ->
189 lists:foldl(fun({AppName, Vsn}, {ReleaseApps0, RuntimeDeps1}) ->
190 {_, Path} =
191 sin_dep_resolver:resolve(ResolverState1,
192 AppName, Vsn),
193 case get_project_app(AppName, ProjectApps) of
194 {ok, PApp, _} ->
195 {[PApp#app{path=Path,
196 type=runtime,
197 project=true} | ReleaseApps0],
198 RuntimeDeps1};
199 not_found ->
200 {ReleaseApps0,
201 [#app{name=AppName, vsn=Vsn, path=Path,
202 type=runtime,
203 project=false} | RuntimeDeps1]}
205 end, {[], []}, RuntimeDeps0).
207 get_project_app(AppName, ProjectApps) ->
208 ec_lists:search(fun(PApp=#app{name=PAppName}) ->
209 case PAppName == AppName of
210 true ->
211 {ok,PApp};
212 _ ->
213 not_found
215 end,
216 ProjectApps).
218 in_runtime(App0, RuntimeDeps) ->
219 lists:any(fun(#app{name=AppName})
220 when App0 == AppName ->
221 true;
222 (_) ->
223 false
224 end, RuntimeDeps).
226 -spec format_app(sin_config:config(), sinan:app()) -> ok.
227 format_app(Config, #app{name=Name0, vsn=Vsn0, path=Path}) ->
228 Name1 = string:left(erlang:atom_to_list(Name0), 25),
229 Vsn1 = string:left(Vsn0, 10),
230 sin_log:verbose(Config, " ~s ~s : ~s", [Name1, Vsn1, Path]).
233 %% @doc Format an exception thrown by this module
234 -spec format_exception(sin_exceptions:exception()) ->
235 string().
236 format_exception(?SIN_EXEP_UNPARSE(_, {failed, {possible_culprit, SpecList1}})) ->
237 ["Unable to resolve compile time dependencies, probably due "
238 "to the following constraints:~n",
239 lists:map(fun({AppName, LimitSet, Sources}) ->
240 io_lib:format(" constraint on ~p with constraints ~p "
241 "originating from these application(s) ~p",
242 [AppName, sets:to_list(LimitSet),
243 sets:to_list(Sources)]);
244 (AppName) when is_atom(AppName) ->
245 [" application", erlang:atom_to_list(AppName), " in the project "]
246 end, SpecList1)];
247 format_exception(Exception) ->
248 sin_exceptions:format_exception(Exception).
250 %%====================================================================
251 %% Internal functions
252 %%====================================================================
253 -spec get_compiletime_deps(sin_config:config(),
254 sin_state:state(),
255 sin_dep_solver:state(),
256 [sin_dep_solver:spec()]) ->
257 {sin_dep_solver:state(),
258 [sin_dep_solver:spec()]}.
259 get_compiletime_deps(Config, State0, SolverState0, DefaultSpecs) ->
260 CompiletimeApps0 = Config:match(compile_deps, []),
262 {CompiletimeApps1, CompileSpecs} = remove_excluded(CompiletimeApps0,
263 DefaultSpecs),
264 CompiletimeDeps0 =
265 case sin_dep_solver:all_deps(Config, SolverState0,
266 CompiletimeApps1,
267 CompileSpecs) of
268 Error1 = {failed, {possible_culprit, _SpecList1}} ->
269 ?SIN_RAISE(State0, Error1);
270 {_, Deps1} ->
271 lists:filter(fun({excluded, _}) ->
272 false;
273 (_) ->
274 true
275 end, Deps1)
276 end,
277 {SolverState0, CompiletimeDeps0}.
280 get_runtime_deps(Config, State0, SolverState0, Apps, Specs) ->
281 case sin_dep_solver:all_deps(Config, SolverState0, Apps,
282 Specs) of
283 Error1 = {failed, {possible_culprit, _SpecList1}} ->
284 ?SIN_RAISE(State0, Error1);
285 {SolverState1, Deps0} ->
286 {SolverState1,
287 lists:filter(fun({excluded, _}) ->
288 false;
289 (_) ->
290 true
291 end, Deps0)}
292 end.
294 remove_excluded(Apps0, Constraints) ->
295 Apps1 = lists:filter(fun(#app{name=AppName}) ->
296 not lists:member({exclude, AppName},
297 Constraints)
298 end, Apps0),
299 DefaultSpecs =
300 [{AppName, Vsn} || #app{name=AppName, vsn=Vsn} <- Apps1],
302 {[AppName || #app{name=AppName} <- Apps1], DefaultSpecs ++ Constraints}.