aboutsummaryrefslogblamecommitdiffstats
path: root/src/rcl_prv_assembler.erl
blob: 1559c2d860839f92676c2c7e614042f7250d529f (plain) (tree)
1
                                                                         












































                                                                                









                                                                              



















                                                                                     


                                                                                       

                                                                  




                                                                        
 


                                                                      














                                                                        








                                                            


                                                                                   











                                                                  









                                                                               
                                           

                                  
                                              
                
                                             
        
 

                                         


                                      








                                                                






                                                                            










                                                             




                                                 
                                             

                                                                   
                                                                      






                      







                                                                 
                                         














                                                                      


                                                            











                                                               




















































                                                                                                                       
                                                                                                            






                                                                                                     















                                                                                   
                                                                               


                                          

                                                           



                                                                       

                                                           































                                                                           















                                          
                                                       









                                                     
                          
 




























                                                                                                       

           
                                                                      
%% -*- erlang-indent-level: 4; indent-tabs-mode: nil; fill-column: 80 -*-
%%% 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 <[email protected]>
%%% @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 create_output_dir(OutputDir) of
        ok ->
            case rcl_release:realized(Release) of
                true ->
                    copy_app_directories_to_output(State, Release, OutputDir);
                false ->
                    ?RCL_ERROR({unresolved_release, RelName, RelVsn})
            end;
        Error ->
            Error
    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({unable_to_create_output_dir, OutputDir}) ->
    io_lib:format("Unable to create output directory (possible permissions issue): ~s",
                  [OutputDir]);
format_error({release_script_generation_error, Module, Errors}) ->
    ["Errors generating release \n",
     rcl_util:indent(1), Module:format_error(Errors)];
format_error({unable_to_make_symlink, AppDir, TargetDir, Reason}) ->
    io_lib:format("Unable to symlink directory ~s to ~s because \n~s~s",
                  [AppDir, TargetDir, rcl_util:indent(1),
                   file:format_error(Reason)]).

%%%===================================================================
%%% Internal Functions
%%%===================================================================
-spec create_output_dir(file:name()) ->
                               ok | {error, Reason::term()}.
create_output_dir(OutputDir) ->
    case filelib:is_dir(OutputDir) of
        false ->
            case rcl_util:mkdir_p(OutputDir) of
                ok ->
                    ok;
                {error, _} ->
                    ?RCL_ERROR({unable_to_create_output_dir, OutputDir})
            end;
        true ->
            ok
    end.

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,
                          lists:flatten(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]),
    if
        AppDir == TargetDir ->
            %% No need to do anything here, discover found something already in
            %% a release dir
            ok;
        true ->
            copy_app(App, AppDir, TargetDir)
    end.

copy_app(App, AppDir, TargetDir) ->
    remove_symlink_or_directory(TargetDir),
    case rcl_app_info:link(App) of
        true ->
            link_directory(AppDir, TargetDir);
        false ->
            copy_directory(AppDir, TargetDir)
    end.

remove_symlink_or_directory(TargetDir) ->
    case ec_file:is_symlink(TargetDir) of
        true ->
            ec_file:remove(TargetDir);
        false ->
            case filelib:is_dir(TargetDir) of
                true ->
                    ok = ec_file:remove(TargetDir, [recursive]);
                false ->
                    ok
            end
    end.

link_directory(AppDir, TargetDir) ->
    case file:make_symlink(AppDir, TargetDir) of
        {error, Reason} ->
            ?RCL_ERROR({unable_to_make_symlink, AppDir, TargetDir, Reason});
        ok ->
            ok
    end.

copy_directory(AppDir, TargetDir) ->
    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 ->
            ok = rcl_util:mkdir_p(SubTarget),
            case ec_file:copy(SubSource, SubTarget, [recursive]) of
                {error, E} ->
                    ?RCL_ERROR({ec_file_error, AppDir, SubTarget, 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),
    ErlOpts = rcl_state:get(State, erl_opts, ""),
    StartFile = bin_file_contents(RelName, RelVsn,
                                  rcl_release:erts(Release),
                                  ErlOpts),
    %% We generate the start script by default, unless the user
    %% tells us not too
    case rcl_state:get(State, generate_start_script, true) of
        false ->
            ok;
        _ ->
            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)
    end,
    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]),
                    ok = file:write_file(filename:join([LocalErts, "bin", "erl"]), erl_script(ErtsVersion)),
                    case rcl_state:get(State, extended_start_script, false) of
                        true ->
                            ok = ec_file:copy(filename:join([Prefix, "bin", "start_clean.boot"]),
                                              filename:join([OutputDir, "bin", "start_clean.boot"]));
                        false ->
                            ok
                    end,
                    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 ~n",
                  [ReleaseFile, Options]),
    case make_script(Name, Options)  of
        ok ->
            rcl_log:error(rcl_state:log(State),
                          "release successfully created!"),
            {ok, State};
        error ->
            ?RCL_ERROR({release_script_generation_error, ReleaseFile});
        {ok, _, []} ->
            rcl_log:error(rcl_state:log(State),
                          "release successfully created!"),
            {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)].

erl_script(ErtsVsn) ->
    [<<"#!/bin/sh
set -e

SCRIPT_DIR=`dirname $0`
ROOTDIR=`cd $SCRIPT_DIR/../../ && pwd`
BINDIR=$ROOTDIR/erts-">>, ErtsVsn, <<"/bin
EMU=beam
PROGNAME=`echo $0 | sed 's/.*\\///'`
export EMU
export ROOTDIR
export BINDIR
export PROGNAME
exec \"$BINDIR/erlexec\" ${1+\"$@\"}
">>].

bin_file_contents(RelName, RelVsn, ErtsVsn, ErlOpts) ->
    [<<"#!/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
ERL_OPTS=">>, ErlOpts, <<"

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

cd $ROOTDIR

$BINDIR/erlexec $ERL_OPTS $SYS_CONFIG -boot $REL_DIR/$REL_NAME $@">>].