From 078d3c349b3f465dc2f45f0bbfcff297e82074e5 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 26 Dec 2012 20:19:54 -0500 Subject: overlay support This includes support for overlays and fairly complete tests --- rebar.config | 3 + src/rcl_prv_overlay.erl | 324 ++++++++++++++++++++++++++++++++++++++++++++ src/rcl_state.erl | 7 +- test/rclt_release_SUITE.erl | 163 +++++++++++++++++++++- 4 files changed, 492 insertions(+), 5 deletions(-) create mode 100644 src/rcl_prv_overlay.erl diff --git a/rebar.config b/rebar.config index 3294d13..3561991 100644 --- a/rebar.config +++ b/rebar.config @@ -8,6 +8,9 @@ {erlware_commons, ".*", {git, "https://github.com/erlware/erlware_commons.git", {branch, "next"}}}, + {erlydtl, ".*", + {git, "https://github.com/evanmiller/erlydtl.git", + {branch, "master"}}}, {getopt, "", {git, "https://github.com/jcomellas/getopt.git", {branch, "master"}}}]}. diff --git a/src/rcl_prv_overlay.erl b/src/rcl_prv_overlay.erl new file mode 100644 index 0000000..af914ee --- /dev/null +++ b/src/rcl_prv_overlay.erl @@ -0,0 +1,324 @@ +%% -*- 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_overlay). + +-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), + case rcl_release:realized(Release) of + true -> + generate_overlay_vars(State, Release); + 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({unable_to_read_varsfile, FileName, Reason}) -> + io_lib:format("Unable to read vars file (~s) for overlay due to: ~p", + [FileName, Reason]); +format_error({overlay_failed, Errors}) -> + [[format_error(rcl_util:error_reason(Error)), "\n"] || Error <- Errors]; +format_error({dir_render_failed, Dir, Error}) -> + io_lib:format("rendering mkdir path failed ~s with ~p", + [Dir, Error]); +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)]); +format_error({copy_failed, FromFile, ToFile, Err}) -> + io_lib:format("Unable to copy from ~s to ~s because of ~p", + [FromFile, ToFile, Err]); +format_error({unable_to_write, ToFile, Reason}) -> + io_lib:format("Unable to write to ~s because ~p", + [ToFile, Reason]); +format_error({unable_to_enclosing_dir, ToFile, Reason}) -> + io_lib:format("Unable to create enclosing directory for ~s because ~p", + [ToFile, Reason]); +format_error({unable_to_render_template, FromFile, Reason}) -> + io_lib:format("Unable to render template ~s because ~p", + [FromFile, Reason]); +format_error({unable_to_compile_template, FromFile, Reason}) -> + io_lib:format("Unable to compile template ~s because ~p", + [FromFile, Reason]); +format_error({unable_to_make_dir, Absolute, Error}) -> + io_lib:format("Unable to make directory ~s because ~p", + [Absolute, Error]). + +%%%=================================================================== +%%% Internal Functions +%%%=================================================================== +-spec generate_overlay_vars(rcl_state:t(), rcl_release:t()) -> + {ok, rcl_state:t()} | relcool:error(). +generate_overlay_vars(State, Release) -> + StateVars = generate_state_vars(State), + ReleaseVars = generate_release_vars(Release), + get_overlay_vars_from_file(State, StateVars ++ ReleaseVars). + +-spec get_overlay_vars_from_file(rcl_state:t(), proplists:proplist()) -> + {ok, rcl_state:t()} | relcool:error(). +get_overlay_vars_from_file(State, OverlayVars) -> + case rcl_state:get(State, overlay_vars, undefined) of + undefined -> + do_overlay(State, OverlayVars); + FileName -> + read_overlay_vars(State, OverlayVars, FileName) + end. + +-spec read_overlay_vars(rcl_state:t(), proplists:proplist(), file:name()) -> + {ok, rcl_state:t()} | relcool:error(). +read_overlay_vars(State, OverlayVars, FileName) -> + case file:consult(FileName) of + {ok, Terms} -> + do_overlay(State, OverlayVars ++ Terms); + {error, Reason} -> + ?RCL_ERROR({unable_to_read_varsfile, FileName, Reason}) + end. + +-spec generate_release_vars(rcl_release:t()) -> proplists:proplist(). +generate_release_vars(Release) -> + [{erts_vsn, rcl_release:erts(Release)}, + {release_erts_version, rcl_release:erts(Release)}, + {release_name, rcl_release:name(Release)}, + {rel_vsn, rcl_release:vsn(Release)}, + {release_version, rcl_release:vsn(Release)}, + {release_applications, lists:map(fun(App) -> + rcl_app_info:name(App) + end, rcl_release:application_details(Release))}, + {release, [generate_app_vars(App)|| App <- rcl_release:application_details(Release)]}, + {release_goals, [if + erlang:is_list(Constraint) -> + Constraint; + true -> + rcl_depsolver:format_constraint(Constraint) + end || Constraint <- rcl_release:goals(Release)]}]. + +-spec generate_app_vars(rcl_app_info:t()) -> AppInfo::tuple(). +generate_app_vars(App) -> + {rcl_app_info:name(App), + [{version, rcl_app_info:vsn_as_string(App)}, + {dir, rcl_app_info:dir(App)}, + {active_dependencies, rcl_app_info:active_deps(App)}, + {library_dependencies, rcl_app_info:library_deps(App)}, + {link, rcl_app_info:link(App)}]}. + +-spec generate_state_vars(rcl_state:t()) -> proplists:proplist(). +generate_state_vars(State) -> + [{log, rcl_log:format(rcl_state:log(State))}, + {output_dir, rcl_state:output_dir(State)}, + {target_dir, rcl_state:output_dir(State)}, + {overridden, [AppName || {AppName, _} <- rcl_state:overrides(State)]}, + {overrides, rcl_state:overrides(State)}, + {goals, [rcl_depsolver:format_constraint(Constraint) || + Constraint <- rcl_state:goals(State)]}, + {lib_dirs, rcl_state:lib_dirs(State)}, + {config_files, rcl_state:config_files(State)}, + {providers, rcl_state:providers(State)}, + {sys_config, rcl_state:sys_config(State)}, + {root_dir, rcl_state:root_dir(State)}, + {default_release_name, case rcl_state:default_release(State) of + {Name0, _} -> + Name0 + end}, + {default_release_version, case rcl_state:default_release(State) of + {_, Vsn0} -> + Vsn0 + end}, + {default_release, case rcl_state:default_release(State) of + {Name1, undefined} -> + erlang:atom_to_list(Name1); + {Name1, Vsn1} -> + erlang:atom_to_list(Name1) ++ "-" ++ Vsn1 + end}]. + +-spec do_overlay(rcl_state:t(), proplists:proplist()) -> + {ok, rcl_state:t()} | relcool:error(). +do_overlay(State, OverlayVars) -> + case rcl_state:get(State, overlay, undefined) of + undefined -> + {ok, State}; + Overlays -> + handle_errors(State, + ec_plists:map(fun(Overlay) -> + io:format("--->Doing ~p~n", [Overlay]), + Res = do_individual_overlay(State, OverlayVars, + Overlay), + io:format("-->Done ~p~n", [Overlay]), + Res + end, Overlays)) + end. + +-spec handle_errors(rcl_state:t(), [ok | relcool:error()]) -> + {ok, rcl_state:t()} | relcool:error(). +handle_errors(State, Result) -> + case [Error || Error <- Result, + rcl_util:is_error(Error)] of + Errors = [_|_] -> + ?RCL_ERROR({overlay_failed, Errors}); + [] -> + {ok, State} + end. + +-spec do_individual_overlay(rcl_state:t(), proplists:proplist(), + OverlayDirective::term()) -> + {ok, rcl_state:t()} | relcool:error(). +do_individual_overlay(State, OverlayVars, {mkdir, Dir}) -> + ModuleName = make_template_name("rcl_mkdir_template", Dir), + io:format("compiling to ~p ~n", [ModuleName]), + case erlydtl:compile(erlang:iolist_to_binary(Dir), ModuleName) of + {ok, ModuleName} -> + io:format("compiled ~n"), + case render(ModuleName, OverlayVars) of + {ok, IoList} -> + io:format("rendered ~n"), + Absolute = filename:absname(filename:join(rcl_state:root_dir(State), + erlang:iolist_to_binary(IoList))), + io:format("got ~p ~n", [Absolute]), + case rcl_util:mkdir_p(Absolute) of + {error, Error} -> + ?RCL_ERROR({unable_to_make_dir, Absolute, Error}); + ok -> + ok + end; + {error, Error} -> + ?RCL_ERROR({dir_render_failed, Dir, Error}) + end; + {error, Reason} -> + ?RCL_ERROR({unable_to_compile_template, Dir, Reason}) + end; +do_individual_overlay(State, OverlayVars, {copy, From, To}) -> + FromTemplateName = make_template_name("rcl_copy_from_template", From), + ToTemplateName = make_template_name("rcl_copy_to_template", To), + file_render_do(State, OverlayVars, From, FromTemplateName, + fun(FromFile) -> + file_render_do(State, OverlayVars, To, ToTemplateName, + fun(ToFile) -> + filelib:ensure_dir(ToFile), + case ec_file:copy(FromFile, ToFile) of + ok -> + ok; + {error, Err} -> + ?RCL_ERROR({copy_failed, + FromFile, + ToFile, Err}) + end + end) + end); +do_individual_overlay(State, OverlayVars, {template, From, To}) -> + FromTemplateName = make_template_name("rcl_template_from_template", From), + ToTemplateName = make_template_name("rcl_template_to_template", To), + file_render_do(State, OverlayVars, From, FromTemplateName, + fun(FromFile) -> + file_render_do(State, OverlayVars, To, ToTemplateName, + fun(ToFile) -> + render_template(OverlayVars, + erlang:binary_to_list(FromFile), + ToFile) + end) + end). + +-spec render_template(proplists:proplist(), iolist(), file:name()) -> + ok | relcool:error(). +render_template(OverlayVars, FromFile, ToFile) -> + TemplateName = make_template_name("rcl_template_renderer", FromFile), + case erlydtl:compile(FromFile, TemplateName) of + Good when Good =:= ok; ok =:= {ok, TemplateName} -> + case render(TemplateName, OverlayVars) of + {ok, IoData} -> + io:format("Rendering ~p~n", [IoData]), + case filelib:ensure_dir(ToFile) of + ok -> + case file:write_file(ToFile, IoData) of + ok -> + ok; + {error, Reason} -> + ?RCL_ERROR({unable_to_write, ToFile, Reason}) + end; + {error, Reason} -> + ?RCL_ERROR({unable_to_enclosing_dir, ToFile, Reason}) + end; + {error, Reason} -> + ?RCL_ERROR({unable_to_render_template, FromFile, Reason}) + end; + {error, Reason} -> + ?RCL_ERROR({unable_to_compile_template, FromFile, Reason}) + end. + +-spec file_render_do(rcl_state:t(), proplists:proplist(), iolist(), module(), + fun((term()) -> {ok, rcl_state:t()} | relcool:error())) -> + {ok, rcl_state:t()} | relcool:error(). +file_render_do(State, OverlayVars, Data, TemplateName, NextAction) -> + case erlydtl:compile(erlang:iolist_to_binary(Data), TemplateName) of + {ok, TemplateName} -> + case render(TemplateName, OverlayVars) of + {ok, IoList} -> + Absolute = filename:absname(filename:join(rcl_state:root_dir(State), + erlang:iolist_to_binary(IoList))), + NextAction(Absolute); + {error, Error} -> + ?RCL_ERROR({render_failed, Data, Error}) + end; + {error, Reason} -> + ?RCL_ERROR({unable_to_compile_template, Data, Reason}) + end. + +-spec make_template_name(string(), term()) -> module(). +make_template_name(Base, Value) -> + %% Seed so we get different values each time + random:seed(erlang:now()), + Hash = erlang:phash2(Value), + Ran = random:uniform(10000000), + erlang:list_to_atom(Base ++ "_" ++ + erlang:integer_to_list(Hash) ++ + "_" ++ erlang:integer_to_list(Ran)). + +-spec render(module(), proplists:proplist()) -> {ok, iolist()} | {error, Reason::term()}. +render(ModuleName, OverlayVars) -> + try + ModuleName:render(OverlayVars) + catch + _:Reason -> + {error, Reason} + end. diff --git a/src/rcl_state.erl b/src/rcl_state.erl index 21fbbb3..eb70ecc 100644 --- a/src/rcl_state.erl +++ b/src/rcl_state.erl @@ -280,9 +280,10 @@ 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), - {AssemblerProvider, {ok, State4}} = rcl_provider:new(rcl_prv_assembler, State3), - State4#state_t{providers=[ConfigProvider, DiscoveryProvider, - ReleaseProvider, AssemblerProvider]}. + {OverlayProvider, {ok, State4}} = rcl_provider:new(rcl_prv_overlay, State3), + {AssemblerProvider, {ok, State5}} = rcl_provider:new(rcl_prv_assembler, State4), + State5#state_t{providers=[ConfigProvider, DiscoveryProvider, + ReleaseProvider, OverlayProvider, AssemblerProvider]}. %% @doc config files can come in as either a single file name or as a list of diff --git a/test/rclt_release_SUITE.erl b/test/rclt_release_SUITE.erl index b37fd12..f4f0ecc 100644 --- a/test/rclt_release_SUITE.erl +++ b/test/rclt_release_SUITE.erl @@ -27,7 +27,8 @@ make_release/1, make_overridden_release/1, make_rerun_overridden_release/1, - make_implicit_config_release/1]). + make_implicit_config_release/1, + overlay_release/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -52,7 +53,7 @@ init_per_testcase(_, Config) -> all() -> [make_release, make_overridden_release, make_implicit_config_release, - make_rerun_overridden_release]. + make_rerun_overridden_release, overlay_release]. make_release(Config) -> LibDir1 = proplists:get_value(lib1, Config), @@ -235,6 +236,129 @@ make_rerun_overridden_release(Config) -> OverrideApp ++ "-" ++ OverrideVsn])), ?assertMatch(OverrideAppDir, Real). +overlay_release(Config) -> + LibDir1 = proplists:get_value(lib1, Config), + [(fun({Name, Vsn}) -> + create_app(LibDir1, Name, Vsn, [kernel, stdlib], []) + end)(App) + || + App <- + [{create_random_name("lib_app1_"), create_random_vsn()} + || _ <- lists:seq(1, 100)]], + + create_app(LibDir1, "goal_app_1", "0.0.1", [stdlib,kernel,non_goal_1], []), + create_app(LibDir1, "lib_dep_1", "0.0.1", [stdlib,kernel], []), + create_app(LibDir1, "goal_app_2", "0.0.1", [stdlib,kernel,goal_app_1,non_goal_2], []), + create_app(LibDir1, "non_goal_1", "0.0.1", [stdlib,kernel], [lib_dep_1]), + create_app(LibDir1, "non_goal_2", "0.0.1", [stdlib,kernel], []), + + ConfigFile = filename:join([LibDir1, "relcool.config"]), + OverlayVars = filename:join([LibDir1, "vars.config"]), + Template = filename:join([LibDir1, "test_template"]), + write_config(ConfigFile, + [{overlay_vars, OverlayVars}, + {overlay, [{mkdir, "{{target_dir}}/fooo"}, + {copy, OverlayVars, + "{{target_dir}}/{{foo_dir}}/vars.config"}, + {template, Template, + "{{target_dir}}/test_template_resolved"}]}, + {release, {foo, "0.0.1"}, + [goal_app_1, + goal_app_2]}]), + + VarsFile = filename:join([LibDir1, "vars.config"]), + write_config(VarsFile, [{yahoo, "yahoo"}, + {yahoo2, [{foo, "bar"}]}, + {foo_dir, "foodir"}]), + + TemplateFile = filename:join([LibDir1, "test_template"]), + ok = file:write_file(TemplateFile, test_template_contents()), + + OutputDir = filename:join([proplists:get_value(data_dir, Config), + create_random_name("relcool-output")]), + + {ok, State} = relcool:do(undefined, undefined, [], [LibDir1], 2, + OutputDir, [ConfigFile]), + + [{{foo, "0.0.1"}, Release}] = ec_dictionary:to_list(rcl_state:releases(State)), + AppSpecs = rcl_release:applications(Release), + ?assert(lists:keymember(stdlib, 1, AppSpecs)), + ?assert(lists:keymember(kernel, 1, AppSpecs)), + ?assert(lists:member({non_goal_1, "0.0.1"}, AppSpecs)), + ?assert(lists:member({non_goal_2, "0.0.1"}, AppSpecs)), + ?assert(lists:member({goal_app_1, "0.0.1"}, AppSpecs)), + ?assert(lists:member({goal_app_2, "0.0.1"}, AppSpecs)), + ?assert(lists:member({lib_dep_1, "0.0.1", load}, AppSpecs)), + + ?assert(ec_file:exists(filename:join(OutputDir, "fooo"))), + ?assert(ec_file:exists(filename:join([OutputDir, "foodir", "vars.config"]))), + + TemplateData = case file:consult(filename:join([OutputDir, test_template_resolved])) of + {ok, Details} -> + Details; + Error -> + erlang:throw({failed_to_consult, Error}) + end, + + ?assertEqual(erlang:system_info(version), + proplists:get_value(erts_vsn, TemplateData)), + ?assertEqual(erlang:system_info(version), + proplists:get_value(release_erts_version, TemplateData)), + ?assertEqual("0.0.1", + proplists:get_value(release_version, TemplateData)), + ?assertEqual(foo, + proplists:get_value(release_name, TemplateData)), + ?assertEqual([kernel,stdlib,lib_dep_1,non_goal_2,non_goal_1, + goal_app_1,goal_app_2], + proplists:get_value(release_applications, TemplateData)), + ?assert(proplists:is_defined(std_version, TemplateData)), + ?assert(proplists:is_defined(kernel_version, TemplateData)), + ?assertEqual("0.0.1", + proplists:get_value(non_goal_1_version, TemplateData)), + ?assertEqual("0.0.1", + proplists:get_value(non_goal_2_version, TemplateData)), + ?assertEqual("0.0.1", + proplists:get_value(goal_app_1_version, TemplateData)), + ?assertEqual("0.0.1", + proplists:get_value(goal_app_2_version, TemplateData)), + ?assertEqual("0.0.1", + proplists:get_value(lib_dep_1, TemplateData)), + ?assert(proplists:is_defined(lib_dep_1_dir, TemplateData)), + ?assertEqual([stdlib,kernel], + proplists:get_value(lib_dep_1_active, TemplateData)), + ?assertEqual([], + proplists:get_value(lib_dep_1_library, TemplateData)), + ?assertEqual("false", + proplists:get_value(lib_dep_1_link, TemplateData)), + ?assertEqual("(2:debug)", + proplists:get_value(log, TemplateData)), + ?assertEqual(OutputDir, + proplists:get_value(output_dir, TemplateData)), + ?assertEqual(OutputDir, + proplists:get_value(target_dir, TemplateData)), + ?assertEqual([], + proplists:get_value(overridden, TemplateData)), + ?assertEqual([""], + proplists:get_value(goals, TemplateData)), + ?assert(proplists:is_defined(lib_dirs, TemplateData)), + ?assert(proplists:is_defined(config_files, TemplateData)), + ?assertEqual([""], + proplists:get_value(goals, TemplateData)), + ?assertEqual("undefined", + proplists:get_value(sys_config, TemplateData)), + ?assert(proplists:is_defined(root_dir, TemplateData)), + ?assertEqual(foo, + proplists:get_value(default_release_name, TemplateData)), + ?assertEqual("0.0.1", + proplists:get_value(default_release_version, TemplateData)), + ?assertEqual("foo-0.0.1", + proplists:get_value(default_release, TemplateData)), + ?assertEqual("yahoo", + proplists:get_value(yahoo, TemplateData)), + ?assertEqual("bar", + proplists:get_value(yahoo2_foo, TemplateData)), + ?assertEqual("foodir", + proplists:get_value(foo_dir, TemplateData)). %%%=================================================================== %%% Helper Functions @@ -278,3 +402,38 @@ create_random_vsn() -> write_config(Filename, Values) -> ok = ec_file:write(Filename, [io_lib:format("~p.\n", [Val]) || Val <- Values]). + +test_template_contents() -> + "{erts_vsn, \"{{erts_vsn}}\"}.\n" + "{release_erts_version, \"{{release_erts_version}}\"}.\n" + "{release_name, {{release_name}}}.\n" + "{rel_vsn, \"{{release_version}}\"}.\n" + "{release_version, \"{{release_version}}\"}.\n" + "{release_applications, [{{ release_applications|join:\", \" }}]}.\n" + "{std_version, \"{{release.stdlib.version}}\"}.\n" + "{kernel_version, \"{{release.kernel.version}}\"}.\n" + "{non_goal_1_version, \"{{release.non_goal_1.version}}\"}.\n" + "{non_goal_2_version, \"{{release.non_goal_2.version}}\"}.\n" + "{goal_app_1_version, \"{{release.goal_app_1.version}}\"}.\n" + "{goal_app_2_version, \"{{release.goal_app_2.version}}\"}.\n" + "{lib_dep_1, \"{{release.lib_dep_1.version}}\"}.\n" + "{lib_dep_1_dir, \"{{release.lib_dep_1.dir}}\"}.\n" + "{lib_dep_1_active, [{{ release.lib_dep_1.active_dependencies|join:\", \" }}]}.\n" + "{lib_dep_1_library, [{{ release.lib_dep_1.library_dependencies|join:\", \" }}]}.\n" + "{lib_dep_1_link, \"{{release.lib_dep_1.link}}\"}.\n" + "{log, \"{{log}}\"}.\n" + "{output_dir, \"{{output_dir}}\"}.\n" + "{target_dir, \"{{target_dir}}\"}.\n" + "{overridden, [{{ overridden|join:\", \" }}]}.\n" + "{goals, [\"{{ goals|join:\", \" }}\"]}.\n" + "{lib_dirs, [\"{{ lib_dirs|join:\", \" }}\"]}.\n" + "{config_files, [\"{{ config_files|join:\", \" }}\"]}.\n" + "{providers, [{{ providers|join:\", \" }}]}.\n" + "{sys_config, \"{{sys_config}}\"}.\n" + "{root_dir, \"{{root_dir}}\"}.\n" + "{default_release_name, {{default_release_name}}}.\n" + "{default_release_version, \"{{default_release_version}}\"}.\n" + "{default_release, \"{{default_release}}\"}.\n" + "{yahoo, \"{{yahoo}}\"}.\n" + "{yahoo2_foo, \"{{yahoo2.foo}}\"}.\n" + "{foo_dir, \"{{foo_dir}}\"}.\n". -- cgit v1.2.3