#!/usr/bin/env escript %% -*- erlang -*- %% %% %CopyrightBegin% %% %% Copyright Ericsson AB 2014. All Rights Reserved. %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %% %% %CopyrightEnd% %% %%%------------------------------------------------------------------- %%% @author Rickard Green %%% @copyright (C) 2014, Rickard Green %%% @doc %%% Verify runtime dependencies when patching OTP applications. %%% @end %%% Created : 4 Mar 2014 by Rickard Green %%%------------------------------------------------------------------- -mode(compile). -export([main/1]). main(Args) -> {Force, Release, SourceDir, TargetDir, AppList} = parse_args(Args, false, [], [], [], []), SourceAppInfo = read_source_app_info(AppList, SourceDir), AppVsnsTab0 = current_target_app_vsns(TargetDir, Release), AppVsnsTab1 = add_source_app_vsns(SourceAppInfo, AppVsnsTab0), case verify_runtime_deps(SourceAppInfo, AppVsnsTab1, true) of true -> ok; false -> case Force of true -> warn("Your OTP development system was updated with " "unfulfilled runtime dependencies. The system " "may not be working as expected.", []); false -> err("Unfulfilled runtime dependencies. " "See warnings above.~n", []) end end, halt(0). parse_args(["-force" | Args], _, Release, SourceDir, TargetDir, Apps) -> parse_args(Args, true, Release, SourceDir, TargetDir, Apps); parse_args(["-release", Release | Args], Force, _, SourceDir, TargetDir, Apps) -> parse_args(Args, Force, Release, SourceDir, TargetDir, Apps); parse_args(["-source", SourceDir | Args], Force, Release, _, TargetDir, Apps) -> parse_args(Args, Force, Release, SourceDir, TargetDir, Apps); parse_args(["-target", TargetDir | Args], Force, Release, SourceDir, _, Apps) -> parse_args(Args, Force, Release, SourceDir, TargetDir, Apps); parse_args([App | Args], Force, Release, SourceDir, TargetDir, OldApps) -> parse_args(Args, Force, Release, SourceDir, TargetDir, [App | OldApps]); parse_args([], _, [], _, _, _) -> err("Missing release~n", []); parse_args([], _, _, [], _, _) -> err("Missing source directory~n", []); parse_args([], _, _, _, [], _) -> err("Missing target directory~n", []); parse_args([], _, _, _, _, []) -> err("Missing applications~n"); parse_args([], Force, Release, SourceDir, TargetDir, Apps) -> {Force, Release, SourceDir, TargetDir, Apps}. %warn(Format) -> % warn(Format, []). warn(Format, Args) -> io:format(standard_error, "WARNING: " ++ Format, Args). err(Format) -> err(Format, []). err(Format, Args) -> io:format(standard_error, "ERROR: " ++ Format, Args), halt(1). read_file(FileName) -> case file:read_file(FileName) of {ok, Content} -> binary_to_list(Content); {error, Error} -> err("Failed to read ~s: ~p~n", [FileName, Error]) end. consult_file(FileName) -> case file:consult(FileName) of {ok, Terms} -> Terms; {error, Error} -> err("Failed to consult ~s: ~p~n", [FileName, Error]) end. current_target_app_vsns(TargetDir, Release) -> IAV = read_file(filename:join([TargetDir, "releases", Release, "installed_application_versions"])), DirList = string:tokens(IAV, "\n\r\t "), LibDir = filename:join(TargetDir, "lib"), make_app_vsns_tab(DirList, LibDir, gb_trees:empty()). make_app_vsns_tab([], _LibDir, GBT) -> GBT; make_app_vsns_tab([AppVer | AppVsns], LibDir, GBT0) -> GBT1 = try case file:read_file_info(filename:join(LibDir, AppVer)) of {ok, _FInfo} -> [App, Vsn] = string:tokens(AppVer, "-"), add_app_vsn(App, Vsn, GBT0); _ -> GBT0 end catch _:_ -> warn("Unexpected directory: ~p~n", [filename:join(LibDir, AppVer)]), GBT0 end, make_app_vsns_tab(AppVsns, LibDir, GBT1). add_app_vsn(App, VsnList, GBT) when is_atom(App) -> Vsn = parse_vsn(VsnList), case gb_trees:lookup(App, GBT) of none -> gb_trees:insert(App, [Vsn], GBT); {value, Vsns} -> gb_trees:update(App, [Vsn | Vsns], GBT) end; add_app_vsn(AppStr, VsnList, GBT) -> add_app_vsn(list_to_atom(AppStr), VsnList, GBT). add_source_app_vsns([], AppVsnsTab) -> AppVsnsTab; add_source_app_vsns([{App, Vsn, _IReqs} | AI], AppVsnsTab) -> add_source_app_vsns(AI, add_app_vsn(App, Vsn, AppVsnsTab)). read_source_app_info([], _SourceDir) -> []; read_source_app_info([App | Apps], SourceDir) -> AppFile = case App of "erts" -> filename:join([SourceDir, "erts", "preloaded", "ebin", "erts.app"]); _ -> filename:join([SourceDir, "lib", App, "ebin", App ++ ".app"]) end, AppAtom = list_to_atom(App), case consult_file(AppFile) of [{application, AppAtom, InfoList}] -> Vsn = case lists:keyfind(vsn, 1, InfoList) of {vsn, V} -> V; _ -> err("Missing vsn in ~p~n", AppFile) end, AI = case lists:keyfind(runtime_dependencies, 1, InfoList) of {runtime_dependencies, IReqs} -> case parse_inst_reqs(IReqs) of error -> err("Failed to parse runtime_dependencies in ~p~n", [AppFile]); ParsedIReqs -> {AppAtom, Vsn, ParsedIReqs} end; _ -> {AppAtom, Vsn, []} end, [AI | read_source_app_info(Apps, SourceDir)]; _ -> err("Failed to parse ~p~n", [AppFile]) end. parse_vsn(VsnStr) -> list_to_tuple(lists:map(fun (IL) -> list_to_integer(IL) end, string:tokens(VsnStr, "."))). parse_inst_reqs(InstReqs) -> try parse_inst_reqs_aux(InstReqs) catch _ : _ -> error end. parse_inst_reqs_aux([]) -> []; parse_inst_reqs_aux([IR | IRs]) -> [App, VsnStr] = string:tokens(IR, "-"), [{list_to_atom(App), parse_vsn(VsnStr)} | parse_inst_reqs_aux(IRs)]. make_app_vsn_str({App, VsnTup}) -> make_app_vsn_str(tuple_to_list(VsnTup), [atom_to_list(App), $-]). make_app_vsn_str([I], Acc) -> lists:flatten([Acc, integer_to_list(I)]); make_app_vsn_str([I | Is], Acc) -> make_app_vsn_str(Is, [Acc, integer_to_list(I), $.]). missing_min_req(App, AppVsn, IReq) -> warn("Unfulfilled runtime dependency for application ~p-~s: ~s~n", [App, AppVsn, make_app_vsn_str(IReq)]). verify_runtime_deps([], _AppVsnsTab, Res) -> Res; verify_runtime_deps([{App, Vsn, IReqs} | SAIs], AppVsnsTab, Res0) -> Res = lists:foldl( fun ({IRApp, IRMinVsn} = InstReq, AccRes) -> case gb_trees:lookup(IRApp, AppVsnsTab) of none -> missing_min_req(App, Vsn, InstReq), false; {value, AppVsns} -> try lists:foreach( fun (AppVsn) -> case meets_min_req(AppVsn, IRMinVsn) of true -> throw(true); false -> false end end, AppVsns), missing_min_req(App, Vsn, InstReq), false catch throw : true -> AccRes end end end, Res0, IReqs), verify_runtime_deps(SAIs, AppVsnsTab, Res). meets_min_req(Vsn, Vsn) -> true; meets_min_req({X}, VsnReq) -> meets_min_req({X, 0, 0}, VsnReq); meets_min_req({X, Y}, VsnReq) -> meets_min_req({X, Y, 0}, VsnReq); meets_min_req(Vsn, {X}) -> meets_min_req(Vsn, {X, 0, 0}); meets_min_req(Vsn, {X, Y}) -> meets_min_req(Vsn, {X, Y, 0}); meets_min_req({X, _Y, _Z}, {XReq, _YReq, _ZReq}) when X > XReq -> true; meets_min_req({X, Y, _Z}, {X, YReq, _ZReq}) when Y > YReq -> true; meets_min_req({X, Y, Z}, {X, Y, ZReq}) when Z > ZReq -> true; meets_min_req({_X, _Y, _Z}, {_XReq, _YReq, _ZReq}) -> false; meets_min_req(Vsn, VsnReq) -> gp_meets_min_req(mk_gp_vsn_list(Vsn), mk_gp_vsn_list(VsnReq)). gp_meets_min_req([X, Y, Z | _Vs], [X, Y, Z]) -> true; gp_meets_min_req([X, Y, Z | _Vs], [XReq, YReq, ZReq]) -> meets_min_req({X, Y, Z}, {XReq, YReq, ZReq}); gp_meets_min_req([X, Y, Z | Vs], [X, Y, Z | VReqs]) -> gp_meets_min_req_tail(Vs, VReqs); gp_meets_min_req(_Vsn, _VReq) -> %% Versions on different version branches, i.e., the minimum %% required functionality is not included in Vsn. false. gp_meets_min_req_tail([V | Vs], [V | VReqs]) -> gp_meets_min_req_tail(Vs, VReqs); gp_meets_min_req_tail([], []) -> true; gp_meets_min_req_tail([_V | _Vs], []) -> true; gp_meets_min_req_tail([V | _Vs], [VReq]) when V > VReq -> true; gp_meets_min_req_tail(_Vs, _VReqs) -> %% Versions on different version branches, i.e., the minimum %% required functionality is not included in Vsn. false. mk_gp_vsn_list(Vsn) -> [X, Y, Z | Tail] = tuple_to_list(Vsn), [X, Y, Z | remove_trailing_zeroes(Tail)]. remove_trailing_zeroes([]) -> []; remove_trailing_zeroes([0 | Vs]) -> case remove_trailing_zeroes(Vs) of [] -> []; NewVs -> [0 | NewVs] end; remove_trailing_zeroes([V | Vs]) -> [V | remove_trailing_zeroes(Vs)].