diff options
-rwxr-xr-x | bootstrap | 293 |
1 files changed, 265 insertions, 28 deletions
@@ -179,6 +179,8 @@ bootstrap_rebar3() -> -define(FMT(Str, Args), lists:flatten(io_lib:format(Str, Args))). %%/rebar.hrl %%rebar_file_utils +-include_lib("kernel/include/file.hrl"). + symlink_or_copy(Source, Target) -> Link = case os:type() of {win32, _} -> @@ -190,55 +192,63 @@ symlink_or_copy(Source, Target) -> ok -> ok; {error, eexist} -> - ok; + exists; {error, _} -> - cp_r([Source], Target) + case os:type() of + {win32, _} -> + S = unicode:characters_to_list(Source), + T = unicode:characters_to_list(Target), + case filelib:is_dir(S) of + true -> + win32_symlink_or_copy(S, T); + false -> + cp_r([S], T) + end; + _ -> + case filelib:is_dir(Target) of + true -> + ok; + false -> + cp_r([Source], Target) + end + end end. -make_relative_path(Source, Target) -> - do_make_relative_path(filename:split(Source), filename:split(Target)). - -do_make_relative_path([H|T1], [H|T2]) -> - do_make_relative_path(T1, T2); -do_make_relative_path(Source, Target) -> - Base = lists:duplicate(max(length(Target) - 1, 0), ".."), - filename:join(Base ++ Source). - +-spec cp_r(list(string()), file:filename()) -> 'ok'. cp_r([], _Dest) -> ok; cp_r(Sources, Dest) -> case os:type() of {unix, _} -> - EscSources = [escape_path(Src) || Src <- Sources], + EscSources = [escape_chars(Src) || Src <- Sources], SourceStr = join(EscSources, " "), - os:cmd(?FMT("cp -R ~s \"~s\"", [SourceStr, Dest])), + {ok, []} = sh(?FMT("cp -Rp ~ts \"~ts\"", + [SourceStr, escape_double_quotes(Dest)]), + [{use_stdout, false}, abort_on_error]), ok; {win32, _} -> lists:foreach(fun(Src) -> ok = cp_r_win32(Src,Dest) end, Sources), ok end. -xcopy_win32(Source,Dest)-> - R = os:cmd(?FMT("xcopy \"~s\" \"~s\" /q /y /e 2> nul", - [filename:nativename(Source), filename:nativename(Dest)])), - case length(R) > 0 of - %% when xcopy fails, stdout is empty and and error message is printed - %% to stderr (which is redirected to nul) +%% @private Compatibility function for windows +win32_symlink_or_copy(Source, Target) -> + Res = sh(?FMT("cmd /c mklink /j \"~ts\" \"~ts\"", + [escape_double_quotes(filename:nativename(Target)), + escape_double_quotes(filename:nativename(Source))]), + [{use_stdout, false}, return_on_error]), + case win32_mklink_ok(Res, Target) of true -> ok; - false -> - {error, lists:flatten( - io_lib:format("Failed to xcopy from ~s to ~s~n", - [Source, Dest]))} + false -> cp_r_win32(Source, drop_last_dir_from_path(Target)) end. cp_r_win32({true, SourceDir}, {true, DestDir}) -> %% from directory to directory - SourceBase = filename:basename(SourceDir), - ok = case file:make_dir(filename:join(DestDir, SourceBase)) of + ok = case file:make_dir(DestDir) of {error, eexist} -> ok; Other -> Other end, - ok = xcopy_win32(SourceDir, filename:join(DestDir, SourceBase)); + ok = xcopy_win32(SourceDir, DestDir); cp_r_win32({false, Source} = S,{true, DestDir}) -> %% from file to directory cp_r_win32(S, {false, filename:join(DestDir, filename:basename(Source))}); @@ -272,10 +282,231 @@ cp_r_win32(Source,Dest) -> end, filelib:wildcard(Source)), ok. -escape_path(Str) -> - re:replace(Str, "([ ()?])", "\\\\&", [global, {return, list}]). +%% drops the last 'node' of the filename, presumably the last dir such as 'src' +%% this is because cp_r_win32/2 automatically adds the dir name, to appease +%% robocopy and be more uniform with POSIX +drop_last_dir_from_path([]) -> + []; +drop_last_dir_from_path(Path) -> + case lists:droplast(filename:split(Path)) of + [] -> []; + Dirs -> filename:join(Dirs) + end. + +%% @private specifically pattern match against the output +%% of the windows 'mklink' shell call; different values from +%% what win32_ok/1 handles +win32_mklink_ok({ok, _}, _) -> + true; +win32_mklink_ok({error,{1,"Local NTFS volumes are required to complete the operation.\n"}}, _) -> + false; +win32_mklink_ok({error,{1,"Cannot create a file when that file already exists.\n"}}, Target) -> + % File or dir is already in place; find if it is already a symlink (true) or + % if it is a directory (copy-required; false) + is_symlink(Target); +win32_mklink_ok(_, _) -> + false. + +xcopy_win32(Source,Dest)-> + %% "xcopy \"~ts\" \"~ts\" /q /y /e 2> nul", Changed to robocopy to + %% handle long names. May have issues with older windows. + Cmd = case filelib:is_dir(Source) of + true -> + %% For robocopy, copying /a/b/c/ to /d/e/f/ recursively does not + %% create /d/e/f/c/*, but rather copies all files to /d/e/f/*. + %% The usage we make here expects the former, not the later, so we + %% must manually add the last fragment of a directory to the `Dest` + %% in order to properly replicate POSIX platforms + NewDest = filename:join([Dest, filename:basename(Source)]), + ?FMT("robocopy \"~ts\" \"~ts\" /e 1> nul", + [escape_double_quotes(filename:nativename(Source)), + escape_double_quotes(filename:nativename(NewDest))]); + false -> + ?FMT("robocopy \"~ts\" \"~ts\" \"~ts\" /e 1> nul", + [escape_double_quotes(filename:nativename(filename:dirname(Source))), + escape_double_quotes(filename:nativename(Dest)), + escape_double_quotes(filename:basename(Source))]) + end, + Res = sh(Cmd, [{use_stdout, false}, return_on_error]), + case win32_ok(Res) of + true -> ok; + false -> + {error, lists:flatten( + io_lib:format("Failed to copy ~ts to ~ts~n", + [Source, Dest]))} + end. + +is_symlink(Filename) -> + {ok, Info} = file:read_link_info(Filename), + Info#file_info.type == symlink. + +win32_ok({ok, _}) -> true; +win32_ok({error, {Rc, _}}) when Rc<9; Rc=:=16 -> true; +win32_ok(_) -> false. + %%/rebar_file_utils +%%rebar_utils + +%% escape\ as\ a\ shell\? +escape_chars(Str) when is_atom(Str) -> + escape_chars(atom_to_list(Str)); +escape_chars(Str) -> + re:replace(Str, "([ ()?`!$&;\"\'])", "\\\\&", + [global, {return, list}, unicode]). + +%% "escape inside these" +escape_double_quotes(Str) -> + re:replace(Str, "([\"\\\\`!$&*;])", "\\\\&", + [global, {return, list}, unicode]). + +sh(Command0, Options0) -> + DefaultOptions = [{use_stdout, false}], + Options = [expand_sh_flag(V) + || V <- proplists:compact(Options0 ++ DefaultOptions)], + + ErrorHandler = proplists:get_value(error_handler, Options), + OutputHandler = proplists:get_value(output_handler, Options), + + Command = lists:flatten(patch_on_windows(Command0, proplists:get_value(env, Options, []))), + PortSettings = proplists:get_all_values(port_settings, Options) ++ + [exit_status, {line, 16384}, use_stdio, stderr_to_stdout, hide, eof], + Port = open_port({spawn, Command}, PortSettings), + + try + case sh_loop(Port, OutputHandler, []) of + {ok, _Output} = Ok -> + Ok; + {error, {_Rc, _Output}=Err} -> + ErrorHandler(Command, Err) + end + after + port_close(Port) + end. + +sh_loop(Port, Fun, Acc) -> + receive + {Port, {data, {eol, Line}}} -> + sh_loop(Port, Fun, Fun(Line ++ "\n", Acc)); + {Port, {data, {noeol, Line}}} -> + sh_loop(Port, Fun, Fun(Line, Acc)); + {Port, eof} -> + Data = lists:flatten(lists:reverse(Acc)), + receive + {Port, {exit_status, 0}} -> + {ok, Data}; + {Port, {exit_status, Rc}} -> + {error, {Rc, Data}} + end + end. + +expand_sh_flag(return_on_error) -> + {error_handler, + fun(_Command, Err) -> + {error, Err} + end}; +expand_sh_flag(abort_on_error) -> + {error_handler, + fun log_and_abort/2}; +expand_sh_flag({use_stdout, false}) -> + {output_handler, + fun(Line, Acc) -> + [Line | Acc] + end}; +expand_sh_flag({cd, _CdArg} = Cd) -> + {port_settings, Cd}; +expand_sh_flag({env, _EnvArg} = Env) -> + {port_settings, Env}. + +%% We do the shell variable substitution ourselves on Windows and hope that the +%% command doesn't use any other shell magic. +patch_on_windows(Cmd, Env) -> + case os:type() of + {win32,nt} -> + Cmd1 = "cmd /q /c " + ++ lists:foldl(fun({Key, Value}, Acc) -> + expand_env_variable(Acc, Key, Value) + end, Cmd, Env), + %% Remove left-over vars + re:replace(Cmd1, "\\\$\\w+|\\\${\\w+}", "", + [global, {return, list}, unicode]); + _ -> + Cmd + end. + +%% @doc Given env. variable `FOO' we want to expand all references to +%% it in `InStr'. References can have two forms: `$FOO' and `${FOO}' +%% The end of form `$FOO' is delimited with whitespace or EOL +-spec expand_env_variable(string(), string(), term()) -> string(). +expand_env_variable(InStr, VarName, RawVarValue) -> + case chr(InStr, $$) of + 0 -> + %% No variables to expand + InStr; + _ -> + ReOpts = [global, unicode, {return, list}], + VarValue = re:replace(RawVarValue, "\\\\", "\\\\\\\\", ReOpts), + %% Use a regex to match/replace: + %% Given variable "FOO": match $FOO\s | $FOOeol | ${FOO} + RegEx = io_lib:format("\\\$(~ts(\\W|$)|{~ts})", [VarName, VarName]), + re:replace(InStr, RegEx, [VarValue, "\\2"], ReOpts) + end. + +-spec log_and_abort(string(), {integer(), string()}) -> no_return(). +log_and_abort(Command, {Rc, Output}) -> + io:format("sh(~ts)~n" + "failed with return code ~w and the following output:~n" + "~ts", [Command, Rc, Output]), + throw(bootstrap_abort). + +%%/rebar_utils + +%%rebar_dir +make_relative_path(Source, Target) -> + AbsSource = make_normalized_path(Source), + AbsTarget = make_normalized_path(Target), + do_make_relative_path(filename:split(AbsSource), filename:split(AbsTarget)). + +%% @private based on fragments of paths, replace the number of common +%% segments by `../' bits, and add the rest of the source alone after it +-spec do_make_relative_path([string()], [string()]) -> file:filename(). +do_make_relative_path([H|T1], [H|T2]) -> + do_make_relative_path(T1, T2); +do_make_relative_path(Source, Target) -> + Base = lists:duplicate(max(length(Target) - 1, 0), ".."), + filename:join(Base ++ Source). + +make_normalized_path(Path) -> + AbsPath = make_absolute_path(Path), + Components = filename:split(AbsPath), + make_normalized_path(Components, []). + +make_absolute_path(Path) -> + case filename:pathtype(Path) of + absolute -> + Path; + relative -> + {ok, Dir} = file:get_cwd(), + filename:join([Dir, Path]); + volumerelative -> + Volume = hd(filename:split(Path)), + {ok, Dir} = file:get_cwd(Volume), + filename:join([Dir, Path]) + end. + +-spec make_normalized_path([string()], [string()]) -> file:filename(). +make_normalized_path([], NormalizedPath) -> + filename:join(lists:reverse(NormalizedPath)); +make_normalized_path([H|T], NormalizedPath) -> + case H of + "." when NormalizedPath == [], T == [] -> make_normalized_path(T, ["."]); + "." -> make_normalized_path(T, NormalizedPath); + ".." when NormalizedPath == [] -> make_normalized_path(T, [".."]); + ".." when hd(NormalizedPath) =/= ".." -> make_normalized_path(T, tl(NormalizedPath)); + _ -> make_normalized_path(T, [H|NormalizedPath]) + end. +%%/rebar_dir + setup_env() -> %% We don't need or want relx providers loaded yet application:load(rebar), @@ -412,3 +643,9 @@ join([], Sep) when is_list(Sep) -> []; join([H|T], Sep) -> H ++ lists:append([Sep ++ X || X <- T]). + +%% Same for chr; no non-deprecated equivalent in OTP20+ +chr(S, C) when is_integer(C) -> chr(S, C, 1). +chr([C|_Cs], C, I) -> I; +chr([_|Cs], C, I) -> chr(Cs, C, I+1); +chr([], _C, _I) -> 0.
\ No newline at end of file |