Permalink
Browse files

Add 'generate-upgrade' command

To support OTP release upgrades I have added support for building
upgrade packages. Support for this is included in the
rebar_upgrade module, specifically generate_upgrade/2. It requires
one variable to be set on the command line 'previous_release' which
is the absolute path or relative path from 'rel/' to the previous
release one is upgrading from. Running an upgrade will create the
needed files, including a relup and result in a tarball containing
the upgrade being written to 'rel/'. When done it cleans up the
temporary files systools created.

Usage:
$ rebar generate-upgrade previous_release=/path/to/old/version

This also includes a dummy application that can be used to test
upgrades as well as an example.

Special thanks to Daniel Reverri, Jesper Louis Andersen and
Richard Jones for comments and patches.
  • Loading branch information...
1 parent 3fd3bfc commit 5298e93a180e6db87a33f26eb6a2db06e8065dc7 joewilliams committed with tuncer Jan 27, 2011
View
@@ -32,6 +32,7 @@
rebar_require_vsn,
rebar_subdirs,
rebar_templater,
+ rebar_upgrade,
rebar_utils,
rebar_xref,
getopt,
@@ -79,7 +80,8 @@
]},
{rel_dir, [
- rebar_reltool
+ rebar_reltool,
+ rebar_upgrade
]}
]}
]}
@@ -117,13 +117,19 @@ case "$1" in
$ERTS_PATH/to_erl $PIPE_DIR
;;
- console)
+ console|console_clean)
+ # .boot file typically just $SCRIPT (ie, the app name)
+ # however, for debugging, sometimes start_clean.boot is useful:
+ case "$1" in
+ console) BOOTFILE=$SCRIPT ;;
+ console_clean) BOOTFILE=start_clean ;;
+ esac
# Setup beam-required vars
ROOTDIR=$RUNNER_BASE_DIR
BINDIR=$ROOTDIR/erts-$ERTS_VSN/bin
EMU=beam
PROGNAME=`echo $0 | sed 's/.*\\///'`
- CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$SCRIPT -embedded -config $RUNNER_ETC_DIR/app.config -args_file $RUNNER_ETC_DIR/vm.args -- ${1+"$@"}"
+ CMD="$BINDIR/erlexec -boot $RUNNER_BASE_DIR/releases/$APP_VSN/$BOOTFILE -embedded -config $RUNNER_ETC_DIR/app.config -args_file $RUNNER_ETC_DIR/vm.args -- ${1+"$@"}"
export EMU
export ROOTDIR
export BINDIR
View
@@ -211,6 +211,8 @@ delete-deps Delete fetched dependencies
generate [dump_spec=0/1] Build release with reltool
+generate-upgrade [previous_release=path] Build an upgrade package
+
eunit [suite=foo] Run eunit [test/foo_tests.erl] tests
ct [suite=] [case=] Run common_test suites in ./test
@@ -264,8 +266,8 @@ filter_flags([Item | Rest], Commands) ->
command_names() ->
["build-plt", "check-deps", "check-plt", "clean", "compile", "create",
"create-app", "create-node", "ct", "delete-deps", "dialyze", "doc",
- "eunit", "generate", "get-deps", "help", "list-templates", "update-deps",
- "version", "xref"].
+ "eunit", "generate", "generate-upgrade", "get-deps", "help",
+ "list-templates", "update-deps", "version", "xref"].
unabbreviate_command_names([]) ->
[];
View
@@ -0,0 +1,206 @@
+%% -*- tab-width: 4;erlang-indent-level: 4;indent-tabs-mode: nil -*-
+%% ex: ts=4 sw=4 et
+%% -------------------------------------------------------------------
+%%
+%% rebar: Erlang Build Tools
+%%
+%% Copyright (c) 2011 Joe Williams <joe@joetify.com>
+%%
+%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%% of this software and associated documentation files (the "Software"), to deal
+%% in the Software without restriction, including without limitation the rights
+%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%% copies of the Software, and to permit persons to whom the Software is
+%% furnished to do so, subject to the following conditions:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%% THE SOFTWARE.
+%% -------------------------------------------------------------------
+
+-module(rebar_upgrade).
+
+-include("rebar.hrl").
+-include_lib("kernel/include/file.hrl").
+
+-export(['generate-upgrade'/2]).
+
+%% public api
+
+'generate-upgrade'(_Config, ReltoolFile) ->
+ case rebar_config:get_global(previous_release, false) of
+ false ->
+ ?ABORT("previous_release=PATH is required to "
+ "create upgrade package~n", []);
+ OldVerPath ->
+ %% Run checks to make sure that building a package is possible
+ {NewName, NewVer} = run_checks(OldVerPath, ReltoolFile),
+ NameVer = NewName ++ "_" ++ NewVer,
+
+ %% Save the code path prior to doing anything
+ OrigPath = code:get_path(),
+
+ %% Prepare the environment for building the package
+ ok = setup(OldVerPath, NewName, NewVer, NameVer),
+
+ %% Build the package
+ run_systools(NameVer, NewName),
+
+ %% Boot file changes
+ boot_files(NewVer, NewName),
+
+ %% Extract upgrade and tar it back up with changes
+ make_tar(NameVer),
+
+ %% Clean up files that systools created
+ ok = cleanup(NameVer, NewName, NewVer),
+
+ %% Restore original path
+ true = code:set_path(OrigPath),
+
+ ok
+ end.
+
+%% internal api
+
+run_checks(OldVerPath, ReltoolFile) ->
+ true = prop_check(filelib:is_dir(OldVerPath),
+ "Release directory doesn't exist (~p)~n", [OldVerPath]),
+
+ {Name, Ver} = get_release_name(ReltoolFile),
+
+ NamePath = filename:join([".", Name]),
+ true = prop_check(filelib:is_dir(NamePath),
+ "Release directory doesn't exist (~p)~n", [NamePath]),
+
+ {NewName, NewVer} = get_release_version(Name, NamePath),
+ {OldName, OldVer} = get_release_version(Name, OldVerPath),
+
+ true = prop_check(NewName == OldName,
+ "New and old .rel release names do not match~n", []),
+ true = prop_check(Name == NewName,
+ "Reltool and .rel release names do not match~n", []),
+ true = prop_check(NewVer =/= OldVer,
+ "New and old .rel contain the same version~n", []),
+ true = prop_check(Ver == NewVer,
+ "Reltool and .rel versions do not match~n", []),
+
+ {NewName, NewVer}.
+
+get_release_name(ReltoolFile) ->
+ %% expect sys to be the first proplist in reltool.config
+ case file:consult(ReltoolFile) of
+ {ok, [{sys, Config}| _]} ->
+ %% expect the first rel in the proplist to be the one you want
+ {rel, Name, Ver, _} = proplists:lookup(rel, Config),
+ {Name, Ver};
+ _ ->
+ ?ABORT("Failed to parse ~s~n", [ReltoolFile])
+ end.
+
+get_release_version(Name, Path) ->
+ [RelFile] = filelib:wildcard(filename:join([Path, "releases", "*",
+ Name ++ ".rel"])),
+ [BinDir|_] = re:replace(RelFile, Name ++ "\\.rel", ""),
+ {ok, [{release, {Name1, Ver}, _, _}]} =
+ file:consult(filename:join([binary_to_list(BinDir),
+ Name ++ ".rel"])),
+ {Name1, Ver}.
+
+prop_check(true, _, _) -> true;
+prop_check(false, Msg, Args) -> ?ABORT(Msg, Args).
+
+setup(OldVerPath, NewName, NewVer, NameVer) ->
+ NewRelPath = filename:join([".", NewName]),
+ Src = filename:join([NewRelPath, "releases",
+ NewVer, NewName ++ ".rel"]),
+ Dst = filename:join([".", NameVer ++ ".rel"]),
+ {ok, _} = file:copy(Src, Dst),
+ ok = code:add_pathsa(
+ lists:append([
+ filelib:wildcard(filename:join([OldVerPath,
+ "releases", "*"])),
+ filelib:wildcard(filename:join([OldVerPath,
+ "lib", "*", "ebin"])),
+ filelib:wildcard(filename:join([NewRelPath,
+ "lib", "*", "ebin"])),
+ filelib:wildcard(filename:join([NewRelPath, "*"]))
+ ])).
+
+run_systools(NewVer, Name) ->
+ Opts = [silent],
+ NameList = [Name],
+ case systools:make_relup(NewVer, NameList, NameList, Opts) of
+ {error, _, _Message} ->
+ ?ABORT("Systools aborted with: ~p~n", [_Message]);
+ _ ->
+ ?DEBUG("Relup created~n", []),
+ case systools:make_script(NewVer, Opts) of
+ {error, _, _Message1} ->
+ ?ABORT("Systools aborted with: ~p~n", [_Message1]);
+ _ ->
+ ?DEBUG("Script created~n", []),
+ case systools:make_tar(NewVer, Opts) of
+ {error, _, _Message2} ->
+ ?ABORT("Systools aborted with: ~p~n", [_Message2]);
+ _ ->
+ ok
+ end
+ end
+ end.
+
+boot_files(Ver, Name) ->
+ ok = file:make_dir(filename:join([".", "releases"])),
+ ok = file:make_dir(filename:join([".", "releases", Ver])),
+ ok = file:make_symlink(filename:join(["start.boot"]),
+ filename:join([".", "releases", Ver, Name ++ ".boot"])),
+ {ok, _} = file:copy(filename:join([".", Name, "releases", Ver, "start_clean.boot"]),
+ filename:join([".", "releases", Ver, "start_clean.boot"])).
+
+make_tar(NameVer) ->
+ Filename = NameVer ++ ".tar.gz",
+ ok = erl_tar:extract(Filename, [compressed]),
+ ok = file:delete(Filename),
+ {ok, Tar} = erl_tar:open(Filename, [write, compressed]),
+ ok = erl_tar:add(Tar, "lib", []),
+ ok = erl_tar:add(Tar, "releases", []),
+ ok = erl_tar:close(Tar),
+ ?CONSOLE("~s upgrade package created~n", [NameVer]).
+
+cleanup(NameVer, Name, Ver) ->
+ ?DEBUG("Removing files needed for building the upgrade~n", []),
+ Files = [
+ filename:join([".", "releases", Ver, Name ++ ".boot"]),
+ filename:join([".", NameVer ++ ".rel"]),
+ filename:join([".", NameVer ++ ".boot"]),
+ filename:join([".", NameVer ++ ".script"]),
+ filename:join([".", "relup"])
+ ],
+ [ok = file:delete(F) || F <- Files],
+
+ ok = remove_dir_tree("releases"),
+ ok = remove_dir_tree("lib").
+
+%% taken from http://www.erlang.org/doc/system_principles/create_target.html
+remove_dir_tree(Dir) ->
+ remove_all_files(".", [Dir]).
+remove_all_files(Dir, Files) ->
+ lists:foreach(fun(File) ->
+ FilePath = filename:join([Dir, File]),
+ {ok, FileInfo} = file:read_file_info(FilePath),
+ case FileInfo#file_info.type of
+ directory ->
+ {ok, DirFiles} = file:list_dir(FilePath),
+ remove_all_files(FilePath, DirFiles),
+ file:del_dir(FilePath);
+ _ ->
+ file:delete(FilePath)
+ end
+ end, Files).
@@ -0,0 +1,38 @@
+#### Building version 0.1
+ rebar compile
+ rebar generate
+ mv rel/dummy rel/dummy_0.1
+ rebar clean
+ # start the release:
+ cd rel/dummy_0.1
+ bin/dummy console
+
+ erl> dummy_server:get_state().
+ erl> dummy_server:set_state(123).
+ erl> dummy_server:get_state().
+
+#### Building version 0.2
+
+ # Now, in another terminal we prepare an upgrade..
+
+ # change release version numbers from 0.1 to 0.2 in
+ $EDITOR apps/dummy/src/dummy.app.src
+ $EDITOR rel/reltool.config
+
+ rebar compile
+ rebar generate
+ rebar generate-upgrade previous_release=dummy_0.1
+ tar -zvtf rel/dummy_0.2.tar.gz
+
+
+#### Deploying with release_handler
+ mv rel/dummy_0.2.tar.gz rel/dummy_0.1/releases/
+
+ # Now use release_handler in the running erlang console for the deploy:
+
+ erl> release_handler:unpack_release("dummy_0.2").
+ erl> release_handler:install_release("0.2").
+ erl> release_handler:make_permanent("0.2").
+
+ erl> release_handler:which_releases().
+ erl> dummy_server:get_state().
@@ -0,0 +1,8 @@
+{"0.2",
+ [{"0.1",[
+ {update, dummy_server, {advanced, [foo]}}
+ ]}],
+ [{"0.1",[
+ {update, dummy_server, {advanced, [foo]}}
+ ]}]
+}.
@@ -0,0 +1,9 @@
+{application, dummy, [
+ {description, "a dummy app"},
+ {vsn, "0.1"},
+ {registered, [
+ dummy_app
+ ]},
+ {mod, {dummy_app, []}},
+ {applications, [kernel, stdlib, sasl]}
+]}.
@@ -0,0 +1,9 @@
+-module(dummy_app).
+-behaviour(application).
+
+-export([start/2, stop/1]).
+
+start(_,_) ->
+ dummy_sup:start_link().
+
+stop(_) -> ok.
@@ -0,0 +1,56 @@
+-module(dummy_server).
+-behaviour(gen_server).
+
+-export([start_link/0, set_state/1, get_state/0]).
+
+-export([init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
+
+%%
+
+start_link() ->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
+
+set_state(What) ->
+ gen_server:call(?MODULE, {set_state, What}).
+
+get_state() ->
+ gen_server:call(?MODULE, get_state).
+
+
+%%
+
+init([]) ->
+ say("init, setting state to 0", []),
+ {ok, 0}.
+
+
+handle_call({set_state, NewState}, _From, _State) ->
+ {reply, {ok, NewState}, NewState};
+
+handle_call(get_state, _From, State) ->
+ {reply, State, State}.
+
+handle_cast('__not_implemented', State) ->
+ {noreply, State}.
+
+handle_info(_Info, State) ->
+ say("info ~p, ~p.", [_Info, State]),
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ say("terminate ~p, ~p", [_Reason, _State]),
+ ok.
+
+code_change(_OldVsn, State, _Extra) ->
+ say("code_change ~p, ~p, ~p", [_OldVsn, State, _Extra]),
+ {ok, State}.
+
+%% Internal
+
+say(Format, Data) ->
+ io:format("~p:~p: ~s~n", [?MODULE, self(), io_lib:format(Format, Data)]).
Oops, something went wrong.

0 comments on commit 5298e93

Please sign in to comment.