From e72f965e861127cd97cdef82905370540a0d4a80 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 19 Sep 2012 17:42:24 -0700 Subject: fully support testing of release builds --- Makefile | 15 +- rebar.config | 2 +- relcool.config | 3 + src/rcl_prv_assembler.erl | 328 ++++++++++++++++++++++++++++++++++++++++++++ src/rcl_prv_config.erl | 2 + src/rcl_release.erl | 17 ++- src/rcl_state.erl | 15 +- src/relcool.erl | 8 ++ test/rclt_release_SUITE.erl | 1 + 9 files changed, 383 insertions(+), 8 deletions(-) create mode 100644 relcool.config create mode 100644 src/rcl_prv_assembler.erl diff --git a/Makefile b/Makefile index d69468b..7615f9e 100644 --- a/Makefile +++ b/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 diff --git a/rebar.config b/rebar.config index b64dd08..160315a 100644 --- a/rebar.config +++ b/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"}}}]}. diff --git a/relcool.config b/relcool.config new file mode 100644 index 0000000..a86ab3f --- /dev/null +++ b/relcool.config @@ -0,0 +1,3 @@ +%% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- +{release, {relcool, "0.0.1"}, + [relcool]}. diff --git a/src/rcl_prv_assembler.erl b/src/rcl_prv_assembler.erl new file mode 100644 index 0000000..de8c531 --- /dev/null +++ b/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 +%%% @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 $@">>]. diff --git a/src/rcl_prv_config.erl b/src/rcl_prv_config.erl index e75bf85..8fb32f2 100644 --- a/src/rcl_prv_config.erl +++ b/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)}; diff --git a/src/rcl_release.erl b/src/rcl_release.erl index 9dbb866..f1a439c 100644 --- a/src/rcl_release.erl +++ b/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 diff --git a/src/rcl_state.erl b/src/rcl_state.erl index 7526c1c..2d27de2 100644 --- a/src/rcl_state.erl +++ b/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 diff --git a/src/relcool.erl b/src/relcool.erl index 806f473..13ef2ea 100644 --- a/src/relcool.erl +++ b/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 %%============================================================================ diff --git a/test/rclt_release_SUITE.erl b/test/rclt_release_SUITE.erl index 5abc218..7cdf362 100644 --- a/test/rclt_release_SUITE.erl +++ b/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) -> -- cgit v1.2.3