aboutsummaryrefslogblamecommitdiffstats
path: root/src/rlx_prv_assembler.erl
blob: cb5bbed476814ccf4303c6edd7e6d03169096b11 (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(rlx_prv_assembler).

-behaviour(provider).

-export([init/1,
         do/1,
         format_error/1]).

-include("relx.hrl").

-define(PROVIDER, release).
-define(DEPS, [resolve_release]).

%%============================================================================
%% API
%%============================================================================
-spec init(rlx_state:t()) -> {ok, rlx_state:t()}.
init(State) ->
    State1 = rlx_state:add_provider(State, providers:create([{name, ?PROVIDER},
                                                             {module, ?MODULE},
                                                             {deps, ?DEPS},
                                                             {hooks, {[], [overlay]}}])),
    {ok, State1}.

%% @doc recursively dig down into the library directories specified in the state
%% looking for OTP Applications
-spec do(rlx_state:t()) -> {ok, rlx_state:t()} | relx:error().
do(State) ->
    print_dev_mode(State),
    {RelName, RelVsn} = rlx_state:default_configured_release(State),
    Release = rlx_state:get_realized_release(State, RelName, RelVsn),
    OutputDir = rlx_state:output_dir(State),
    case create_output_dir(OutputDir) of
        ok ->
            case rlx_release:realized(Release) of
                true ->
                    case copy_app_directories_to_output(State, Release, OutputDir) of
                        {ok, State1} ->
                            case rlx_state:debug_info(State1) =:= strip
                                andalso rlx_state:dev_mode(State1) =:= false of
                                true ->
                                    case beam_lib:strip_release(OutputDir) of
                                        {ok, _} ->
                                            {ok, State1};
                                        {error, _, Reason} ->
                                            ?RLX_ERROR({strip_release, Reason})
                                    end;
                                false ->
                                    {ok, State1}
                            end;
                        E ->
                            E
                    end;
                false ->
                    ?RLX_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({vmargs_does_not_exist, Path}) ->
    io_lib:format("The vm.args file specified for this release (~s) does not exist!",
                  [Path]);
format_error({vmargs_src_does_not_exist, Path}) ->
    io_lib:format("The vm.args.src file specified for this release (~s) does not exist!",
                  [Path]);
format_error({config_does_not_exist, Path}) ->
    io_lib:format("The sys.config file specified for this release (~s) does not exist!",
                  [Path]);
format_error({config_src_does_not_exist, Path}) ->
    io_lib:format("The sys.config.src file specified for this release (~s) does not exist!",
                  [Path]);
format_error({sys_config_parse_error, ConfigPath, Reason}) ->
    io_lib:format("The config file (~s) specified for this release could not be opened or parsed: ~s",
                  [ConfigPath, file:format_error(Reason)]);
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",
     rlx_util:indent(2), 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",
     rlx_util:indent(2), 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, rlx_util:indent(2),
                   file:format_error(Reason)]);
format_error(boot_script_generation_error) ->
    "Unknown internal release error generating start_clean.boot";
format_error({boot_script_generation_warning, Module, Warnings}) ->
    ["Warnings generating start_clean.boot \s",
     rlx_util:indent(2), Module:format_warning(Warnings)];
format_error({boot_script_generation_error, Module, Errors}) ->
    ["Errors generating start_clean.boot \n",
     rlx_util:indent(2), Module:format_error(Errors)];
format_error({strip_release, Reason}) ->
    io_lib:format("Stripping debug info from release beam files failed becuase ~s",
                  [beam_lib:format_error(Reason)]);
format_error({rewrite_app_file, AppFile, Error}) ->
    io_lib:format("Unable to rewrite .app file ~s due to ~p",
                  [AppFile, Error]).

%%%===================================================================
%%% Internal Functions
%%%===================================================================
print_dev_mode(State) ->
    case rlx_state:dev_mode(State) of
        true ->
            ec_cmd_log:info(rlx_state:log(State),
                            "Dev mode enabled, release will be symlinked");
        false ->
            ok
    end.

-spec create_output_dir(file:name()) ->
                               ok | {error, Reason::term()}.
