|
|
@@ -25,15 +25,14 @@ Usage:
|
|
|
|
|
|
Options:
|
|
|
|
|
|
- --check Don't update the appfile, just check that they are complete
|
|
|
- --repo Upstream git repo URL
|
|
|
- --remote Get upstream repo URL from the specified git remote
|
|
|
- --skip-build Don't rebuild the releases. May produce wrong results
|
|
|
- --make-command A command used to assemble the release
|
|
|
- --release-dir Release directory
|
|
|
- --src-dirs Directories where source code is found. Defaults to '{src,apps,lib-*}/**/'
|
|
|
- --binary-rel-url Binary release URL pattern. %VSN% variable is substituted with the version in release tag.
|
|
|
- E.g. \"https://github.com/emqx/emqx/releases/download/v%VSN%/emqx-%VSN%-otp-24.2.1-1-el7-amd64.tar.gz\"
|
|
|
+ --check Don't update the appfile, just check that they are complete
|
|
|
+ --repo Upsteam git repo URL
|
|
|
+ --remote Get upstream repo URL from the specified git remote
|
|
|
+ --skip-build Don't rebuild the releases. May produce wrong results
|
|
|
+ --make-command A command used to assemble the release
|
|
|
+ --prev-release-dir Previous version's release dir (if already built/extracted)
|
|
|
+ --release-dir Release directory
|
|
|
+ --src-dirs Directories where source code is found. Defaults to '{src,apps,lib-*}/**/'
|
|
|
".
|
|
|
|
|
|
-record(app,
|
|
|
@@ -48,7 +47,7 @@ default_options() ->
|
|
|
, check => false
|
|
|
, prev_tag => undefined
|
|
|
, src_dirs => "{src,apps,lib-*}/**/"
|
|
|
- , binary_rel_url => undefined
|
|
|
+ , prev_beams_dir => undefined
|
|
|
}.
|
|
|
|
|
|
%% App-specific actions that should be added unconditionally to any update/downgrade:
|
|
|
@@ -56,7 +55,8 @@ app_specific_actions(_) ->
|
|
|
[].
|
|
|
|
|
|
ignored_apps() ->
|
|
|
- [emqx_dashboard, emqx_management] ++ otp_standard_apps().
|
|
|
+ [gpb %% only a build tool
|
|
|
+ ] ++ otp_standard_apps().
|
|
|
|
|
|
main(Args) ->
|
|
|
#{prev_tag := Baseline} = Options = parse_args(Args, default_options()),
|
|
|
@@ -68,7 +68,7 @@ parse_args([PrevTag = [A|_]], State) when A =/= $- ->
|
|
|
parse_args(["--check"|Rest], State) ->
|
|
|
parse_args(Rest, State#{check => true});
|
|
|
parse_args(["--skip-build"|Rest], State) ->
|
|
|
- parse_args(Rest, State#{make_command => "true"});
|
|
|
+ parse_args(Rest, State#{make_command => undefined});
|
|
|
parse_args(["--repo", Repo|Rest], State) ->
|
|
|
parse_args(Rest, State#{clone_url => Repo});
|
|
|
parse_args(["--remote", Remote|Rest], State) ->
|
|
|
@@ -77,15 +77,16 @@ parse_args(["--make-command", Command|Rest], State) ->
|
|
|
parse_args(Rest, State#{make_command => Command});
|
|
|
parse_args(["--release-dir", Dir|Rest], State) ->
|
|
|
parse_args(Rest, State#{beams_dir => Dir});
|
|
|
+parse_args(["--prev-release-dir", Dir|Rest], State) ->
|
|
|
+ parse_args(Rest, State#{prev_beams_dir => Dir});
|
|
|
parse_args(["--src-dirs", Pattern|Rest], State) ->
|
|
|
parse_args(Rest, State#{src_dirs => Pattern});
|
|
|
-parse_args(["--binary-rel-url", URL|Rest], State) ->
|
|
|
- parse_args(Rest, State#{binary_rel_url => {ok, URL}});
|
|
|
parse_args(_, _) ->
|
|
|
fail(usage()).
|
|
|
|
|
|
main(Options, Baseline) ->
|
|
|
{CurrRelDir, PrevRelDir} = prepare(Baseline, Options),
|
|
|
+ putopt(prev_beams_dir, PrevRelDir),
|
|
|
log("~n===================================~n"
|
|
|
"Processing changes..."
|
|
|
"~n===================================~n"),
|
|
|
@@ -93,38 +94,9 @@ main(Options, Baseline) ->
|
|
|
PrevAppsIdx = index_apps(PrevRelDir),
|
|
|
%% log("Curr: ~p~nPrev: ~p~n", [CurrAppsIdx, PrevAppsIdx]),
|
|
|
AppupChanges = find_appup_actions(CurrAppsIdx, PrevAppsIdx),
|
|
|
- case getopt(check) of
|
|
|
- true ->
|
|
|
- case AppupChanges of
|
|
|
- [] ->
|
|
|
- ok;
|
|
|
- _ ->
|
|
|
- Diffs =
|
|
|
- lists:filtermap(
|
|
|
- fun({App, {Upgrade, Downgrade, OldUpgrade, OldDowngrade}}) ->
|
|
|
- case parse_appup_diffs(Upgrade, OldUpgrade,
|
|
|
- Downgrade, OldDowngrade) of
|
|
|
- ok ->
|
|
|
- false;
|
|
|
- {diffs, Diffs} ->
|
|
|
- {true, {App, Diffs}}
|
|
|
- end
|
|
|
- end,
|
|
|
- AppupChanges),
|
|
|
- case Diffs =:= [] of
|
|
|
- true ->
|
|
|
- ok;
|
|
|
- false ->
|
|
|
- set_invalid(),
|
|
|
- log("ERROR: The appup files are incomplete. Missing changes:~n ~p",
|
|
|
- [Diffs])
|
|
|
- end
|
|
|
- end;
|
|
|
- false ->
|
|
|
- update_appups(AppupChanges)
|
|
|
- end,
|
|
|
- check_appup_files(),
|
|
|
- warn_and_exit(is_valid()).
|
|
|
+ ok = update_appups(AppupChanges),
|
|
|
+ ok = check_appup_files(),
|
|
|
+ ok = warn_and_exit(is_valid()).
|
|
|
|
|
|
warn_and_exit(true) ->
|
|
|
log("
|
|
|
@@ -136,47 +108,34 @@ warn_and_exit(false) ->
|
|
|
log("~nERROR: Incomplete appups found. Please inspect the output for more details.~n"),
|
|
|
halt(1).
|
|
|
|
|
|
-prepare(Baseline, Options = #{make_command := MakeCommand, beams_dir := BeamDir, binary_rel_url := BinRel}) ->
|
|
|
+prepare(Baseline, Options = #{make_command := MakeCommand, beams_dir := BeamDir}) ->
|
|
|
log("~n===================================~n"
|
|
|
"Baseline: ~s"
|
|
|
"~n===================================~n", [Baseline]),
|
|
|
log("Building the current version...~n"),
|
|
|
- bash(MakeCommand),
|
|
|
- log("Downloading and building the previous release...~n"),
|
|
|
+ ok = bash(MakeCommand),
|
|
|
PrevRelDir =
|
|
|
- case BinRel of
|
|
|
+ case maps:get(prev_beams_dir, Options, undefined) of
|
|
|
undefined ->
|
|
|
+ log("Building the previous release...~n"),
|
|
|
{ok, PrevRootDir} = build_prev_release(Baseline, Options),
|
|
|
filename:join(PrevRootDir, BeamDir);
|
|
|
- {ok, _URL} ->
|
|
|
- {ok, PrevRootDir} = download_prev_release(Baseline, Options),
|
|
|
- PrevRootDir
|
|
|
+ Dir ->
|
|
|
+ %% already built
|
|
|
+ Dir
|
|
|
end,
|
|
|
{BeamDir, PrevRelDir}.
|
|
|
|
|
|
build_prev_release(Baseline, #{clone_url := Repo, make_command := MakeCommand}) ->
|
|
|
- BaseDir = "/tmp/emqx-baseline/",
|
|
|
+ BaseDir = "/tmp/emqx-appup-base/",
|
|
|
Dir = filename:basename(Repo, ".git") ++ [$-|Baseline],
|
|
|
- %% TODO: shallow clone
|
|
|
Script = "mkdir -p ${BASEDIR} &&
|
|
|
cd ${BASEDIR} &&
|
|
|
- { [ -d ${DIR} ] || git clone --branch ${TAG} ${REPO} ${DIR}; } &&
|
|
|
+ { [ -d ${DIR} ] || git clone --depth 1 --branch ${TAG} ${REPO} ${DIR}; } &&
|
|
|
cd ${DIR} &&" ++ MakeCommand,
|
|
|
Env = [{"REPO", Repo}, {"TAG", Baseline}, {"BASEDIR", BaseDir}, {"DIR", Dir}],
|
|
|
- bash(Script, Env),
|
|
|
- {ok, filename:join(BaseDir, Dir)}.
|
|
|
-
|
|
|
-download_prev_release(Tag, #{binary_rel_url := {ok, URL0}, clone_url := Repo}) ->
|
|
|
- URL = string:replace(URL0, "%TAG%", Tag, all),
|
|
|
- BaseDir = "/tmp/emqx-baseline-bin/",
|
|
|
- Dir = filename:basename(Repo, ".git") ++ [$-|Tag],
|
|
|
- Filename = filename:join(BaseDir, Dir),
|
|
|
- Script = "mkdir -p ${OUTFILE} &&
|
|
|
- wget -c -O ${OUTFILE}.tar.gz ${URL} &&
|
|
|
- tar -zxf ${OUTFILE}.tar.gz -C ${OUTFILE}",
|
|
|
- Env = [{"TAG", Tag}, {"OUTFILE", Filename}, {"URL", URL}],
|
|
|
- bash(Script, Env),
|
|
|
- {ok, Filename}.
|
|
|
+ ok = bash(Script, Env),
|
|
|
+ {ok, filename:join([BaseDir, Dir, "_build/*/lib"])}.
|
|
|
|
|
|
find_upstream_repo(Remote) ->
|
|
|
string:trim(os:cmd("git remote get-url " ++ Remote)).
|
|
|
@@ -205,16 +164,16 @@ find_appup_actions(_App, AppIdx, AppIdx) ->
|
|
|
find_appup_actions(App,
|
|
|
CurrAppIdx = #app{version = CurrVersion},
|
|
|
PrevAppIdx = #app{version = PrevVersion}) ->
|
|
|
- {OldUpgrade0, OldDowngrade0} = find_old_appup_actions(App, PrevVersion),
|
|
|
+ {OldUpgrade0, OldDowngrade0} = find_base_appup_actions(App, PrevVersion),
|
|
|
OldUpgrade = ensure_all_patch_versions(App, CurrVersion, OldUpgrade0),
|
|
|
OldDowngrade = ensure_all_patch_versions(App, CurrVersion, OldDowngrade0),
|
|
|
- Upgrade = merge_update_actions(App, diff_app(App, CurrAppIdx, PrevAppIdx), OldUpgrade),
|
|
|
- Downgrade = merge_update_actions(App, diff_app(App, PrevAppIdx, CurrAppIdx), OldDowngrade),
|
|
|
- if OldUpgrade =:= Upgrade andalso OldDowngrade =:= Downgrade ->
|
|
|
- %% The appup file has been already updated:
|
|
|
- [];
|
|
|
- true ->
|
|
|
- [{App, {Upgrade, Downgrade, OldUpgrade, OldDowngrade}}]
|
|
|
+ UpDiff = diff_app(up, App, CurrAppIdx, PrevAppIdx),
|
|
|
+ DownDiff = diff_app(down, App, PrevAppIdx, CurrAppIdx),
|
|
|
+ Upgrade = merge_update_actions(App, UpDiff, OldUpgrade, PrevVersion),
|
|
|
+ Downgrade = merge_update_actions(App, DownDiff, OldDowngrade, PrevVersion),
|
|
|
+ case OldUpgrade =:= Upgrade andalso OldDowngrade =:= Downgrade of
|
|
|
+ true -> [];
|
|
|
+ false -> [{App, {Upgrade, Downgrade, OldUpgrade, OldDowngrade}}]
|
|
|
end.
|
|
|
|
|
|
%% To avoid missing one patch version when upgrading, we try to
|
|
|
@@ -265,7 +224,7 @@ diff_appup_instructions(ComputedChanges, PresentChanges) ->
|
|
|
[],
|
|
|
ComputedChanges).
|
|
|
|
|
|
-%% For external dependencies, checks if any missing diffs are present
|
|
|
+%% checks if any missing diffs are present
|
|
|
%% and groups them by `up' and `down' types.
|
|
|
parse_appup_diffs(Upgrade, OldUpgrade, Downgrade, OldDowngrade) ->
|
|
|
DiffUp = diff_appup_instructions(Upgrade, OldUpgrade),
|
|
|
@@ -275,61 +234,91 @@ parse_appup_diffs(Upgrade, OldUpgrade, Downgrade, OldDowngrade) ->
|
|
|
%% no diff for external dependency; ignore
|
|
|
ok;
|
|
|
_ ->
|
|
|
- set_invalid(),
|
|
|
Diffs = #{ up => DiffUp
|
|
|
, down => DiffDown
|
|
|
},
|
|
|
{diffs, Diffs}
|
|
|
end.
|
|
|
|
|
|
+%% TODO: handle regexes
|
|
|
+%% Since the first argument may be a regex itself, we would need to
|
|
|
+%% check if it is "contained" within other regexes inside list of
|
|
|
+%% versions in the second argument.
|
|
|
find_matching_version(VsnOrRegex, PresentChanges) ->
|
|
|
proplists:get_value(VsnOrRegex, PresentChanges).
|
|
|
|
|
|
-find_old_appup_actions(App, PrevVersion) ->
|
|
|
- {Upgrade0, Downgrade0} =
|
|
|
- case locate(ebin_current, App, ".appup") of
|
|
|
- {ok, AppupFile} ->
|
|
|
- log("Found the previous appup file: ~s~n", [AppupFile]),
|
|
|
- {_, U, D} = read_appup(AppupFile),
|
|
|
- {U, D};
|
|
|
+find_base_appup_actions(App, PrevVersion) ->
|
|
|
+ {Upgrade, Downgrade} =
|
|
|
+ case locate_appup(App) of
|
|
|
+ {ok, AppupSrcFile} ->
|
|
|
+ log("INFO: Using ~s as a source of previous update actions~n", [AppupSrcFile]),
|
|
|
+ read_appup(AppupSrcFile);
|
|
|
undefined ->
|
|
|
- %% Fallback to the app.src file, in case the
|
|
|
- %% application doesn't have a release (useful for the
|
|
|
- %% apps that live outside the EMQX monorepo):
|
|
|
- case locate(src, App, ".appup.src") of
|
|
|
- {ok, AppupSrcFile} ->
|
|
|
- log("Using ~s as a source of previous update actions~n", [AppupSrcFile]),
|
|
|
- {_, U, D} = read_appup(AppupSrcFile),
|
|
|
- {U, D};
|
|
|
- undefined ->
|
|
|
- {[], []}
|
|
|
- end
|
|
|
+ log("INFO: no appup base found for ~p~n", [App]),
|
|
|
+ {[], []}
|
|
|
end,
|
|
|
- {ensure_version(PrevVersion, Upgrade0), ensure_version(PrevVersion, Downgrade0)}.
|
|
|
+ {ensure_version(PrevVersion, Upgrade), ensure_version(PrevVersion, Downgrade)}.
|
|
|
|
|
|
-merge_update_actions(App, Changes, Vsns) ->
|
|
|
+merge_update_actions(App, Changes, Vsns, PrevVersion) ->
|
|
|
lists:map(fun(Ret = {<<".*">>, _}) ->
|
|
|
Ret;
|
|
|
({Vsn, Actions}) ->
|
|
|
- {Vsn, do_merge_update_actions(App, Changes, Actions)}
|
|
|
+ case is_skipped_version(App, Vsn, PrevVersion) of
|
|
|
+ true ->
|
|
|
+ log("WARN: ~p has version ~s skipped over?~n", [App, Vsn]),
|
|
|
+ {Vsn, Actions};
|
|
|
+ false ->
|
|
|
+ {Vsn, do_merge_update_actions(App, Changes, Actions)}
|
|
|
+ end
|
|
|
end,
|
|
|
Vsns).
|
|
|
|
|
|
+%% say current version is 1.1.3, and the compare base is version 1.1.1,
|
|
|
+%% but there is a 1.1.2 in appup we may skip merging instructions for
|
|
|
+%% 1.1.2 because it's not used and no way to know what has been changed
|
|
|
+is_skipped_version(App, Vsn, PrevVersion) when is_list(Vsn) andalso is_list(PrevVersion) ->
|
|
|
+ case is_app_external(App) andalso parse_version_number(Vsn) of
|
|
|
+ {ok, VsnTuple} ->
|
|
|
+ case parse_version_number(PrevVersion) of
|
|
|
+ {ok, PrevVsnTuple} ->
|
|
|
+ VsnTuple > PrevVsnTuple;
|
|
|
+ _ ->
|
|
|
+ false
|
|
|
+ end;
|
|
|
+ _ ->
|
|
|
+ false
|
|
|
+ end;
|
|
|
+is_skipped_version(_App, _Vsn, _PrevVersion) ->
|
|
|
+ %% if app version is a regexp, we don't know for sure
|
|
|
+ %% return 'false' to be on the safe side
|
|
|
+ false.
|
|
|
+
|
|
|
do_merge_update_actions(App, {New0, Changed0, Deleted0}, OldActions) ->
|
|
|
AppSpecific = app_specific_actions(App) -- OldActions,
|
|
|
AlreadyHandled = lists:flatten(lists:map(fun process_old_action/1, OldActions)),
|
|
|
New = New0 -- AlreadyHandled,
|
|
|
Changed = Changed0 -- AlreadyHandled,
|
|
|
Deleted = Deleted0 -- AlreadyHandled,
|
|
|
- Reloads = [{load_module, M, brutal_purge, soft_purge, []}
|
|
|
- || not contains_restart_application(App, OldActions),
|
|
|
- M <- Changed ++ New],
|
|
|
+ HasRestart = contains_restart_application(App, OldActions),
|
|
|
+ Actions =
|
|
|
+ case HasRestart of
|
|
|
+ true ->
|
|
|
+ [];
|
|
|
+ false ->
|
|
|
+ [{load_module, M, brutal_purge, soft_purge, []} || M <- Changed] ++
|
|
|
+ [{add_module, M} || M <- New]
|
|
|
+ end,
|
|
|
{OldActionsWithStop, OldActionsAfterStop} =
|
|
|
find_application_stop_instruction(App, OldActions),
|
|
|
OldActionsWithStop ++
|
|
|
- Reloads ++
|
|
|
+ Actions ++
|
|
|
OldActionsAfterStop ++
|
|
|
- [{delete_module, M} || M <- Deleted] ++
|
|
|
+ case HasRestart of
|
|
|
+ true ->
|
|
|
+ [];
|
|
|
+ false ->
|
|
|
+ [{delete_module, M} || M <- Deleted]
|
|
|
+ end ++
|
|
|
AppSpecific.
|
|
|
|
|
|
%% If an entry restarts an application, there's no need to use
|
|
|
@@ -358,8 +347,12 @@ find_application_stop_instruction(Application, Actions) ->
|
|
|
%% already handled
|
|
|
process_old_action({purge, Modules}) ->
|
|
|
Modules;
|
|
|
+process_old_action({add_module, Module}) ->
|
|
|
+ [Module];
|
|
|
process_old_action({delete_module, Module}) ->
|
|
|
[Module];
|
|
|
+process_old_action({update, Module, _Change}) ->
|
|
|
+ [Module];
|
|
|
process_old_action(LoadModule) when is_tuple(LoadModule) andalso
|
|
|
element(1, LoadModule) =:= load_module ->
|
|
|
element(2, LoadModule);
|
|
|
@@ -420,11 +413,19 @@ vsn_number_to_string({Major, Minor, Patch}) ->
|
|
|
|
|
|
read_appup(File) ->
|
|
|
%% NOTE: appup file is a script, it may contain variables or functions.
|
|
|
+ case do_read_appup(File) of
|
|
|
+ {ok, {U, D}} -> {U, D};
|
|
|
+ {error, Reason} -> fail("Failed to parse appup file ~p~n~p", [File, Reason])
|
|
|
+ end.
|
|
|
+
|
|
|
+do_read_appup(File) ->
|
|
|
case file:script(File, [{'VSN', "VSN"}]) of
|
|
|
- {ok, Terms} ->
|
|
|
- Terms;
|
|
|
- Error ->
|
|
|
- fail("Failed to parse appup file ~s: ~p", [File, Error])
|
|
|
+ {ok, {_, U, D}} ->
|
|
|
+ {ok, {U, D}};
|
|
|
+ {ok, Other} ->
|
|
|
+ {error, {bad_appup_format, Other}};
|
|
|
+ {error, Reason} ->
|
|
|
+ {error, Reason}
|
|
|
end.
|
|
|
|
|
|
check_appup_files() ->
|
|
|
@@ -439,52 +440,77 @@ update_appups(Changes) ->
|
|
|
Changes).
|
|
|
|
|
|
do_update_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade) ->
|
|
|
- case locate(src, App, ".appup.src") of
|
|
|
+ case locate_current_src(App, ".appup.src") of
|
|
|
{ok, AppupFile} ->
|
|
|
case contains_contents(AppupFile, Upgrade, Downgrade) of
|
|
|
true ->
|
|
|
ok;
|
|
|
false ->
|
|
|
- render_appfile(AppupFile, Upgrade, Downgrade)
|
|
|
+ render_appup(App, AppupFile, Upgrade, Downgrade)
|
|
|
end;
|
|
|
undefined ->
|
|
|
- case create_stub(App) of
|
|
|
- {ok, AppupFile} ->
|
|
|
- render_appfile(AppupFile, Upgrade, Downgrade);
|
|
|
- false ->
|
|
|
- case parse_appup_diffs(Upgrade, OldUpgrade,
|
|
|
- Downgrade, OldDowngrade) of
|
|
|
- ok ->
|
|
|
- %% no diff for external dependency; ignore
|
|
|
- ok;
|
|
|
- {diffs, Diffs} ->
|
|
|
- set_invalid(),
|
|
|
- log("ERROR: Appup file for the external dependency '~p' is not complete.~n Missing changes: ~100p~n", [App, Diffs]),
|
|
|
- log("NOTE: Some changes above might be already covered by regexes.~n")
|
|
|
- end
|
|
|
- end
|
|
|
+ maybe_create_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade)
|
|
|
+ end.
|
|
|
+
|
|
|
+maybe_create_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade) ->
|
|
|
+ case create_stub(App) of
|
|
|
+ {ok, AppupFile} ->
|
|
|
+ render_appup(App, AppupFile, Upgrade, Downgrade);
|
|
|
+ external ->
|
|
|
+ %% for external appup, the best we can do is to validate it
|
|
|
+ _ = check_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade),
|
|
|
+ ok
|
|
|
+ end.
|
|
|
+
|
|
|
+check_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade) ->
|
|
|
+ case parse_appup_diffs(Upgrade, OldUpgrade, Downgrade, OldDowngrade) of
|
|
|
+ ok ->
|
|
|
+ %% no diff for external dependency; ignore
|
|
|
+ ok;
|
|
|
+ {diffs, Diffs} ->
|
|
|
+ set_invalid(),
|
|
|
+ log("ERROR: Appup file for '~p' is not complete.~n"
|
|
|
+ "Missing:~100p~n", [App, Diffs]),
|
|
|
+ notok
|
|
|
end.
|
|
|
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
%% Appup file creation
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
|
|
-render_appfile(File, Upgrade, Downgrade) ->
|
|
|
- IOList = io_lib:format("%% -*- mode: erlang -*-\n{VSN,~n ~p,~n ~p}.~n", [Upgrade, Downgrade]),
|
|
|
+render_appup(App, File, Up, Down) ->
|
|
|
+ IsCheck = getopt(check),
|
|
|
+ case do_read_appup(File) of
|
|
|
+ {ok, {U, D}} when U =:= Up andalso D =:= Down ->
|
|
|
+ ok;
|
|
|
+ {ok, {OldU, OldD}} when IsCheck ->
|
|
|
+ check_appup(App, Up, Down, OldU, OldD);
|
|
|
+ {ok, {_, _}} ->
|
|
|
+ do_render_appup(File, Up, Down);
|
|
|
+ {error, enoent} when IsCheck ->
|
|
|
+ %% failed to read old file, exit
|
|
|
+ log("ERROR: ~s is missing", [File]),
|
|
|
+ set_invalid()
|
|
|
+ end.
|
|
|
+
|
|
|
+do_render_appup(File, Up, Down) ->
|
|
|
+ IOList = io_lib:format("%% -*- mode: erlang -*-~n"
|
|
|
+ "%% Unless you know what you are doing, DO NOT edit manually!!~n"
|
|
|
+ "{VSN,~n ~p,~n ~p}.~n", [Up, Down]),
|
|
|
ok = file:write_file(File, IOList).
|
|
|
|
|
|
create_stub(App) ->
|
|
|
Ext = ".app.src",
|
|
|
- case locate(src, App, Ext) of
|
|
|
+ case locate_current_src(App, Ext) of
|
|
|
{ok, AppSrc} ->
|
|
|
DirName = filename:dirname(AppSrc),
|
|
|
AppupFile = filename:basename(AppSrc, Ext) ++ ".appup.src",
|
|
|
Default = {<<".*">>, []},
|
|
|
AppupFileFullpath = filename:join(DirName, AppupFile),
|
|
|
- render_appfile(AppupFileFullpath, [Default], [Default]),
|
|
|
+ render_appup(App, AppupFileFullpath, [Default], [Default]),
|
|
|
{ok, AppupFileFullpath};
|
|
|
undefined ->
|
|
|
- false
|
|
|
+ external
|
|
|
end.
|
|
|
|
|
|
%% we check whether the destination file already has the contents we
|
|
|
@@ -503,8 +529,11 @@ contains_contents(File, Upgrade, Downgrade) ->
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
|
|
index_apps(ReleaseDir) ->
|
|
|
- Apps0 = maps:from_list([index_app(filename:join(ReleaseDir, AppFile)) ||
|
|
|
- AppFile <- filelib:wildcard("**/ebin/*.app", ReleaseDir)]),
|
|
|
+ log("INFO: indexing apps in ~s~n", [ReleaseDir]),
|
|
|
+ AppFiles0 = filelib:wildcard("**/ebin/*.app", ReleaseDir),
|
|
|
+ %% everything in _build sub-dir e.g. cuttlefish/_build should be ignored
|
|
|
+ AppFiles = lists:filter(fun(File) -> re:run(File, "_build") =:= nomatch end, AppFiles0),
|
|
|
+ Apps0 = maps:from_list([index_app(filename:join(ReleaseDir, AppFile)) || AppFile <- AppFiles]),
|
|
|
maps:without(ignored_apps(), Apps0).
|
|
|
|
|
|
index_app(AppFile) ->
|
|
|
@@ -517,7 +546,7 @@ index_app(AppFile) ->
|
|
|
, modules = Modules
|
|
|
}}.
|
|
|
|
|
|
-diff_app(App,
|
|
|
+diff_app(UpOrDown, App,
|
|
|
#app{version = NewVersion, modules = NewModules},
|
|
|
#app{version = OldVersion, modules = OldModules}) ->
|
|
|
{New, Changed} =
|
|
|
@@ -535,18 +564,32 @@ diff_app(App,
|
|
|
, NewModules
|
|
|
),
|
|
|
Deleted = maps:keys(maps:without(maps:keys(NewModules), OldModules)),
|
|
|
- NChanges = length(New) + length(Changed) + length(Deleted),
|
|
|
- if NewVersion =:= OldVersion andalso NChanges > 0 ->
|
|
|
- set_invalid(),
|
|
|
- log("ERROR: Application '~p' contains changes, but its version is not updated~n", [App]);
|
|
|
- NewVersion > OldVersion ->
|
|
|
- log("INFO: Application '~p' has been updated: ~p -> ~p~n", [App, OldVersion, NewVersion]),
|
|
|
+ Changes = lists:filter(fun({_T, L}) -> length(L) > 0 end,
|
|
|
+ [{added, New}, {changed, Changed}, {deleted, Deleted}]),
|
|
|
+ case NewVersion =:= OldVersion of
|
|
|
+ true when Changes =:= [] ->
|
|
|
+ %% no change
|
|
|
ok;
|
|
|
- true ->
|
|
|
+ true ->
|
|
|
+ set_invalid(),
|
|
|
+ case UpOrDown =:= up of
|
|
|
+ true ->
|
|
|
+ %% only log for the upgrade case because it would be the same result
|
|
|
+ log("ERROR: Application '~p' contains changes, but its version is not updated. ~s",
|
|
|
+ [App, format_changes(Changes)]);
|
|
|
+ false ->
|
|
|
+ ok
|
|
|
+ end;
|
|
|
+ false ->
|
|
|
+ log("INFO: Application '~p' has been updated: ~p --[~p]--> ~p~n", [App, OldVersion, UpOrDown, NewVersion]),
|
|
|
+ log("INFO: changes [~p]: ~p~n", [UpOrDown, Changes]),
|
|
|
ok
|
|
|
end,
|
|
|
{New, Changed, Deleted}.
|
|
|
|
|
|
+format_changes(Changes) ->
|
|
|
+ lists:map(fun({Tag, List}) -> io_lib:format("~p: ~p~n", [Tag, List]) end, Changes).
|
|
|
+
|
|
|
-spec hashsums(file:filename()) -> #{module() => binary()}.
|
|
|
hashsums(EbinDir) ->
|
|
|
maps:from_list(lists:map(
|
|
|
@@ -560,7 +603,7 @@ hashsums(EbinDir) ->
|
|
|
|
|
|
is_app_external(App) ->
|
|
|
Ext = ".app.src",
|
|
|
- case locate(src, App, Ext) of
|
|
|
+ case locate_current_src(App, Ext) of
|
|
|
{ok, _} ->
|
|
|
false;
|
|
|
undefined ->
|
|
|
@@ -576,8 +619,16 @@ init_globals(Options) ->
|
|
|
ets:insert(globals, {valid, true}),
|
|
|
ets:insert(globals, {options, Options}).
|
|
|
|
|
|
+putopt(Option, Value) ->
|
|
|
+ ets:insert(globals, {{option, Option}, Value}).
|
|
|
+
|
|
|
getopt(Option) ->
|
|
|
- maps:get(Option, ets:lookup_element(globals, options, 2)).
|
|
|
+ case ets:lookup(globals, {option, Option}) of
|
|
|
+ [] ->
|
|
|
+ maps:get(Option, ets:lookup_element(globals, options, 2));
|
|
|
+ [{_, V}] ->
|
|
|
+ V
|
|
|
+ end.
|
|
|
|
|
|
%% Set a global flag that something about the appfiles is invalid
|
|
|
set_invalid() ->
|
|
|
@@ -590,33 +641,48 @@ is_valid() ->
|
|
|
%% Utility functions
|
|
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
|
|
|
|
-%% Locate a file in a specified application
|
|
|
-locate(ebin_current, App, Suffix) ->
|
|
|
- ReleaseDir = getopt(beams_dir),
|
|
|
- AppStr = atom_to_list(App),
|
|
|
- case filelib:wildcard(ReleaseDir ++ "/**/ebin/" ++ AppStr ++ Suffix) of
|
|
|
- [File] ->
|
|
|
+locate_appup(App) ->
|
|
|
+ case locate_current_rel(App, ".appup.src") of
|
|
|
+ {ok, File} ->
|
|
|
{ok, File};
|
|
|
- [] ->
|
|
|
- undefined
|
|
|
- end;
|
|
|
-locate(src, App, Suffix) ->
|
|
|
- AppStr = atom_to_list(App),
|
|
|
+ undefined ->
|
|
|
+ %% fallback to .appup
|
|
|
+ locate_current_rel(App, ".appup")
|
|
|
+ end.
|
|
|
+
|
|
|
+locate_current_rel(App, Suffix) ->
|
|
|
+ CurDir = getopt(beams_dir),
|
|
|
+ do_locate(filename:join([CurDir, "**"]), App, Suffix).
|
|
|
+
|
|
|
+%% Locate a file in a specified application
|
|
|
+locate_current_src(App, Suffix) ->
|
|
|
SrcDirs = getopt(src_dirs),
|
|
|
- case filelib:wildcard(SrcDirs ++ AppStr ++ Suffix) of
|
|
|
+ do_locate(SrcDirs, App, Suffix).
|
|
|
+
|
|
|
+do_locate(Dir, App, Suffix) ->
|
|
|
+ AppStr = atom_to_list(App),
|
|
|
+ Pattern = filename:join(Dir, AppStr ++ Suffix),
|
|
|
+ case find_app(Pattern) of
|
|
|
[File] ->
|
|
|
{ok, File};
|
|
|
[] ->
|
|
|
- undefined
|
|
|
+ undefined;
|
|
|
+ Files ->
|
|
|
+ error({more_than_one_app_found, Files})
|
|
|
end.
|
|
|
|
|
|
+find_app(Pattern) ->
|
|
|
+ lists:filter(fun(D) -> re:run(D, "apps/.*/_build") =:= nomatch end,
|
|
|
+ filelib:wildcard(Pattern)).
|
|
|
+
|
|
|
+bash(undefined) -> ok;
|
|
|
bash(Script) ->
|
|
|
bash(Script, []).
|
|
|
|
|
|
bash(Script, Env) ->
|
|
|
log("+ ~s~n+ Env: ~p~n", [Script, Env]),
|
|
|
case cmd("bash", #{args => ["-c", Script], env => Env}) of
|
|
|
- 0 -> true;
|
|
|
+ 0 -> ok;
|
|
|
_ -> fail("Failed to run command: ~s", [Script])
|
|
|
end.
|
|
|
|