diff options
author | Fred Hebert <mononcqc@ferd.ca> | 2017-12-08 07:47:44 -0500 |
---|---|---|
committer | Fred Hebert <mononcqc@ferd.ca> | 2017-12-08 07:47:44 -0500 |
commit | 553cee679f1dbe5badaff5ba9c2b98ef953c047e (patch) | |
tree | 84b3c9557621fc4965e7608353775911af0a3d40 /bootstrap | |
parent | 8b2bca0e64d5f54b97f6315aa5b2c3380ed92c99 (diff) |
Update bootstrap script // fix windows issues
The bootstrap script has been mostly unchanged for a long period of
time, and has not benefited from all the changes and improvements that
rebar3 itself had, including (but not limited to) path escaping,
handling of directories on windows, and edge case management when it
comes to file and symlink handling.
This patch brings the updates seen in rebar_string_utils, rebar_utils,
and rebar_dir into the bootstrap script so that fewer people have build
issues when starting from source, from scratch.
Diffstat (limited to 'bootstrap')
-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 |