create_output_dir(OutputDir) ->
    case ec_file:is_dir(OutputDir) of
        false ->
            case rlx_util:mkdir_p(OutputDir) of
                ok ->
                    ok;
                {error, _} ->
                    ?RLX_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),
    IncludeSrc = rlx_state:include_src(State),
    IncludeErts = rlx_state:get(State, include_erts, true),
    Apps = prepare_applications(State, rlx_release:application_details(Release)),
    Result = lists:filter(fun({error, _}) ->
                                  true;
                             (_) ->
                                  false
                          end,
                         lists:flatten(ec_plists:map(fun(App) ->
                                                             copy_app(State, LibDir, App, IncludeSrc, IncludeErts)
                                                     end, Apps))),
    case Result of
        [E | _] ->
            E;
        [] ->
            create_release_info(State, Release, OutputDir)
    end.

prepare_applications(State, Apps) ->
    case rlx_state:dev_mode(State) of
        true ->
            [rlx_app_info:link(App, true) || App <- Apps];
        false ->
            Apps
    end.

copy_app(State, LibDir, App, IncludeSrc, IncludeErts) ->
    AppName = erlang:atom_to_list(rlx_app_info:name(App)),
    AppVsn = rlx_app_info:original_vsn(App),
    AppDir = rlx_app_info:dir(App),
    TargetDir = filename:join([LibDir, AppName ++ "-" ++ AppVsn]),
    case AppDir == ec_cnv:to_binary(TargetDir) of
        true ->
            %% No need to do anything here, discover found something already in
            %% a release dir
            ok;
        false ->
            case IncludeErts of
                false ->
                    case is_erts_lib(AppDir) of
                        true ->
                            [];
                        false ->
                            copy_app_(State, App, AppDir, TargetDir, IncludeSrc)
                    end;
                _ ->
                    copy_app_(State, App, AppDir, TargetDir, IncludeSrc)
            end
    end.

is_erts_lib(Dir) ->
    lists:prefix(filename:split(list_to_binary(code:lib_dir())), filename:split(Dir)).

copy_app_(State, App, AppDir, TargetDir, IncludeSrc) ->
    remove_symlink_or_directory(TargetDir),
    case rlx_app_info:link(App) of
        true ->
            link_directory(AppDir, TargetDir),
            rewrite_app_file(State, App, AppDir);
        false ->
            copy_directory(State, App, AppDir, TargetDir, IncludeSrc),
            rewrite_app_file(State, App, TargetDir)
    end.

%% If excluded apps exist in this App's applications list we must write a new .app
rewrite_app_file(State, App, TargetDir) ->
    Name = rlx_app_info:name(App),
    ActiveDeps = rlx_app_info:active_deps(App),
    IncludedDeps = rlx_app_info:library_deps(App),
    AppFile = filename:join([TargetDir, "ebin", ec_cnv:to_list(Name) ++ ".app"]),
    {ok, [{application, AppName, AppData0}]} = file:consult(AppFile),
    OldActiveDeps = proplists:get_value(applications, AppData0, []),
    OldIncludedDeps = proplists:get_value(included_applications, AppData0, []),
    OldModules = proplists:get_value(modules, AppData0, []),
    ExcludedModules = proplists:get_value(Name,
                                          rlx_state:exclude_modules(State), []),

    %% maybe replace excluded apps
    AppData2 =
        case {OldActiveDeps, OldIncludedDeps} of
            {ActiveDeps, IncludedDeps} ->
                AppData0;
            _ ->
                AppData1 = lists:keyreplace(applications
                                           ,1
                                           ,AppData0
                                           ,{applications, ActiveDeps}),
                lists:keyreplace(included_applications
                                 ,1
                                 ,AppData1
                                 ,{included_applications, IncludedDeps})
        end,
    %% maybe replace excluded modules
    AppData3 =
        case ExcludedModules of
            [] -> AppData2;
            _ ->
                lists:keyreplace(modules
                                 ,1
                                 ,AppData2
                                 ,{modules, OldModules -- ExcludedModules})
        end,
    Spec = [{application, AppName, AppData3}],
    case write_file_if_contents_differ(AppFile, Spec) of
        ok -> ok;
        Error -> ?RLX_ERROR({rewrite_app_file, AppFile, Error})
    end.

write_file_if_contents_differ(Filename, Spec) ->
    ToWrite = io_lib:format("~p.\n", Spec),
    case file:consult(Filename) of
        {ok, Spec} ->
            ok;
        {ok,  _} ->
            file:write_file(Filename, ToWrite);
        {error,  _} ->
            file:write_file(Filename, ToWrite)
    end.

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

link_directory(AppDir, TargetDir) ->
    case rlx_util:symlink_or_copy(AppDir, TargetDir) of
        {error, Reason} ->
            ?RLX_ERROR({unable_to_make_symlink, AppDir, TargetDir, Reason});
        ok ->
            ok
    end.

