Permalink
Browse files

fully support testing of release builds

  • Loading branch information...
1 parent 8f26705 commit e72f965e861127cd97cdef82905370540a0d4a80 @ericbmerritt ericbmerritt committed Sep 20, 2012
Showing with 383 additions and 8 deletions.
  1. +10 −5 Makefile
  2. +1 −1 rebar.config
  3. +3 −0 relcool.config
  4. +328 −0 src/rcl_prv_assembler.erl
  5. +2 −0 src/rcl_prv_config.erl
  6. +16 −1 src/rcl_release.erl
  7. +14 −1 src/rcl_state.erl
  8. +8 −0 src/relcool.erl
  9. +1 −0 test/rclt_release_SUITE.erl
View
15 Makefile
@@ -35,7 +35,8 @@ ifeq ($(REBAR),)
$(error "Rebar not available on this system")
endif
-.PHONY: all compile doc clean test dialyzer typer shell distclean pdf get-deps escript
+.PHONY: all compile doc clean test dialyzer typer shell distclean pdf \
+ get-deps escript clean-common-test-data
all: compile escript dialyzer test
@@ -65,10 +66,10 @@ escript:
doc:
$(REBAR) skip_deps=true doc
-eunit: compile
+eunit: compile clean-common-test-data
$(REBAR) skip_deps=true eunit
-ct: compile
+ct: compile clean-common-test-data
$(REBAR) skip_deps=true ct
test: compile eunit ct
@@ -98,9 +99,13 @@ shell: get-deps compile
pdf:
pandoc README.md -o README.pdf
-clean:
- - rm -rf $(CURDIR)/test/*.beam
+clean-common-test-data:
+# We have to do this because of the unique way we generate test
+# data. Without this rebar eunit gets very confused
- rm -rf $(CURDIR)/test/*_SUITE_data
+
+clean: clean-common-test-data
+ - rm -rf $(CURDIR)/test/*.beam
- rm -rf $(CURDIR)/logs
$(REBAR) skip_deps=true clean
View
2 rebar.config
@@ -4,7 +4,7 @@
{git, "https://github.com/ericbmerritt/neotoma.git", {tag, "1.5.1"}}},
{erlware_commons, ".*",
{git, "https://github.com/ericbmerritt/erlware_commons.git",
- {branch, "semver-format"}}},
+ {branch, "next"}}},
{getopt, "",
{git, "https://github.com/jcomellas/getopt.git", {tag, "v0.5.1"}}}]}.
View
3 relcool.config
@@ -0,0 +1,3 @@
+%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+{release, {relcool, "0.0.1"},
+ [relcool]}.
View
328 src/rcl_prv_assembler.erl
@@ -0,0 +1,328 @@
+%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*-
+%%% Copyright 2012 Erlware, LLC. All Rights Reserved.
+%%%
+%%% This file is provided to you 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.
+%%%---------------------------------------------------------------------------
+%%% @author Eric Merritt <ericbmerritt@gmail.com>
+%%% @copyright (C) 2012 Erlware, LLC.
+%%%
+%%% @doc Given a complete built release this provider assembles that release
+%%% into a release directory.
+-module(rcl_prv_assembler).
+
+-behaviour(rcl_provider).
+
+-export([init/1,
+ do/1,
+ format_error/1]).
+
+-include_lib("relcool/include/relcool.hrl").
+
+%%============================================================================
+%% API
+%%============================================================================
+-spec init(rcl_state:t()) -> {ok, rcl_state:t()}.
+init(State) ->
+ {ok, State}.
+
+%% @doc recursively dig down into the library directories specified in the state
+%% looking for OTP Applications
+-spec do(rcl_state:t()) -> {ok, rcl_state:t()} | relcool:error().
+do(State) ->
+ {RelName, RelVsn} = rcl_state:default_release(State),
+ Release = rcl_state:get_release(State, RelName, RelVsn),
+ OutputDir = rcl_state:output_dir(State),
+ case rcl_release:realized(Release) of
+ true ->
+ copy_app_directories_to_output(State, Release, OutputDir);
+ false ->
+ ?RCL_ERROR({unresolved_release, RelName, RelVsn})
+ end.
+
+-spec format_error(ErrorDetail::term()) -> iolist().
+format_error({unresolved_release, RelName, RelVsn}) ->
+ io_lib:format("The release has not been resolved ~p-~s", [RelName, RelVsn]);
+format_error({ec_file_error, AppDir, TargetDir, E}) ->
+ io_lib:format("Unable to copy OTP App from ~s to ~s due to ~p",
+ [AppDir, TargetDir, E]);
+format_error({config_does_not_exist, Path}) ->
+ io_lib:format("The config file specified for this release (~s) does not exist!",
+ [Path]);
+format_error({specified_erts_does_not_exist, ErtsVersion}) ->
+ io_lib:format("Specified version of erts (~s) does not exist",
+ [ErtsVersion]);
+format_error({release_script_generation_error, RelFile}) ->
+ io_lib:format("Unknown internal release error generating the release file to ~s",
+ [RelFile]);
+format_error({release_script_generation_warning, Module, Warnings}) ->
+ ["Warnings generating release \s",
+ rcl_util:indent(1), Module:format_warning(Warnings)];
+format_error({release_script_generation_error, Module, Errors}) ->
+ ["Errors generating release \n",
+ rcl_util:indent(1), Module:format_error(Errors)].
+
+
+
+%%%===================================================================
+%%% Internal Functions
+%%%===================================================================
+copy_app_directories_to_output(State, Release, OutputDir) ->
+ LibDir = filename:join([OutputDir, "lib"]),
+ ok = ec_file:mkdir_p(LibDir),
+ Apps = rcl_release:application_details(Release),
+ Result = lists:filter(fun({error, _}) ->
+ true;
+ (_) ->
+ false
+ end,
+ ec_plists:map(fun(App) ->
+ copy_app(LibDir, App)
+ end, Apps)),
+ case Result of
+ [E | _] ->
+ E;
+ [] ->
+ create_release_info(State, Release, OutputDir)
+ end.
+
+copy_app(LibDir, App) ->
+ AppName = erlang:atom_to_list(rcl_app_info:name(App)),
+ AppVsn = rcl_app_info:vsn_as_string(App),
+ AppDir = rcl_app_info:dir(App),
+ TargetDir = filename:join([LibDir, AppName ++ "-" ++ AppVsn]),
+ ec_plists:map(fun(SubDir) ->
+ copy_dir(AppDir, TargetDir, SubDir)
+ end, ["ebin",
+ "include",
+ "priv",
+ "src"
+ "c_src",
+ "README",
+ "LICENSE"]).
+
+copy_dir(AppDir, TargetDir, SubDir) ->
+ SubSource = filename:join(AppDir, SubDir),
+ SubTarget = filename:join(TargetDir, SubDir),
+ case filelib:is_dir(SubSource) of
+ true ->
+ case filelib:is_dir(SubTarget) of
+ true ->
+ ec_file:remove(SubTarget, [recursive]);
+ false ->
+ ok
+ end,
+ ok = ec_file:mkdir_p(SubTarget),
+ case ec_file:copy(SubSource, SubTarget, [recursive]) of
+ {error, E} ->
+ ?RCL_ERROR({ec_file_error, AppDir, TargetDir, E});
+ ok ->
+ ok
+ end;
+ false ->
+ ok
+ end.
+
+
+
+create_release_info(State, Release, OutputDir) ->
+ RelName = erlang:atom_to_list(rcl_release:name(Release)),
+ ReleaseDir = filename:join([OutputDir,
+ "releases",
+ RelName ++ "-" ++
+ rcl_release:vsn(Release)]),
+ ReleaseFile = filename:join([ReleaseDir, RelName ++ ".rel"]),
+ ok = ec_file:mkdir_p(ReleaseDir),
+ case rcl_release:metadata(Release) of
+ {ok, Meta} ->
+ ok = ec_file:write_term(ReleaseFile, Meta),
+ write_bin_file(State, Release, OutputDir, ReleaseDir);
+ E ->
+ E
+ end.
+
+
+write_bin_file(State, Release, OutputDir, RelDir) ->
+ RelName = erlang:atom_to_list(rcl_release:name(Release)),
+ RelVsn = rcl_release:vsn(Release),
+ BinDir = filename:join([OutputDir, "bin"]),
+ ok = ec_file:mkdir_p(BinDir),
+ VsnRel = filename:join(BinDir, RelName ++ "-" ++ RelVsn),
+ BareRel = filename:join(BinDir, RelName),
+ StartFile = bin_file_contents(RelName, RelVsn, rcl_release:erts(Release)),
+ ok = file:write_file(VsnRel, StartFile),
+ ok = file:change_mode(VsnRel, 8#777),
+ ok = file:write_file(BareRel, StartFile),
+ ok = file:change_mode(BareRel, 8#777),
+ copy_or_generate_sys_config_file(State, Release, OutputDir, RelDir).
+
+%% @doc copy config/sys.config or generate one to releases/VSN/sys.config
+-spec copy_or_generate_sys_config_file(rcl_state:t(), rcl_release:t(),
+ file:name(), file:name()) ->
+ {ok, rcl_state:t()} | relcool:error().
+copy_or_generate_sys_config_file(State, Release, OutputDir, RelDir) ->
+ RelSysConfPath = filename:join([RelDir, "sys.config"]),
+ case rcl_state:sys_config(State) of
+ undefined ->
+ ok = generate_sys_config_file(RelSysConfPath),
+ include_erts(State, Release, OutputDir, RelDir);
+ ConfigPath ->
+ case filelib:is_regular(ConfigPath) of
+ false ->
+ ?RCL_ERROR({config_does_not_exist, ConfigPath});
+ true ->
+ ok = ec_file:copy(ConfigPath, RelSysConfPath),
+ include_erts(State, Release, OutputDir, RelDir)
+ end
+ end.
+
+%% @doc write a generic sys.config to the path RelSysConfPath
+-spec generate_sys_config_file(string()) -> ok.
+generate_sys_config_file(RelSysConfPath) ->
+ {ok, Fd} = file:open(RelSysConfPath, [write]),
+ io:format(Fd,
+ "%% Thanks to Ulf Wiger at Ericcson for these comments:~n"
+ "%%~n"
+ "%% This file is identified via the erl command line option -config File.~n"
+ "%% Note that File should have no extension, e.g.~n"
+ "%% erl -config .../sys (if this file is called sys.config)~n"
+ "%%~n"
+ "%% In this file, you can redefine application environment variables.~n"
+ "%% This way, you don't have to modify the .app files of e.g. OTP applications.~n"
+ "[].~n", []),
+ file:close(Fd).
+
+%% @doc Optionally add erts directory to release, if defined.
+-spec include_erts(rcl_state:t(), rcl_release:t(), file:name(), file:name()) -> {ok, rcl_state:t()} | relcool:error().
+include_erts(State, Release, OutputDir, RelDir) ->
+ case rcl_state:get(State, include_erts, true) of
+ true ->
+ Prefix = code:root_dir(),
+ ErtsVersion = rcl_release:erts(Release),
+ ErtsDir = filename:join([Prefix, "erts-" ++ ErtsVersion]),
+ LocalErts = filename:join([OutputDir, "erts-" ++ ErtsVersion]),
+ case filelib:is_dir(ErtsDir) of
+ false ->
+ ?RCL_ERROR({specified_erts_does_not_exist, ErtsVersion});
+ true ->
+ ok = ec_file:mkdir_p(LocalErts),
+ ok = ec_file:copy(ErtsDir, LocalErts, [recursive]),
+ make_boot_script(State, Release, OutputDir, RelDir)
+ end;
+ _ ->
+ make_boot_script(State, Release, OutputDir, RelDir)
+ end.
+
+
+-spec make_boot_script(rcl_state:t(), rcl_release:t(), file:name(), file:name()) ->
+ {ok, rcl_state:t()} | relcool:error().
+make_boot_script(State, Release, OutputDir, RelDir) ->
+ Options = [{path, [RelDir | get_code_paths(Release, OutputDir)]},
+ {outdir, RelDir},
+ no_module_tests, silent],
+ Name = erlang:atom_to_list(rcl_release:name(Release)),
+ ReleaseFile = filename:join([RelDir, Name ++ ".rel"]),
+ rcl_log:debug(rcl_state:log(State),
+ "Creating script from release file ~s \n with options ~p",
+ [ReleaseFile, Options]),
+ case make_script(Name, Options) of
+ ok ->
+ {ok, State};
+ error ->
+ ?RCL_ERROR({release_script_generation_error, ReleaseFile});
+ {ok, _, []} ->
+ {ok, State};
+ {ok,Module,Warnings} ->
+ ?RCL_ERROR({release_script_generation_warn, Module, Warnings});
+ {error,Module,Error} ->
+ ?RCL_ERROR({release_script_generation_error, Module, Error})
+ end.
+
+-spec make_script(string(), [term()]) ->
+ ok |
+ error |
+ {ok, module(), [term()]} |
+ {error,module,[term()]}.
+make_script(Name, Options) ->
+ %% Erts 5.9 introduced a non backwards compatible option to
+ %% erlang this takes that into account
+ Erts = erlang:system_info(version),
+ case ec_semver:gte(Erts, "5.9") of
+ true ->
+ systools:make_script(Name, [no_warn_sasl | Options]);
+ _ ->
+ systools:make_script(Name, Options)
+ end.
+
+%% @doc Generates the correct set of code paths for the system.
+-spec get_code_paths(rcl_release:t(), file:name()) -> [file:name()].
+get_code_paths(Release, OutDir) ->
+ LibDir = filename:join(OutDir, "lib"),
+ [filename:join([LibDir,
+ erlang:atom_to_list(rcl_app_info:name(App)) ++ "-" ++
+ rcl_app_info:vsn_as_string(App), "ebin"]) ||
+ App <- rcl_release:application_details(Release)].
+
+bin_file_contents(RelName, RelVsn, ErtsVsn) ->
+ [<<"#!/bin/sh
+
+set -e
+
+SCRIPT_DIR=`dirname $0`
+RELEASE_ROOT_DIR=`cd $SCRIPT_DIR/.. && pwd`
+REL_NAME=">>, RelName, <<"
+REL_VSN=">>, RelVsn, <<"
+ERTS_VSN=">>, ErtsVsn, <<"
+REL_DIR=$RELEASE_ROOT_DIR/releases/$REL_NAME-$REL_VSN
+
+ERTS_DIR=
+SYS_CONFIG=
+ROOTDIR=
+
+ERTS_DIR=
+SYS_CONFIG=
+ROOTDIR=
+
+find_erts_dir() {
+ local erts_dir=$RELEASE_ROOT_DIR/erts-$ERTS_VSN
+ if [ -d \"$erts_dir\" ]; then
+ ERTS_DIR=$erts_dir;
+ ROOTDIR=$RELEASE_ROOT_DIR
+ else
+ local erl=`which erl`
+ local erl_root=`$erl -noshell -eval \"io:format(\\\"~s\\\", [code:root_dir()]).\" -s init stop`
+ ERTS_DIR=$erl_root/erts-$ERTS_VSN
+ ROOTDIR=$erl_root
+ fi
+
+}
+
+find_sys_config() {
+ local possible_sys=$REL_DIR/sys.config
+ if [ -f \"$possible_sys\" ]; then
+ SYS_CONFIG=\"-config $possible_sys\"
+ fi
+}
+
+find_erts_dir
+find_sys_config
+export ROOTDIR=$RELEASE_ROOT_DIR
+export BINDIR=$ERTS_DIR/bin
+export EMU=beam
+export PROGNAME=erl
+export LD_LIBRARY_PATH=$ERTS_DIR/lib
+
+
+
+$BINDIR/erlexec $SYS_CONFIG -boot $REL_DIR/$REL_NAME $@">>].
View
2 src/rcl_prv_config.erl
@@ -99,6 +99,8 @@ load_terms({release, {RelName, Vsn}, {erts, ErtsVsn},
{ok, Release1} ->
{ok, rcl_state:add_release(State, Release1)}
end;
+load_terms({sys_config, SysConfig}, {ok, State}) ->
+ {ok, rcl_state:sys_config(State, filename:absname(SysConfig))};
load_terms({Name, Value}, {ok, State})
when erlang:is_atom(Name) ->
{ok, rcl_state:put(State, Name, Value)};
View
17 src/rcl_release.erl
@@ -33,6 +33,7 @@
applications/1,
application_details/1,
realized/1,
+ metadata/1,
format/1,
format/2,
format_error/1]).
@@ -142,6 +143,17 @@ application_details(#release_t{app_detail=App}) ->
realized(#release_t{realized=Realized}) ->
Realized.
+-spec metadata(t()) -> term().
+metadata(#release_t{name=Name, vsn=Vsn, erts=ErtsVsn, applications=Apps,
+ realized=Realized}) ->
+ case Realized of
+ true ->
+ {ok, {release, {erlang:atom_to_list(Name), Vsn}, {erts, ErtsVsn},
+ Apps}};
+ false ->
+ ?RCL_ERROR({not_realized, Name, Vsn})
+ end.
+
-spec format(t()) -> iolist().
format(Release) ->
format(0, Release).
@@ -177,7 +189,10 @@ format_error({topo_error, E}) ->
format_error({failed_to_parse, Con}) ->
io_lib:format("Failed to parse constraint ~p", [Con]);
format_error({invalid_constraint, Con}) ->
- io_lib:format("Invalid constraint specified ~p", [Con]).
+ io_lib:format("Invalid constraint specified ~p", [Con]);
+format_error({not_realized, Name, Vsn}) ->
+ io_lib:format("Unable to produce metadata release: ~p-~s has not been realized",
+ [Name, Vsn]).
%%%===================================================================
%%% Internal Functions
View
15 src/rcl_state.erl
@@ -31,6 +31,8 @@
config_files/1,
providers/1,
providers/2,
+ sys_config/1,
+ sys_config/2,
add_release/2,
get_release/3,
update_release/2,
@@ -61,6 +63,7 @@
providers = [] :: [rcl_provider:t()],
available_apps = [] :: [rcl_app_info:t()],
default_release :: {rcl_release:name(), rcl_release:vsn()},
+ sys_config :: file:filename() | undefined,
releases :: ec_dictionary:dictionary({ReleaseName::atom(),
ReleaseVsn::string()},
rcl_release:t()),
@@ -122,6 +125,14 @@ config_files(#state_t{config_files=ConfigFiles}) ->
providers(#state_t{providers=Providers}) ->
Providers.
+-spec sys_config(t()) -> file:filename() | undefined.
+sys_config(#state_t{sys_config=SysConfig}) ->
+ SysConfig.
+
+-spec sys_config(t(), file:filename()) -> t().
+sys_config(State, SysConfig) ->
+ State#state_t{sys_config=SysConfig}.
+
-spec providers(t(), [rcl_provider:t()]) -> t().
providers(M, NewProviders) ->
M#state_t{providers=NewProviders}.
@@ -237,7 +248,9 @@ create_logic_providers(State0) ->
{ConfigProvider, {ok, State1}} = rcl_provider:new(rcl_prv_config, State0),
{DiscoveryProvider, {ok, State2}} = rcl_provider:new(rcl_prv_discover, State1),
{ReleaseProvider, {ok, State3}} = rcl_provider:new(rcl_prv_release, State2),
- State3#state_t{providers=[ConfigProvider, DiscoveryProvider, ReleaseProvider]}.
+ {AssemblerProvider, {ok, State4}} = rcl_provider:new(rcl_prv_assembler, State3),
+ State4#state_t{providers=[ConfigProvider, DiscoveryProvider,
+ ReleaseProvider, AssemblerProvider]}.
%%%===================================================================
%%% Test Functions
View
8 src/relcool.erl
@@ -22,10 +22,13 @@
-export([main/1,
do/7,
+ format_error/1,
opt_spec_list/0]).
-export_type([error/0]).
+-include_lib("relcool/include/relcool.hrl").
+
%%============================================================================
%% types
%%============================================================================
@@ -77,6 +80,11 @@ opt_spec_list() ->
{log_level, $V, "verbose", {integer, 2}, "Verbosity level, maybe between 0 and 2"}
].
+-spec format_error(Reason::term()) -> iolist().
+format_error({invalid_return_value, Provider, Value}) ->
+ [rcl_provider:format(Provider), " returned an invalid value ",
+ io_lib:format("~p", [Value])].
+
%%============================================================================
%% internal api
%%============================================================================
View
1 test/rclt_release_SUITE.erl
@@ -110,6 +110,7 @@ get_app_metadata(Name, Vsn, Deps, LibDeps) ->
{vsn, Vsn},
{modules, []},
{included_applications, LibDeps},
+ {registered, []},
{applications, Deps}]}.
create_random_name(Name) ->

0 comments on commit e72f965

Please sign in to comment.