diff --git a/src/rebar_file_utils.erl b/src/rebar_file_utils.erl index 8158312b..89c6d931 100644 --- a/src/rebar_file_utils.erl +++ b/src/rebar_file_utils.erl @@ -29,6 +29,8 @@ -export([try_consult/1, consult_config/2, format_error/1, + symlink_or_create/2, + symlink/2, symlink_or_copy/2, rm_rf/1, cp_r/2, @@ -86,6 +88,36 @@ consult_config(State, Filename) -> format_error({bad_term_file, AppFile, Reason}) -> io_lib:format("Error reading file ~s: ~s", [AppFile, file:format_error(Reason)]). +-spec symlink_or_create(Source, Target) -> Result when + Source :: file:filename(), + Target :: file:filename(), + Result :: Ok | Error, + Ok :: ok, + Error :: {error, string()}. +symlink_or_create(Source, Target) -> + SourceExists = ec_file:is_dir(Source) orelse ec_file:is_symlink(Source), + + case SourceExists of + true -> force_link(Source, Target); + false -> force_shadow_dir(Target) + end. + +-spec symlink(Source, Target) -> Result when + Source :: file:filename(), + Target :: file:filename(), + Result :: Ok | Error, + Ok :: ok, + Error :: {error, string()}. +symlink(Source, Target) -> + case os:type() of + {win32, _} -> + S = unicode:characters_to_list(Source), + T = unicode:characters_to_list(Target), + win32_symlink(S, T); + _ -> + file:make_symlink(Source, Target) + end. + symlink_or_copy(Source, Target) -> Link = case os:type() of {win32, _} -> @@ -119,21 +151,6 @@ symlink_or_copy(Source, Target) -> end end. -win32_symlink(Source, Target) -> - Res = rebar_utils:sh( - ?FMT("cmd /c mklink /j \"~s\" \"~s\"", - [rebar_utils:escape_double_quotes(filename:nativename(Target)), - rebar_utils:escape_double_quotes(filename:nativename(Source))]), - [{use_stdout, false}, return_on_error]), - case win32_ok(Res) of - true -> ok; - false -> - {error, lists:flatten( - io_lib:format("Failed to symlink ~s to ~s~n", - [Source, Target]))} - end. - - %% @doc Remove files and directories. %% Target is a single filename, directoryname or wildcard expression. -spec rm_rf(string()) -> 'ok'. @@ -475,3 +492,39 @@ cp_r_win32(Source,Dest) -> ok = cp_r_win32({filelib:is_dir(Src), Src}, Dst) end, filelib:wildcard(Source)), ok. + +win32_symlink(Source, Target) -> + Res = rebar_utils:sh( + ?FMT("cmd /c mklink /j \"~s\" \"~s\"", + [rebar_utils:escape_double_quotes(filename:nativename(Target)), + rebar_utils:escape_double_quotes(filename:nativename(Source))]), + [{use_stdout, false}, return_on_error]), + case win32_ok(Res) of + true -> ok; + false -> + {error, lists:flatten( + io_lib:format("Failed to symlink ~s to ~s~n", + [Source, Target]))} + end. + +force_link(Source, Target) -> + %% remove any existing dir + ok = case ec_file:is_dir(Target) of + true -> ec_file:remove(Target, [recursive]); + false -> ok + end, + symlink(Source, Target). + +force_shadow_dir(Target) -> + %% remove any existing symlink + ok = case ec_file:is_symlink(Target) of + true -> ec_file:remove(Target); + false -> ok + end, + %% if a directory already exists leave it unaltered, otherwise + %% create it + case ec_file:is_dir(Target) of + true -> ok; + false -> filelib:ensure_dir(filename:join([Target, "dummy"])) + end. + diff --git a/test/rebar_file_utils_SUITE.erl b/test/rebar_file_utils_SUITE.erl index 4cc6a938..f8825d69 100644 --- a/test/rebar_file_utils_SUITE.erl +++ b/test/rebar_file_utils_SUITE.erl @@ -23,7 +23,11 @@ mv_file_diff/1, mv_file_dir_same/1, mv_file_dir_diff/1, - mv_no_clobber/1]). + mv_no_clobber/1, + symlink_existing_dir/1, + create_missing_dir/1, + delete_symlink_and_create_dir/1, + delete_dir_and_create_symlink/1]). -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). @@ -38,7 +42,11 @@ all() -> canonical_path, resolve_link, split_dirname, - mv_warning_is_ignored]. + mv_warning_is_ignored, + symlink_existing_dir, + create_missing_dir, + delete_symlink_and_create_dir, + delete_dir_and_create_symlink]. groups() -> [{tmpdir, [], [raw_tmpdir, empty_tmpdir, simple_tmpdir, multi_tmpdir]}, @@ -329,3 +337,84 @@ mk_base_dir(BasePath, Name) -> Path = filename:join(BasePath, atom_to_list(Name) ++ Index), ec_file:mkdir_p(Path), Path. + + +symlink_existing_dir(Config) -> + PrivDir = ?config(priv_dir, Config), + + Source = filename:join([PrivDir, "exists"]), + Target = filename:join([PrivDir, "_build", "exists"]), + ok = filelib:ensure_dir(filename:join([PrivDir, "_build", "dummy"])), + + ok = filelib:ensure_dir(filename:join([Source, "dummy"])), + + ok = rebar_file_utils:symlink_or_create(Source, Target), + + %% `Target' should be a symlink not a directory, and `Source' + %% should exist + ?assert(ec_file:is_symlink(Target)), + ?assert(ec_file:is_dir(Source)). + +create_missing_dir(Config) -> + PrivDir = ?config(priv_dir, Config), + + Source = filename:join([PrivDir, "missing"]), + Target = filename:join([PrivDir, "_build", "missing"]), + ok = filelib:ensure_dir(filename:join([PrivDir, "_build", "dummy"])), + + ok = rebar_file_utils:symlink_or_create(Source, Target), + + %% `Target' should be a directory not a symlink, and `Source' + %% should not exist + ?assert(ec_file:is_dir(Target)), + ?assert(not ec_file:is_symlink(Target)), + ?assert(not ec_file:is_dir(Source)). + +delete_symlink_and_create_dir(Config) -> + PrivDir = ?config(priv_dir, Config), + + Source = filename:join([PrivDir, "gone"]), + Target = filename:join([PrivDir, "_build", "gone"]), + ok = filelib:ensure_dir(filename:join([PrivDir, "_build", "dummy"])), + + %% create dir temporarily to ensure symlink can be created + ok = filelib:ensure_dir(filename:join([Source, "dummy"])), + + ?assert(ec_file:is_dir(Source)), + + ok = rebar_file_utils:symlink(Source, Target), + + %% remove source dir + ok = ec_file:remove(Source, [recursive]), + + ?assert(ec_file:is_symlink(Target)), + ?assert(not ec_file:is_dir(Source)), + + ok = rebar_file_utils:symlink_or_create(Source, Target), + + %% `Target' should be a directory not a symlink, and `Source' + %% should not exist + ?assert(not ec_file:is_symlink(Target)), + ?assert(ec_file:is_dir(Target)), + ?assert(not ec_file:is_dir(Source)). + +delete_dir_and_create_symlink(Config) -> + PrivDir = ?config(priv_dir, Config), + + Source = filename:join([PrivDir, "created"]), + Target = filename:join([PrivDir, "_build", "created"]), + ok = filelib:ensure_dir(filename:join([PrivDir, "_build", "dummy"])), + + ok = filelib:ensure_dir(filename:join([Source, "dummy"])), + ok = filelib:ensure_dir(filename:join([Target, "dummy"])), + + ?assert(not ec_file:is_symlink(Target)), + ?assert(ec_file:is_dir(Target)), + ?assert(ec_file:is_dir(Source)), + + ok = rebar_file_utils:symlink_or_create(Source, Target), + + %% `Target' should be a symlink not a directory, and `Source' + %% should exist + ?assert(ec_file:is_symlink(Target)), + ?assert(ec_file:is_dir(Source)). \ No newline at end of file