copy_directory(State, App, AppDir, TargetDir, IncludeSrc) ->
    [copy_dir(State, App, AppDir, TargetDir, SubDir)
    || SubDir <- ["ebin",
                  "include",
                  "priv",
                  "lib" |
                  case IncludeSrc of
                      true ->
                          ["src",
                           "c_src"];
                      false ->
                          []
                  end]].

copy_dir(State, App, AppDir, TargetDir, SubDir) ->
    SubSource = filename:join(AppDir, SubDir),
    SubTarget = filename:join(TargetDir, SubDir),
    case ec_file:is_dir(SubSource) of
        true ->
            ok = rlx_util:mkdir_p(SubTarget),
            %% get a list of the modules to be excluded from this app
            AppName = rlx_app_info:name(App),
            ExcludedModules = proplists:get_value(AppName, rlx_state:exclude_modules(State),
                                                  []),
            ExcludedFiles = [filename:join([binary_to_list(SubSource), 
                                            atom_to_list(M) ++ ".beam"]) ||
                                M <- ExcludedModules],
            case copy_dir(SubSource, SubTarget, ExcludedFiles) of
                {error, E} ->
                    ?RLX_ERROR({ec_file_error, AppDir, SubTarget, E});
                ok ->
                    ok
            end;
        false ->
            ok
    end.

%% no files are excluded, just copy the whole dir
copy_dir(SourceDir, TargetDir, []) ->
     case ec_file:copy(SourceDir, TargetDir, [recursive, {file_info, [mode, time]}]) of
        {error, E} -> {error, E};
        ok ->
            ok
    end;
copy_dir(SourceDir, TargetDir, ExcludeFiles) ->
    SourceFiles = filelib:wildcard(
                    filename:join([binary_to_list(SourceDir), "*"])),
    lists:foreach(fun(F) ->
                    ok = ec_file:copy(F,
                                      filename:join([TargetDir,
                                                     filename:basename(F)]), [{file_info, [mode, time]}])
                  end, SourceFiles -- ExcludeFiles).

create_release_info(State0, Release0, OutputDir) ->
    RelName = atom_to_list(rlx_release:name(Release0)),
    ReleaseDir = rlx_util:release_output_dir(State0, Release0),
    ReleaseFile = filename:join([ReleaseDir, RelName ++ ".rel"]),
    StartCleanFile = filename:join([ReleaseDir, "start_clean.rel"]),
    NoDotErlFile = filename:join([ReleaseDir, "no_dot_erlang.rel"]),
    ok = ec_file:mkdir_p(ReleaseDir),
    Release1 = rlx_release:relfile(Release0, ReleaseFile),
    State1 = rlx_state:update_realized_release(State0, Release1),
    case rlx_release:metadata(Release1) of
        {ok, Meta} ->
            case {rlx_release:start_clean_metadata(Release1),
                  rlx_release:no_dot_erlang_metadata(Release1)} of
                {{ok, StartCleanMeta}, {ok, NoDotErlMeta}} ->
                    ok = ec_file:write_term(ReleaseFile, Meta),
                    ok = ec_file:write_term(StartCleanFile, StartCleanMeta),
                    ok = ec_file:write_term(NoDotErlFile, NoDotErlMeta),
                    write_bin_file(State1, Release1, OutputDir, ReleaseDir);
                {{ok, _}, E} ->
                    E;
                {_, E} ->
                    E
            end;
        E ->
            E
    end.

