1#!/usr/bin/env escript
2%% -*- erlang -*-
3
4%%
5%% %CopyrightBegin%
6%%
7%% Copyright Ericsson AB 2014. All Rights Reserved.
8%%
9%% Licensed under the Apache License, Version 2.0 (the "License");
10%% you may not use this file except in compliance with the License.
11%% You may obtain a copy of the License at
12%%
13%%     http://www.apache.org/licenses/LICENSE-2.0
14%%
15%% Unless required by applicable law or agreed to in writing, software
16%% distributed under the License is distributed on an "AS IS" BASIS,
17%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18%% See the License for the specific language governing permissions and
19%% limitations under the License.
20%%
21%% %CopyrightEnd%
22%%
23
24%%%-------------------------------------------------------------------
25%%% @author Rickard Green <rickard@erlang.org>
26%%% @copyright (C) 2014, Rickard Green
27%%% @doc
28%%%    Verify runtime dependencies when patching OTP applications.
29%%% @end
30%%% Created :  4 Mar 2014 by Rickard Green <rickard@erlang.org>
31%%%-------------------------------------------------------------------
32
33-mode(compile).
34
35-export([main/1]).
36
37main(Args) ->
38    {Force, Release, SourceDir, TargetDir, AppList} = parse_args(Args,
39								 false,
40								 [],
41								 [],
42								 [],
43								 []),
44    SourceAppInfo = read_source_app_info(AppList, SourceDir),
45    AppVsnsTab0 = current_target_app_vsns(TargetDir, Release),
46    AppVsnsTab1 = add_source_app_vsns(SourceAppInfo, AppVsnsTab0),
47    case verify_runtime_deps(SourceAppInfo, AppVsnsTab1, true) of
48	true ->
49	    ok;
50	false ->
51	    case Force of
52		true ->
53		    warn("Your OTP development system was updated with "
54			 "unfulfilled runtime dependencies. The system "
55			 "may not be working as expected.", []);
56		false ->
57		    err("Unfulfilled runtime dependencies. "
58			"See warnings above.~n", [])
59	    end
60    end,
61    halt(0).
62
63parse_args(["-force" | Args], _, Release, SourceDir, TargetDir, Apps) ->
64    parse_args(Args, true, Release, SourceDir, TargetDir, Apps);
65parse_args(["-release", Release | Args], Force, _, SourceDir, TargetDir, Apps) ->
66    parse_args(Args, Force, Release, SourceDir, TargetDir, Apps);
67parse_args(["-source", SourceDir | Args], Force, Release, _, TargetDir, Apps) ->
68    parse_args(Args, Force, Release, SourceDir, TargetDir, Apps);
69parse_args(["-target", TargetDir | Args], Force, Release, SourceDir, _, Apps) ->
70    parse_args(Args, Force, Release, SourceDir, TargetDir, Apps);
71parse_args([App | Args], Force, Release, SourceDir, TargetDir, OldApps) ->
72    parse_args(Args, Force, Release, SourceDir, TargetDir, [App | OldApps]);
73parse_args([], _, [], _, _, _) ->
74    err("Missing release~n", []);
75parse_args([], _, _, [], _, _) ->
76    err("Missing source directory~n", []);
77parse_args([], _, _, _, [], _) ->
78    err("Missing target directory~n", []);
79parse_args([], _, _, _, _, []) ->
80    err("Missing applications~n");
81parse_args([], Force, Release, SourceDir, TargetDir, Apps) ->
82    {Force, Release, SourceDir, TargetDir, Apps}.
83
84
85%warn(Format) ->
86%    warn(Format, []).
87
88warn(Format, Args) ->
89    io:format(standard_error, "WARNING: " ++ Format, Args).
90
91err(Format) ->
92    err(Format, []).
93
94err(Format, Args) ->
95    io:format(standard_error, "ERROR: " ++ Format, Args),
96    halt(1).
97
98read_file(FileName) ->
99    case file:read_file(FileName) of
100	{ok, Content} ->
101	    binary_to_list(Content);
102	{error, Error} ->
103	    err("Failed to read ~s: ~p~n", [FileName, Error])
104    end.
105
106consult_file(FileName) ->
107    case file:consult(FileName) of
108	{ok, Terms} ->
109	    Terms;
110	{error, Error} ->
111	    err("Failed to consult ~s: ~p~n", [FileName, Error])
112    end.
113
114current_target_app_vsns(TargetDir, Release) ->
115    IAV = read_file(filename:join([TargetDir, "releases", Release,
116				   "installed_application_versions"])),
117    DirList = string:tokens(IAV, "\n\r\t "),
118    LibDir = filename:join(TargetDir, "lib"),
119    make_app_vsns_tab(DirList, LibDir, gb_trees:empty()).
120
121make_app_vsns_tab([], _LibDir, GBT) ->
122    GBT;
123make_app_vsns_tab([AppVer | AppVsns], LibDir, GBT0) ->
124    GBT1 = try
125	       case file:read_file_info(filename:join(LibDir, AppVer)) of
126		   {ok, _FInfo} ->
127		       [App, Vsn] = string:tokens(AppVer, "-"),
128		       add_app_vsn(App, Vsn, GBT0);
129		   _ ->
130		       GBT0
131	       end
132	   catch
133	       _:_ ->
134		   warn("Unexpected directory: ~p~n",
135			[filename:join(LibDir, AppVer)]),
136		   GBT0
137	   end,
138    make_app_vsns_tab(AppVsns, LibDir, GBT1).
139
140add_app_vsn(App, VsnList, GBT) when is_atom(App) ->
141    Vsn = parse_vsn(VsnList),
142    case gb_trees:lookup(App, GBT) of
143	none ->
144	    gb_trees:insert(App, [Vsn], GBT);
145	{value, Vsns} ->
146	    gb_trees:update(App, [Vsn | Vsns], GBT)
147    end;
148add_app_vsn(AppStr, VsnList, GBT) ->
149    add_app_vsn(list_to_atom(AppStr), VsnList, GBT).
150
151add_source_app_vsns([], AppVsnsTab) ->
152    AppVsnsTab;
153add_source_app_vsns([{App, Vsn, _IReqs} | AI], AppVsnsTab) ->
154    add_source_app_vsns(AI, add_app_vsn(App, Vsn, AppVsnsTab)).
155
156read_source_app_info([], _SourceDir) ->
157    [];
158read_source_app_info([App | Apps], SourceDir) ->
159    AppFile = case App of
160		  "erts" ->
161		      filename:join([SourceDir, "erts", "preloaded", "ebin",
162				     "erts.app"]);
163		  _ ->
164		      filename:join([SourceDir, "lib", App, "ebin",
165				     App ++ ".app"])
166	      end,
167    AppAtom = list_to_atom(App),
168    case consult_file(AppFile) of
169	[{application, AppAtom, InfoList}] ->
170	    Vsn = case lists:keyfind(vsn, 1, InfoList) of
171		      {vsn, V} ->
172			  V;
173		      _ ->
174			  err("Missing vsn in ~p~n", AppFile)
175		  end,
176	    AI = case lists:keyfind(runtime_dependencies, 1, InfoList) of
177		     {runtime_dependencies, IReqs} ->
178			 case parse_inst_reqs(IReqs) of
179			     error ->
180				 err("Failed to parse runtime_dependencies in ~p~n",
181				     [AppFile]);
182			     ParsedIReqs ->
183				 {AppAtom, Vsn, ParsedIReqs}
184			 end;
185		     _ ->
186			 {AppAtom, Vsn, []}
187		 end,
188	    [AI | read_source_app_info(Apps, SourceDir)];
189	_ ->
190	    err("Failed to parse ~p~n", [AppFile])
191    end.
192
193parse_vsn(VsnStr) ->
194    list_to_tuple(lists:map(fun (IL) ->
195				    list_to_integer(IL)
196			    end, string:tokens(VsnStr, "."))).
197
198parse_inst_reqs(InstReqs) ->
199    try
200	parse_inst_reqs_aux(InstReqs)
201    catch
202	_ : _ ->
203	    error
204    end.
205
206parse_inst_reqs_aux([]) ->
207    [];
208parse_inst_reqs_aux([IR | IRs]) ->
209    [App, VsnStr] = string:tokens(IR, "-"),
210    [{list_to_atom(App), parse_vsn(VsnStr)} | parse_inst_reqs_aux(IRs)].
211
212make_app_vsn_str({App, VsnTup}) ->
213    make_app_vsn_str(tuple_to_list(VsnTup), [atom_to_list(App), $-]).
214
215make_app_vsn_str([I], Acc) ->
216    lists:flatten([Acc, integer_to_list(I)]);
217make_app_vsn_str([I | Is], Acc) ->
218    make_app_vsn_str(Is, [Acc, integer_to_list(I), $.]).
219
220missing_min_req(App, AppVsn, IReq) ->
221    warn("Unfulfilled runtime dependency for application ~p-~s: ~s~n",
222	 [App, AppVsn, make_app_vsn_str(IReq)]).
223
224verify_runtime_deps([], _AppVsnsTab, Res) ->
225    Res;
226verify_runtime_deps([{App, Vsn, IReqs} | SAIs], AppVsnsTab, Res0) ->
227    Res = lists:foldl(
228	    fun ({IRApp, IRMinVsn} = InstReq, AccRes) ->
229		    case gb_trees:lookup(IRApp, AppVsnsTab) of
230			none ->
231			    missing_min_req(App, Vsn, InstReq),
232			    false;
233			{value, AppVsns} ->
234			    try
235				lists:foreach(
236				  fun (AppVsn) ->
237					  case meets_min_req(AppVsn, IRMinVsn) of
238					      true ->
239						  throw(true);
240					      false ->
241						  false
242					  end
243				  end,
244				  AppVsns),
245				missing_min_req(App, Vsn, InstReq),
246				false
247			    catch
248				throw : true ->
249				    AccRes
250			    end
251		    end
252	    end,
253	    Res0,
254	    IReqs),
255    verify_runtime_deps(SAIs, AppVsnsTab, Res).
256
257meets_min_req(Vsn, Vsn) ->
258    true;
259meets_min_req({X}, VsnReq) ->
260    meets_min_req({X, 0, 0}, VsnReq);
261meets_min_req({X, Y}, VsnReq) ->
262    meets_min_req({X, Y, 0}, VsnReq);
263meets_min_req(Vsn, {X}) ->
264    meets_min_req(Vsn, {X, 0, 0});
265meets_min_req(Vsn, {X, Y}) ->
266    meets_min_req(Vsn, {X, Y, 0});
267meets_min_req({X, _Y, _Z}, {XReq, _YReq, _ZReq}) when X > XReq ->
268    true;
269meets_min_req({X, Y, _Z}, {X, YReq, _ZReq}) when Y > YReq ->
270    true;
271meets_min_req({X, Y, Z}, {X, Y, ZReq}) when Z > ZReq ->
272    true;
273meets_min_req({_X, _Y, _Z}, {_XReq, _YReq, _ZReq}) ->
274    false;
275meets_min_req(Vsn, VsnReq) ->
276    gp_meets_min_req(mk_gp_vsn_list(Vsn), mk_gp_vsn_list(VsnReq)).
277
278gp_meets_min_req([X, Y, Z | _Vs], [X, Y, Z]) ->
279    true;
280gp_meets_min_req([X, Y, Z | _Vs], [XReq, YReq, ZReq]) ->
281    meets_min_req({X, Y, Z}, {XReq, YReq, ZReq});
282gp_meets_min_req([X, Y, Z | Vs], [X, Y, Z | VReqs]) ->
283    gp_meets_min_req_tail(Vs, VReqs);
284gp_meets_min_req(_Vsn, _VReq) ->
285    %% Versions on different version branches, i.e., the minimum
286    %% required functionality is not included in Vsn.
287    false.
288
289gp_meets_min_req_tail([V | Vs], [V | VReqs]) ->
290    gp_meets_min_req_tail(Vs, VReqs);
291gp_meets_min_req_tail([], []) ->
292    true;
293gp_meets_min_req_tail([_V | _Vs], []) ->
294    true;
295gp_meets_min_req_tail([V | _Vs], [VReq]) when V > VReq ->
296    true;
297gp_meets_min_req_tail(_Vs, _VReqs) ->
298    %% Versions on different version branches, i.e., the minimum
299    %% required functionality is not included in Vsn.
300    false.
301
302mk_gp_vsn_list(Vsn) ->
303    [X, Y, Z | Tail] = tuple_to_list(Vsn),
304    [X, Y, Z | remove_trailing_zeroes(Tail)].
305
306remove_trailing_zeroes([]) ->
307    [];
308remove_trailing_zeroes([0 | Vs]) ->
309    case remove_trailing_zeroes(Vs) of
310	[] -> [];
311	NewVs -> [0 | NewVs]
312    end;
313remove_trailing_zeroes([V | Vs]) ->
314    [V | remove_trailing_zeroes(Vs)].
315