write_bin_file(State, Release, OutputDir, RelDir) ->
    RelName = erlang:atom_to_list(rlx_release:name(Release)),
    RelVsn = rlx_release:vsn(Release),
    BinDir = filename:join([OutputDir, "bin"]),
    ok = ec_file:mkdir_p(BinDir),
    VsnRel = filename:join(BinDir, rlx_release:canonical_name(Release)),
    BareRel = filename:join(BinDir, RelName),
    ErlOpts = rlx_state:get(State, erl_opts, ""),
    {OsFamily, _OsName} = rlx_util:os_type(State),

    StartFile = case rlx_state:get(State, extended_start_script, false) of
                    false ->
                        case rlx_state:get(State, include_nodetool, false) of
                            true ->
                                include_nodetool(BinDir);
                            false ->
                                ok
                        end,
                        bin_file_contents(OsFamily, RelName, RelVsn,
                                          rlx_release:erts(Release),
                                          ErlOpts);
                    true ->
                        %% extended start script needs nodetool so it's
                        %% always included
                        include_nodetool(BinDir),
                        Hooks = expand_hooks(BinDir,
                                             rlx_state:get(State,
                                                           extended_start_script_hooks,
                                                           []),
                                             State),
                        Extensions = rlx_state:get(State,
                                                   extended_start_script_extensions,
                                                   []),
                        extended_bin_file_contents(OsFamily, RelName, RelVsn,
                                                   rlx_release:erts(Release), ErlOpts,
                                                   Hooks, Extensions)
                end,
    %% We generate the start script by default, unless the user
    %% tells us not too
    case rlx_state:get(State, generate_start_script, true) of
        false ->
            ok;
        _ ->
            VsnRelStartFile = case OsFamily of
                unix -> VsnRel;
                win32 -> rlx_string:concat(VsnRel, ".cmd")
            end,
            ok = file:write_file(VsnRelStartFile, StartFile),
            ok = file:change_mode(VsnRelStartFile, 8#777),
            BareRelStartFile = case OsFamily of
                unix -> BareRel;
                win32 -> rlx_string:concat(BareRel, ".cmd")
            end,
            ok = file:write_file(BareRelStartFile, StartFile),
            ok = file:change_mode(BareRelStartFile, 8#777)
    end,
    ReleasesDir = filename:join(OutputDir, "releases"),
    generate_start_erl_data_file(Release, ReleasesDir),
    copy_or_generate_vmargs_file(State, Release, RelDir),
    case copy_or_generate_sys_config_file(State, RelDir) of
        ok ->
            include_erts(State, Release, OutputDir, RelDir);
        E ->
            E
    end.

expand_hooks(_Bindir, [], _State) -> [];
expand_hooks(BinDir, Hooks, _State) ->
    expand_hooks(BinDir, Hooks, [], _State).

expand_hooks(_BinDir, [], Acc, _State) -> Acc;
expand_hooks(BinDir, [{Phase, Hooks0} | Rest], Acc, State) ->
    %% filter and expand hooks to their respective shell scripts
    Hooks =
        lists:foldl(
            fun(Hook, Acc0) ->
                case validate_hook(Phase, Hook) of
                    true ->
                        %% all hooks are relative to the bin dir
                        HookScriptFilename = filename:join([BinDir,
                                                            hook_filename(Hook)]),
                        %% write the hook script file to it's proper location
                        ok = render_hook(hook_template(Hook), HookScriptFilename, State),
                        %% and return the invocation that's to be templated in the
                        %% extended script
                        Acc0 ++ [hook_invocation(Hook)];
                    false ->
                        ec_cmd_log:error(
                            rlx_state:log(State),
                            io_lib:format("~p hook is not allowed in the ~p phase, ignoring it", [Hook, Phase])
                        ),

                        Acc0
                end
            end, [], Hooks0),
    expand_hooks(BinDir, Rest, Acc ++ [{Phase, Hooks}], State).

%% the pid script hook is only allowed in the
%% post_start phase
%% with args
validate_hook(post_start, {pid, _}) -> true;
%% and without args
validate_hook(post_start, pid) -> true;
%% same for wait_for_vm_start, wait_for_process script
validate_hook(post_start, wait_for_vm_start) -> true;
validate_hook(post_start, {wait_for_process, _}) -> true;
%% custom hooks are allowed in all phases
validate_hook(_Phase, {custom, _}) -> true;
%% as well as status hooks
validate_hook(status, _) -> true;
%% deny all others
validate_hook(_, _) -> false.

hook_filename({custom, CustomScript}) -> CustomScript;
hook_filename(pid) -> "hooks/builtin/pid";
hook_filename({pid, _}) -> "hooks/builtin/pid";
hook_filename(wait_for_vm_start) -> "hooks/builtin/wait_for_vm_start";
hook_filename({wait_for_process, _}) -> "hooks/builtin/wait_for_process";
hook_filename(builtin_status) -> "hooks/builtin/status".

hook_invocation({custom, CustomScript}) -> CustomScript;
%% the pid builtin hook with no arguments writes to pid file
%% at /var/run/{{ rel_name }}.pid
hook_invocation(pid) -> rlx_string:join(["hooks/builtin/pid",
                                     "/var/run/$REL_NAME.pid"], "|");
hook_invocation({pid, PidFile}) -> rlx_string:join(["hooks/builtin/pid",
                                                PidFile], "|");
hook_invocation(wait_for_vm_start) -> "hooks/builtin/wait_for_vm_start";
hook_invocation({wait_for_process, Name}) ->
    %% wait_for_process takes an atom as argument
    %% which is the process name to wait for
    rlx_string:join(["hooks/builtin/wait_for_process",
                 atom_to_list(Name)], "|");
hook_invocation(builtin_status) -> "hooks/builtin/status".

hook_template({custom, _}) -> custom;
hook_template(pid) -> builtin_hook_pid;
hook_template({pid, _}) -> builtin_hook_pid;
hook_template(wait_for_vm_start) -> builtin_hook_wait_for_vm_start;
hook_template({wait_for_process, _}) -> builtin_hook_wait_for_process;
hook_template(builtin_status) -> builtin_hook_status.

%% custom hooks are not rendered, they should
%% be copied by the release overlays
render_hook(custom, _, _) -> ok;
render_hook(TemplateName, Script, State) ->
    ec_cmd_log:info(
        rlx_state:log(State),
        "rendering ~p hook to ~p~n",
        [TemplateName, Script]
    ),

    Template = render(TemplateName),
    ok = filelib:ensure_dir(Script),
    _ = ec_file:remove(Script),
    ok = file:write_file(Script, Template),
    ok = file:change_mode(Script, 8#755).

include_nodetool(BinDir) ->
    NodeToolFile = nodetool_contents(),
    InstallUpgradeFile = install_upgrade_escript_contents(),
    NodeTool = filename:join([BinDir, "nodetool"]),
    InstallUpgrade = filename:join([BinDir, "install_upgrade.escript"]),
    ok = file:write_file(NodeTool, NodeToolFile),
    ok = file:write_file(InstallUpgrade, InstallUpgradeFile).

%% @doc generate a start_erl.data file
-spec generate_start_erl_data_file(rlx_release:t(), file:name()) ->
                                   ok | relx:error().
generate_start_erl_data_file(Release, ReleasesDir) ->
    ErtsVersion = rlx_release:erts(Release),
    ReleaseVersion = rlx_release:vsn(Release),
    Data = ErtsVersion ++ " " ++ ReleaseVersion,
    ok = file:write_file(filename:join(ReleasesDir, "start_erl.data"), Data).

%% @doc copy vm.args or generate one to releases/VSN/vm.args
-spec copy_or_generate_vmargs_file(rlx_state:t(), rlx_release:t(), file:name()) ->
                                              {ok, rlx_state:t()} | relx:error().

copy_or_generate_vmargs_file(State, Release, RelDir) ->
    RelVmargsPath = filename:join([RelDir, "vm.args"]),
    RelVmargsSrcPath = filename:join([RelDir, "vm.args.src"]),
    case rlx_state:vm_args_src(State) of
        undefined ->
            case rlx_state:vm_args(State) of
                false ->
                    ok;
                undefined ->
                    RelName = erlang:atom_to_list(rlx_release:name(Release)),
                    unless_exists_write_default(RelVmargsPath, vm_args_file(RelName));
                ArgsPath ->
                    case filelib:is_regular(ArgsPath) of
                        false ->
                            ?RLX_ERROR({vmargs_does_not_exist, ArgsPath});
                        true ->
                            copy_or_symlink_config_file(State, ArgsPath, RelVmargsPath)
                    end
            end;
        ArgsSrcPath ->
            %% print a warning if vm_args is also set
            case rlx_state:vm_args(State) of
                undefined ->
                    ok;
                _->
                    ec_cmd_log:warn(rlx_state:log(State),
                                    "Both vm_args_src and vm_args are set, vm_args will be ignored~n", [])
            end,

            case filelib:is_regular(ArgsSrcPath) of
                false ->
                    ?RLX_ERROR({vmargs_src_does_not_exist, ArgsSrcPath});
                true ->
                    copy_or_symlink_config_file(State, ArgsSrcPath, RelVmargsSrcPath)
            end
    end.

%% @doc copy config/sys.config or generate one to releases/VSN/sys.config
-spec copy_or_generate_sys_config_file(rlx_state:t(), file:name()) ->
                                              {ok, rlx_state:t()} | relx:error().
copy_or_generate_sys_config_file(State, RelDir) ->
    RelSysConfPath = filename:join([RelDir, "sys.config"]),
    RelSysConfSrcPath = filename:join([RelDir, "sys.config.src"]),
    case rlx_state:sys_config_src(State) of
        undefined ->
            case rlx_state:sys_config(State) of
                false ->
                    ok;
                undefined ->
                    unless_exists_write_default(RelSysConfPath, sys_config_file());
                ConfigPath ->
                    case filelib:is_regular(ConfigPath) of
                        false ->
                            ?RLX_ERROR({config_does_not_exist, ConfigPath});
                        true ->
                            %% validate sys.config is valid Erlang terms
                            case file:consult(ConfigPath) of
                                {ok, _} ->
                                    copy_or_symlink_config_file(State, ConfigPath, RelSysConfPath);
                                {error, Reason} ->
                                    ?RLX_ERROR({sys_config_parse_error, ConfigPath, Reason})
                            end
                    end
            end;
        ConfigSrcPath ->
            %% print a warning if sys_config is also set
            case rlx_state:sys_config(State) of
                P when P =:= false orelse P =:= undefined ->
                    ok;
                _->
                    ec_cmd_log:warn(rlx_state:log(State),
                                    "Both sys_config_src and sys_config are set, sys_config will be ignored~n", [])
            end,

            case filelib:is_regular(ConfigSrcPath) of
                false ->
                    ?RLX_ERROR({config_src_does_not_exist, ConfigSrcPath});
                true ->
                    copy_or_symlink_config_file(State, ConfigSrcPath, RelSysConfSrcPath)
            end
    end.

%% @doc copy config/sys.config or generate one to releases/VSN/sys.config
-spec copy_or_symlink_config_file(rlx_state:t(), file:name(), file:name()) ->
                                         ok.
copy_or_symlink_config_file(State, ConfigPath, RelConfPath) ->
    ensure_not_exist(RelConfPath),
    case rlx_state:dev_mode(State) of
        true ->
            ok = rlx_util:symlink_or_copy(ConfigPath, RelConfPath);
        _ ->
            ok = ec_file:copy(ConfigPath, RelConfPath, [{file_info, [mode, time]}])
    end.

%% @doc Optionally add erts directory to release, if defined.
-spec include_erts(rlx_state:t(), rlx_release:t(),  file:name(), file:name()) ->
                          {ok, rlx_state:t()} | relx:error().
include_erts(State, Release, OutputDir, RelDir) ->
    Prefix = case rlx_state:get(State, include_erts, true) of
                 false ->
                     false;
                 true ->
                     code:root_dir();
                 P ->
                     filename:absname(P)
    end,

    case Prefix of
        false ->
            make_boot_script(State, Release, OutputDir, RelDir);
        _ ->
            ec_cmd_log:info(rlx_state:log(State),
                            "Including Erts from ~s~n", [Prefix]),
            ErtsVersion = rlx_release:erts(Release),
            ErtsDir = filename:join([Prefix, "erts-" ++ ErtsVersion]),
            LocalErts = filename:join([OutputDir, "erts-" ++ ErtsVersion]),
            {OsFamily, _OsName} = rlx_util:os_type(State),
            case ec_file:is_dir(ErtsDir) of
                false ->
                    ?RLX_ERROR({specified_erts_does_not_exist, ErtsVersion});
                true ->
                    ok = ec_file:mkdir_p(LocalErts),
                    ok = ec_file:copy(ErtsDir, LocalErts, [recursive, {file_info, [mode, time]}]),
                    case OsFamily of
                        unix ->
                            Erl = filename:join([LocalErts, "bin", "erl"]),
                            ok = ec_file:remove(Erl),
                            ok = file:write_file(Erl, erl_script(ErtsVersion)),
                            ok = file:change_mode(Erl, 8#755);
                        win32 ->
                            ErlIni = filename:join([LocalErts, "bin", "erl.ini"]),
                            ok = ec_file:remove(ErlIni),
                            ok = file:write_file(ErlIni, erl_ini(OutputDir, ErtsVersion))
                    end,

                    %% delete erts src if the user requested it not be included
                    case rlx_state:include_src(State) of
                        true -> ok;
                        false ->
                            SrcDir = filename:join([LocalErts, "src"]),
                            %% ensure the src folder exists before deletion
                            case ec_file:exists(SrcDir) of
                              true -> ok = ec_file:remove(SrcDir, [recursive]);
                              false -> ok
                            end
                    end,

                    case rlx_state:get(State, extended_start_script, false) of
                        true ->

                            NodeToolFile = nodetool_contents(),
                            InstallUpgradeFile = install_upgrade_escript_contents(),
                            NodeTool = filename:join([LocalErts, "bin", "nodetool"]),
                            InstallUpgrade = filename:join([LocalErts, "bin", "install_upgrade.escript"]),
                            ok = file:write_file(NodeTool, NodeToolFile),
                            ok = file:write_file(InstallUpgrade, InstallUpgradeFile),
                            ok = file:change_mode(NodeTool, 8#755),
                            ok = file:change_mode(InstallUpgrade, 8#755);
                        false ->
                            ok
                    end,
                    make_boot_script(State, Release, OutputDir, RelDir)
            end
    end.

-spec make_boot_script(rlx_state:t(), rlx_release:t(), file:name(), file:name()) ->
                              {ok, rlx_state:t()} | relx:error().
make_boot_script(State, Release, OutputDir, RelDir) ->
    Options = [{path, [RelDir | rlx_util:get_code_paths(Release, OutputDir)]},
               {outdir, RelDir},
               {variables, make_boot_script_variables(State)},
               no_module_tests, silent],
    Name = erlang:atom_to_list(rlx_release:name(Release)),
    ReleaseFile = filename:join([RelDir, Name ++ ".rel"]),
    case rlx_util:make_script(Options,
                    fun(CorrectedOptions) ->
                            systools:make_script(Name, CorrectedOptions)
                    end) of
        ok ->
            ec_cmd_log:info(rlx_state:log(State),
                             "release successfully created!"),
            create_RELEASES(OutputDir, ReleaseFile),
            create_no_dot_erlang(RelDir, OutputDir, Options, State),
            create_start_clean(RelDir, OutputDir, Options, State);
        error ->
            ?RLX_ERROR({release_script_generation_error, ReleaseFile});
        {ok, _, []} ->
            ec_cmd_log:info(rlx_state:log(State),
                          "release successfully created!"),
            create_RELEASES(OutputDir, ReleaseFile),
            create_no_dot_erlang(RelDir, OutputDir, Options, State),
            create_start_clean(RelDir, OutputDir, Options, State);
        {ok,Module,Warnings} ->
            ?RLX_ERROR({release_script_generation_warn, Module, Warnings});
        {error,Module,Error} ->
            ?RLX_ERROR({release_script_generation_error, Module, Error})
    end.

make_boot_script_variables(State) ->
    % A boot variable is needed when {include_erts, false} and the application
    % directories are split between the release/lib directory and the erts/lib
    % directory.
    % The built-in $ROOT variable points to the erts directory on Windows
    % (dictated by erl.ini [erlang] Rootdir=) and so a boot variable is made
    % pointing to the release directory
    % On non-Windows, $ROOT is set by the ROOTDIR environment variable as the
    % release directory, so a boot variable is made pointing to the erts
    % directory.
    % NOTE the boot variable can point to either the release/erts root directory
    % or the release/erts lib directory, as long as the usage here matches the
    % usage used in the start up scripts
    case {os:type(), rlx_state:get(State, include_erts, true)} of
        {{win32, _}, false} ->
            [{"RELEASE_DIR", rlx_state:output_dir(State)}];
        {{win32, _}, true} ->
            [];
        _ ->
            [{"ERTS_LIB_DIR", code:lib_dir()}]
    end.

create_no_dot_erlang(RelDir, OutputDir, Options, State) ->
    create_boot_file(RelDir, OutputDir, Options, State, "no_dot_erlang").

create_start_clean(RelDir, OutputDir, Options, State) ->
    create_boot_file(RelDir, OutputDir, Options, State, "start_clean").

create_boot_file(RelDir, OutputDir, Options, State, Name) ->
    case rlx_util:make_script(Options,
                         fun(CorrectedOptions) ->
                                 systools:make_script(Name, CorrectedOptions)
                         end) of
        ok ->
            ok = ec_file:copy(filename:join([RelDir, Name++".boot"]),
                              filename:join([OutputDir, "bin", Name++".boot"]),
                              [{file_info, [mode, time]}]),
            ec_file:remove(filename:join([RelDir, Name++".rel"])),
            ec_file:remove(filename:join([RelDir, Name++".script"])),
            {ok, State};
        error ->
            ?RLX_ERROR(boot_script_generation_error);
        {ok, _, []} ->
            ok = ec_file:copy(filename:join([RelDir, Name++".boot"]),
                              filename:join([OutputDir, "bin", Name++".boot"]),
                              [{file_info, [mode, time]}]),
            ec_file:remove(filename:join([RelDir, Name++".rel"])),
            ec_file:remove(filename:join([RelDir, Name++".script"])),
            {ok, State};
        {ok,Module,Warnings} ->
            ?RLX_ERROR({boot_script_generation_warn, Module, Warnings});
        {error,Module,Error} ->
            ?RLX_ERROR({boot_script_generation_error, Module, Error})
    end.

create_RELEASES(OutputDir, ReleaseFile) ->
    {ok, OldCWD} = file:get_cwd(),
    file:set_cwd(OutputDir),
    release_handler:create_RELEASES("./",
                                    "releases",
                                    ReleaseFile,
                                    []),
    file:set_cwd(OldCWD).

unless_exists_write_default(Path, File) ->
    case ec_file:exists(Path) of
        true ->
            ok;
        false ->
            ok = file:write_file(Path, File)
    end.

-spec ensure_not_exist(file:name()) -> ok.
ensure_not_exist(RelConfPath) ->
    case ec_file:exists(RelConfPath) of
        false ->
            ok;
        _ ->
            ec_file:remove(RelConfPath)
    end.

erl_script(ErtsVsn) ->
    render(erl_script, [{erts_vsn, ErtsVsn}]).

bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts) ->
    Template = case OsFamily of
        unix -> bin;
        win32 -> bin_windows
    end,
    render(Template, [{rel_name, RelName}, {rel_vsn, RelVsn},
                      {erts_vsn, ErtsVsn}, {erl_opts, ErlOpts}]).

extended_bin_file_contents(OsFamily, RelName, RelVsn, ErtsVsn, ErlOpts, Hooks, Extensions) ->
    Template = case OsFamily of
        unix -> extended_bin;
        win32 -> extended_bin_windows
    end,
    %% turn all the hook lists into space separated strings
    PreStartHooks = rlx_string:join(proplists:get_value(pre_start, Hooks, []), " "),
    PostStartHooks = rlx_string:join(proplists:get_value(post_start, Hooks, []), " "),
    PreStopHooks = rlx_string:join(proplists:get_value(pre_stop, Hooks, []), " "),
    PostStopHooks = rlx_string:join(proplists:get_value(post_stop, Hooks, []), " "),
    PreInstallUpgradeHooks = rlx_string:join(proplists:get_value(pre_install_upgrade,
                                                Hooks, []), " "),
    PostInstallUpgradeHooks = rlx_string:join(proplists:get_value(post_install_upgrade,
                                                 Hooks, []), " "),
    StatusHook = rlx_string:join(proplists:get_value(status, Hooks, []), " "),
    {ExtensionsList1, ExtensionDeclarations1} =
        lists:foldl(fun({Name, Script},
                        {ExtensionsList0, ExtensionDeclarations0}) ->
                            ExtensionDeclaration = atom_to_list(Name) ++
                                                   "_extension=\"" ++
                                                   Script ++ "\"",
                            {ExtensionsList0 ++ [atom_to_list(Name)],
                             ExtensionDeclarations0 ++ [ExtensionDeclaration]}
                    end, {[], []}, Extensions),
    % pipe separated string of extensions, to show on the start script usage
    % (eg. foo|bar)
    ExtensionsList = rlx_string:join(ExtensionsList1 ++ ["undefined"], "|"),
    % command separated string of extension script declarations
    % (eg. foo_extension="path/to/foo_script")
    ExtensionDeclarations = rlx_string:join(ExtensionDeclarations1, ";"),
    render(Template, [{rel_name, RelName}, {rel_vsn, RelVsn},
                      {erts_vsn, ErtsVsn}, {erl_opts, ErlOpts},
                      {pre_start_hooks, PreStartHooks},
                      {post_start_hooks, PostStartHooks},
                      {pre_stop_hooks, PreStopHooks},
                      {post_stop_hooks, PostStopHooks},
                      {pre_install_upgrade_hooks, PreInstallUpgradeHooks},
                      {post_install_upgrade_hooks, PostInstallUpgradeHooks},
                      {status_hook, StatusHook},
                      {extensions, ExtensionsList},
                      {extension_declarations, ExtensionDeclarations}]).

erl_ini(OutputDir, ErtsVsn) ->
    ErtsDirName = rlx_string:concat("erts-", ErtsVsn),
    BinDir = filename:join([OutputDir, ErtsDirName, bin]),
    render(erl_ini, [{bin_dir, BinDir}, {output_dir, OutputDir}]).

install_upgrade_escript_contents() ->
    render(install_upgrade_escript).

nodetool_contents() ->
    render(nodetool).

sys_config_file() ->
    render(sys_config).

vm_args_file(RelName) ->
    render(vm_args, [{rel_name, RelName}]).

render(Template) ->
    render(Template, []).

render(Template, Data) ->
    Files = rlx_util:template_files(),
    Tpl = rlx_util:load_file(Files, escript, atom_to_list(Template)),
    {ok, Content} = rlx_util:render(Tpl, Data),
    Content.