From 124f121d38322a3db50458caeac47e84dccee45c Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Fri, 6 May 2016 23:18:55 -0400 Subject: Support package hashes in structure and lockfile - the internal representation for package locks moves from `{Name, {pkg, PkgName, Vsn}, Lvl}` to `{Name, {pkg, PkgName, Vsn, Hash}, Lvl}` - the internal representation for packages moves from `{pkg, PkgName, Vsn}` to `{pkg, PkgName, Vsn, Hash}` - the hash can be `undefined`, meaning no check will be done - no checking is done yet. --- src/rebar.hrl | 1 + src/rebar_app_utils.erl | 16 ++++---- src/rebar_config.erl | 66 ++++++++++++++++++++++++++------- src/rebar_packages.erl | 2 +- src/rebar_pkg_resource.erl | 6 +-- src/rebar_prv_deps.erl | 2 +- test/mock_pkg_resource.erl | 11 +++--- test/rebar_deps_SUITE.erl | 2 +- test/rebar_install_deps_SUITE.erl | 2 +- test/rebar_lock_SUITE.erl | 78 ++++++++++++++++++++++++++++++++++++--- test/rebar_pkg_SUITE.erl | 14 +++---- test/rebar_test_utils.erl | 18 ++++----- 12 files changed, 163 insertions(+), 55 deletions(-) diff --git a/src/rebar.hrl b/src/rebar.hrl index 0b7f0b1..4e1ec00 100644 --- a/src/rebar.hrl +++ b/src/rebar.hrl @@ -22,6 +22,7 @@ -define(DEFAULT_TEST_DEPS_DIR, "test/lib"). -define(DEFAULT_RELEASE_DIR, "rel"). -define(DEFAULT_CONFIG_FILE, "rebar.config"). +-define(CONFIG_VERSION, "1.1.0"). -define(DEFAULT_CDN, "https://repo.hex.pm/"). -define(REMOTE_PACKAGE_DIR, "tarballs"). -define(REMOTE_REGISTRY_FILE, "registry.ets.gz"). diff --git a/src/rebar_app_utils.erl b/src/rebar_app_utils.erl index d3ef841..7028c32 100644 --- a/src/rebar_app_utils.erl +++ b/src/rebar_app_utils.erl @@ -119,17 +119,17 @@ parse_dep(Dep, Parent, DepsDir, State, Locks, Level) -> parse_dep(Parent, {Name, Vsn, {pkg, PkgName}}, DepsDir, IsLock, State) -> {PkgName1, PkgVsn} = {ec_cnv:to_binary(PkgName), ec_cnv:to_binary(Vsn)}, - dep_to_app(Parent, DepsDir, Name, PkgVsn, {pkg, PkgName1, PkgVsn}, IsLock, State); + dep_to_app(Parent, DepsDir, Name, PkgVsn, {pkg, PkgName1, PkgVsn, undefined}, IsLock, State); parse_dep(Parent, {Name, {pkg, PkgName}}, DepsDir, IsLock, State) -> %% Package dependency with different package name from app name - dep_to_app(Parent, DepsDir, Name, undefined, {pkg, ec_cnv:to_binary(PkgName), undefined}, IsLock, State); + dep_to_app(Parent, DepsDir, Name, undefined, {pkg, ec_cnv:to_binary(PkgName), undefined, undefined}, IsLock, State); parse_dep(Parent, {Name, Vsn}, DepsDir, IsLock, State) when is_list(Vsn); is_binary(Vsn) -> %% Versioned Package dependency {PkgName, PkgVsn} = {ec_cnv:to_binary(Name), ec_cnv:to_binary(Vsn)}, - dep_to_app(Parent, DepsDir, PkgName, PkgVsn, {pkg, PkgName, PkgVsn}, IsLock, State); + dep_to_app(Parent, DepsDir, PkgName, PkgVsn, {pkg, PkgName, PkgVsn, undefined}, IsLock, State); parse_dep(Parent, Name, DepsDir, IsLock, State) when is_atom(Name); is_binary(Name) -> %% Unversioned package dependency - dep_to_app(Parent, DepsDir, ec_cnv:to_binary(Name), undefined, {pkg, ec_cnv:to_binary(Name), undefined}, IsLock, State); + dep_to_app(Parent, DepsDir, ec_cnv:to_binary(Name), undefined, {pkg, ec_cnv:to_binary(Name), undefined, undefined}, IsLock, State); parse_dep(Parent, {Name, Source}, DepsDir, IsLock, State) when is_tuple(Source) -> dep_to_app(Parent, DepsDir, Name, [], Source, IsLock, State); parse_dep(Parent, {Name, _Vsn, Source}, DepsDir, IsLock, State) when is_tuple(Source) -> @@ -138,7 +138,9 @@ parse_dep(Parent, {Name, _Vsn, Source, Opts}, DepsDir, IsLock, State) when is_tu ?WARN("Dependency option list ~p in ~p is not supported and will be ignored", [Opts, Name]), dep_to_app(Parent, DepsDir, Name, [], Source, IsLock, State); parse_dep(Parent, {Name, {pkg, PkgName, Vsn}, Level}, DepsDir, IsLock, State) when is_integer(Level) -> - dep_to_app(Parent, DepsDir, Name, Vsn, {pkg, PkgName, Vsn}, IsLock, State); + dep_to_app(Parent, DepsDir, Name, Vsn, {pkg, PkgName, Vsn, undefined}, IsLock, State); +parse_dep(Parent, {Name, {pkg, PkgName, Vsn, Hash}, Level}, DepsDir, IsLock, State) when is_integer(Level) -> + dep_to_app(Parent, DepsDir, Name, Vsn, {pkg, PkgName, Vsn, Hash}, IsLock, State); parse_dep(Parent, {Name, Source, Level}, DepsDir, IsLock, State) when is_tuple(Source) , is_integer(Level) -> dep_to_app(Parent, DepsDir, Name, [], Source, IsLock, State); @@ -170,7 +172,7 @@ dep_to_app(Parent, DepsDir, Name, Vsn, Source, IsLock, State) -> AppInfo5 = rebar_app_info:profiles(AppInfo4, [default]), rebar_app_info:is_lock(AppInfo5, IsLock). -update_source(AppInfo, {pkg, PkgName, PkgVsn}, State) -> +update_source(AppInfo, {pkg, PkgName, PkgVsn, Hash}, State) -> {PkgName1, PkgVsn1} = case PkgVsn of undefined -> get_package(PkgName, "0", State); @@ -180,7 +182,7 @@ update_source(AppInfo, {pkg, PkgName, PkgVsn}, State) -> _ -> {PkgName, PkgVsn} end, - AppInfo1 = rebar_app_info:source(AppInfo, {pkg, PkgName1, PkgVsn1}), + AppInfo1 = rebar_app_info:source(AppInfo, {pkg, PkgName1, PkgVsn1, Hash}), Deps = rebar_packages:deps(PkgName1 ,PkgVsn1 ,State), diff --git a/src/rebar_config.erl b/src/rebar_config.erl index 8d7bcf4..031df8b 100644 --- a/src/rebar_config.erl +++ b/src/rebar_config.erl @@ -56,35 +56,73 @@ consult_lock_file(File) -> [] -> []; [Locks] when is_list(Locks) -> % beta lock file - Locks; + read_attrs(beta, Locks, []); [{Vsn, Locks}|Attrs] when is_list(Locks) -> % versioned lock file - %% Make sure the warning above is to be shown whenever a version - %% newer than the current one is being used, as we can't parse - %% all the contents of the lock file properly. - ?WARN("Rebar3 detected a lock file from a newer version. " - "It will be loaded in compatibility mode, but important " - "information may be missing or lost. It is recommended to " - "upgrade Rebar3.", []), + case Vsn of + ?CONFIG_VERSION -> + ok; + _ -> + %% Make sure the warning below is to be shown whenever a version + %% newer than the current one is being used, as we can't parse + %% all the contents of the lock file properly. + ?WARN("Rebar3 detected a lock file from a newer version. " + "It will be loaded in compatibility mode, but important " + "information may be missing or lost. It is recommended to " + "upgrade Rebar3.", []) + end, read_attrs(Vsn, Locks, Attrs) end. write_lock_file(LockFile, Locks) -> - NewLocks = write_attrs(Locks), + {NewLocks, Attrs} = write_attrs(Locks), %% Write locks in the beta format, at least until it's been long %% enough we can start modifying the lock format. - file:write_file(LockFile, io_lib:format("~p.~n", [NewLocks])). + case Attrs of + [] -> % write the old beta copy to avoid changes + file:write_file(LockFile, io_lib:format("~p.~n", [NewLocks])); + _ -> + file:write_file(LockFile, + io_lib:format("{~p,~p}.~n~p.~n", + [?CONFIG_VERSION, NewLocks, Attrs])) + end. -read_attrs(_Vsn, Locks, _Attrs) -> +read_attrs(_Vsn, Locks, Attrs) -> %% Beta copy does not know how to expand attributes, but %% is ready to support it. - Locks. + expand_locks(Locks, extract_pkg_hashes(Attrs)). + +extract_pkg_hashes(Attrs) -> + Props = case Attrs of + [First|_] -> First; + [] -> [] + end, + proplists:get_value(pkg_hash, Props, []). + +expand_locks([], _Hashes) -> + []; +expand_locks([{Name, {pkg,PkgName,Vsn}, Lvl} | Locks], Hashes) -> + Hash = proplists:get_value(Name, Hashes), + [{Name, {pkg,PkgName,Vsn,Hash}, Lvl} | expand_locks(Locks, Hashes)]; +expand_locks([Lock|Locks], Hashes) -> + [Lock | expand_locks(Locks, Hashes)]. write_attrs(Locks) -> %% No attribute known that needs to be taken out of the structure, %% just return terms as is. - Locks. - + {NewLocks, Hashes} = split_locks(Locks, [], []), + case Hashes of + [] -> {NewLocks, []}; + _ -> {NewLocks, [{pkg_hash, lists:sort(Hashes)}]} + end. +split_locks([], Locks, Hashes) -> + {lists:reverse(Locks), Hashes}; +split_locks([{Name, {pkg,PkgName,Vsn,undefined}, Lvl} | Locks], LAcc, HAcc) -> + split_locks(Locks, [{Name,{pkg,PkgName,Vsn},Lvl}|LAcc], HAcc); +split_locks([{Name, {pkg,PkgName,Vsn,Hash}, Lvl} | Locks], LAcc, HAcc) -> + split_locks(Locks, [{Name,{pkg,PkgName,Vsn},Lvl}|LAcc], [{Name, Hash}|HAcc]); +split_locks([Lock|Locks], LAcc, HAcc) -> + split_locks(Locks, [Lock|LAcc], HAcc). consult_file(File) -> Terms = consult_file_(File), diff --git a/src/rebar_packages.erl b/src/rebar_packages.erl index d4b8a14..8b4611b 100644 --- a/src/rebar_packages.erl +++ b/src/rebar_packages.erl @@ -122,7 +122,7 @@ package_dir(State) -> Error end. -registry_checksum({pkg, Name, Vsn}, State) -> +registry_checksum({pkg, Name, Vsn, _Hash}, State) -> try ?MODULE:verify_table(State), ets:lookup_element(?PACKAGE_TABLE, {Name, Vsn}, 3) diff --git a/src/rebar_pkg_resource.erl b/src/rebar_pkg_resource.erl index ec7e09d..257df21 100644 --- a/src/rebar_pkg_resource.erl +++ b/src/rebar_pkg_resource.erl @@ -19,7 +19,7 @@ lock(_AppDir, Source) -> Source. -needs_update(Dir, {pkg, _Name, Vsn}) -> +needs_update(Dir, {pkg, _Name, Vsn, _Hash}) -> [AppInfo] = rebar_app_discover:find_apps([Dir], all), case rebar_app_info:original_vsn(AppInfo) =:= ec_cnv:to_list(Vsn) of true -> @@ -28,7 +28,7 @@ needs_update(Dir, {pkg, _Name, Vsn}) -> true end. -download(TmpDir, Pkg={pkg, Name, Vsn}, State) -> +download(TmpDir, Pkg={pkg, Name, Vsn, _Hash}, State) -> CDN = rebar_state:get(State, rebar_packages_cdn, ?DEFAULT_CDN), {ok, PackageDir} = rebar_packages:package_dir(State), Package = binary_to_list(<>), @@ -40,7 +40,7 @@ download(TmpDir, Pkg={pkg, Name, Vsn}, State) -> {fetch_fail, Name, Vsn} end. -cached_download(TmpDir, CachePath, Pkg={pkg, Name, Vsn}, Url, ETag, State) -> +cached_download(TmpDir, CachePath, Pkg={pkg, Name, Vsn, _Hash}, Url, ETag, State) -> case request(Url, ETag) of {ok, cached} -> ?INFO("Version cached at ~s is up to date, reusing it", [CachePath]), diff --git a/src/rebar_prv_deps.erl b/src/rebar_prv_deps.erl index 9ff2bfa..c865276 100644 --- a/src/rebar_prv_deps.erl +++ b/src/rebar_prv_deps.erl @@ -97,7 +97,7 @@ display_dep(_State, {Name, _Vsn, Source}) when is_tuple(Source) -> display_dep(_State, {Name, _Vsn, Source, _Opts}) when is_tuple(Source) -> ?CONSOLE("~s* (~s source)", [ec_cnv:to_binary(Name), type(Source)]); %% Locked -display_dep(State, {Name, Source={pkg, _, Vsn}, Level}) when is_integer(Level) -> +display_dep(State, {Name, Source={pkg, _, Vsn, _}, Level}) when is_integer(Level) -> DepsDir = rebar_dir:deps_dir(State), AppDir = filename:join([DepsDir, ec_cnv:to_binary(Name)]), NeedsUpdate = case rebar_fetch:needs_update(AppDir, Source, State) of diff --git a/test/mock_pkg_resource.erl b/test/mock_pkg_resource.erl index a94fe2f..f837713 100644 --- a/test/mock_pkg_resource.erl +++ b/test/mock_pkg_resource.erl @@ -22,8 +22,9 @@ mock() -> mock([]). | {not_in_index, [{App, Vsn}]} | {pkgdeps, [{{App,Vsn}, [Dep]}]}, App :: string(), - Dep :: {App, string(), {pkg, App, Vsn}}, - Vsn :: string(). + Dep :: {App, string(), {pkg, App, Vsn, Hash}}, + Vsn :: string(), + Hash :: string() | undefined. mock(Opts) -> meck:new(?MOD, [no_link]), mock_lock(Opts), @@ -51,7 +52,7 @@ mock_update(Opts) -> ToUpdate = proplists:get_value(upgrade, Opts, []), meck:expect( ?MOD, needs_update, - fun(_Dir, {pkg, App, _Vsn}) -> + fun(_Dir, {pkg, App, _Vsn, _Hash}) -> lists:member(binary_to_list(App), ToUpdate) end). @@ -66,7 +67,7 @@ mock_vsn(_Opts) -> %% @doc For each app to download, create a dummy app on disk instead. %% The configuration for this one (passed in from `mock/1') includes: %% -%% - Specify a version with `{pkg, _, Vsn}' +%% - Specify a version with `{pkg, _, Vsn, _}' %% - Dependencies for each application must be passed of the form: %% `{pkgdeps, [{"app1", [{app2, ".*", {pkg, ...}}]}]}' -- basically %% the `pkgdeps' option takes a key/value list of terms to output directly @@ -76,7 +77,7 @@ mock_download(Opts) -> Config = proplists:get_value(config, Opts, []), meck:expect( ?MOD, download, - fun (Dir, {pkg, AppBin, Vsn}, _) -> + fun (Dir, {pkg, AppBin, Vsn, _}, _) -> App = binary_to_list(AppBin), filelib:ensure_dir(Dir), AppDeps = proplists:get_value({App,Vsn}, Deps, []), diff --git a/test/rebar_deps_SUITE.erl b/test/rebar_deps_SUITE.erl index c95854a..4ef9f79 100644 --- a/test/rebar_deps_SUITE.erl +++ b/test/rebar_deps_SUITE.erl @@ -405,5 +405,5 @@ in_warnings(git, Warns, NameRaw, VsnRaw) -> in_warnings(pkg, Warns, NameRaw, VsnRaw) -> Name = iolist_to_binary(NameRaw), Vsn = iolist_to_binary(VsnRaw), - 1 =< length([1 || {_, [AppName, {pkg, _, AppVsn}]} <- Warns, + 1 =< length([1 || {_, [AppName, {pkg, _, AppVsn, _}]} <- Warns, AppName =:= Name, AppVsn =:= Vsn]). diff --git a/test/rebar_install_deps_SUITE.erl b/test/rebar_install_deps_SUITE.erl index b8b70b3..9ff28c7 100644 --- a/test/rebar_install_deps_SUITE.erl +++ b/test/rebar_install_deps_SUITE.erl @@ -475,5 +475,5 @@ in_warnings(git, Warns, NameRaw, VsnRaw) -> in_warnings(pkg, Warns, NameRaw, VsnRaw) -> Name = iolist_to_binary(NameRaw), Vsn = iolist_to_binary(VsnRaw), - 1 =< length([1 || {_, [AppName, {pkg, _, AppVsn}]} <- Warns, + 1 =< length([1 || {_, [AppName, {pkg, _, AppVsn, _}]} <- Warns, AppName =:= Name, AppVsn =:= Vsn]). diff --git a/test/rebar_lock_SUITE.erl b/test/rebar_lock_SUITE.erl index 00875f7..f1ab3b5 100644 --- a/test/rebar_lock_SUITE.erl +++ b/test/rebar_lock_SUITE.erl @@ -7,7 +7,8 @@ -include_lib("common_test/include/ct.hrl"). -include_lib("eunit/include/eunit.hrl"). -all() -> [current_version, future_versions_no_attrs, future_versions_attrs]. +all() -> [current_version, + beta_version, future_versions_no_attrs, future_versions_attrs]. current_version(Config) -> %% Current version just dumps the locks as is on disk. @@ -15,9 +16,60 @@ current_version(Config) -> Locks = [{<<"app1">>, {git,"some_url", {ref,"some_ref"}}, 2}, {<<"app2">>, {git,"some_url", {ref,"some_ref"}}, 0}, {<<"app3">>, {hg,"some_url", {ref,"some_ref"}}, 1}, + {<<"pkg1">>,{pkg,<<"name">>,<<"0.1.6">>},3}, + {<<"pkg2">>,{pkg,<<"name1">>,<<"1.1.6">>},2}, + {<<"pkg3">>,{pkg,<<"name2">>,<<"3.0.6">>},1} + ], + ExpandedNull = [ + {<<"app1">>, {git,"some_url", {ref,"some_ref"}}, 2}, + {<<"app2">>, {git,"some_url", {ref,"some_ref"}}, 0}, + {<<"app3">>, {hg,"some_url", {ref,"some_ref"}}, 1}, + {<<"pkg1">>,{pkg,<<"name">>,<<"0.1.6">>,undefined},3}, + {<<"pkg2">>,{pkg,<<"name1">>,<<"1.1.6">>,undefined},2}, + {<<"pkg3">>,{pkg,<<"name2">>,<<"3.0.6">>,undefined},1} + ], + %% Simulate a beta lockfile + file:write_file(LockFile, io_lib:format("~p.~n", [Locks])), + %% No properties fetched from a beta lockfile, expand locks + %% to undefined + ?assertEqual(ExpandedNull, + rebar_config:consult_lock_file(LockFile)), + %% Adding hash data + Hashes = [{<<"pkg1">>, <<"tarballhash">>}, + {<<"pkg3">>, <<"otherhash">>}], + ExpandedLocks = [ + {<<"app1">>, {git,"some_url", {ref,"some_ref"}}, 2}, + {<<"app2">>, {git,"some_url", {ref,"some_ref"}}, 0}, + {<<"app3">>, {hg,"some_url", {ref,"some_ref"}}, 1}, + {<<"pkg1">>,{pkg,<<"name">>,<<"0.1.6">>,<<"tarballhash">>},3}, + {<<"pkg2">>,{pkg,<<"name1">>,<<"1.1.6">>,undefined},2}, + {<<"pkg3">>,{pkg,<<"name2">>,<<"3.0.6">>,<<"otherhash">>},1} + ], + file:write_file(LockFile, + io_lib:format("~p.~n~p.~n", + [{"1.1.0", Locks}, + [{pkg_hash, Hashes}]])), + ?assertEqual(ExpandedLocks, rebar_config:consult_lock_file(LockFile)), + %% Then check that we can reverse that + ok = rebar_config:write_lock_file(LockFile, ExpandedLocks), + ?assertEqual({ok, [{"1.1.0", Locks}, [{pkg_hash, Hashes}]]}, + file:consult(LockFile)). + +beta_version(Config) -> + %% Current version just dumps the locks as is on disk. + LockFile = filename:join(?config(priv_dir, Config), "current_version"), + Locks = [{<<"app1">>, {git,"some_url", {ref,"some_ref"}}, 2}, + {<<"app2">>, {git,"some_url", {ref,"some_ref"}}, 0}, + {<<"app3">>, {hg,"some_url", {ref,"some_ref"}}, 1}, {<<"pkg1">>,{pkg,<<"name">>,<<"0.1.6">>},3}], + ExpandedLocks = [ + {<<"app1">>, {git,"some_url", {ref,"some_ref"}}, 2}, + {<<"app2">>, {git,"some_url", {ref,"some_ref"}}, 0}, + {<<"app3">>, {hg,"some_url", {ref,"some_ref"}}, 1}, + {<<"pkg1">>,{pkg,<<"name">>,<<"0.1.6">>,undefined},3} + ], file:write_file(LockFile, io_lib:format("~p.~n", [Locks])), - ?assertEqual(Locks, rebar_config:consult_lock_file(LockFile)). + ?assertEqual(ExpandedLocks, rebar_config:consult_lock_file(LockFile)). future_versions_no_attrs(Config) -> %% Future versions will keep the same core attribute in there, but @@ -27,10 +79,14 @@ future_versions_no_attrs(Config) -> Locks = [{<<"app1">>, {git,"some_url", {ref,"some_ref"}}, 2}, {<<"app2">>, {git,"some_url", {ref,"some_ref"}}, 0}, {<<"app3">>, {hg,"some_url", {ref,"some_ref"}}, 1}, - {<<"pkg1">>,{pkg,<<"name">>,<<"0.1.6">>},3}], + {<<"pkg1">>, {pkg,<<"name">>,<<"0.1.6">>},3}], + ExpandedLocks = [{<<"app1">>, {git,"some_url", {ref,"some_ref"}}, 2}, + {<<"app2">>, {git,"some_url", {ref,"some_ref"}}, 0}, + {<<"app3">>, {hg,"some_url", {ref,"some_ref"}}, 1}, + {<<"pkg1">>, {pkg,<<"name">>,<<"0.1.6">>,undefined},3}], LockData = {"3.5.2", Locks}, file:write_file(LockFile, io_lib:format("~p.~n", [LockData])), - ?assertEqual(Locks, rebar_config:consult_lock_file(LockFile)). + ?assertEqual(ExpandedLocks, rebar_config:consult_lock_file(LockFile)). future_versions_attrs(Config) -> %% Future versions will keep the same core attribute in there, but @@ -41,6 +97,16 @@ future_versions_attrs(Config) -> {<<"app2">>, {git,"some_url", {ref,"some_ref"}}, 0}, {<<"app3">>, {hg,"some_url", {ref,"some_ref"}}, 1}, {<<"pkg1">>,{pkg,<<"name">>,<<"0.1.6">>},3}], + ExpandedLocks = [{<<"app1">>, {git,"some_url", {ref,"some_ref"}}, 2}, + {<<"app2">>, {git,"some_url", {ref,"some_ref"}}, 0}, + {<<"app3">>, {hg,"some_url", {ref,"some_ref"}}, 1}, + {<<"pkg1">>,{pkg,<<"name">>,<<"0.1.6">>, <<"tarballhash">>},3}], + Hashes = [{<<"pkg1">>, <<"tarballhash">>}], LockData = {"3.5.2", Locks}, - file:write_file(LockFile, io_lib:format("~p.~na.~n{b,c}.~n[d,e,f].~n", [LockData])), - ?assertEqual(Locks, rebar_config:consult_lock_file(LockFile)). + file:write_file(LockFile, + io_lib:format("~p.~n~p.~ngarbage.~n", + [LockData, + [{a, x}, + {pkg_hash, Hashes}, + {b, y}]])), + ?assertEqual(ExpandedLocks, rebar_config:consult_lock_file(LockFile)). diff --git a/test/rebar_pkg_SUITE.erl b/test/rebar_pkg_SUITE.erl index 6a75f32..8003b10 100644 --- a/test/rebar_pkg_SUITE.erl +++ b/test/rebar_pkg_SUITE.erl @@ -103,7 +103,7 @@ good_uncached(Config) -> {Pkg,Vsn} = ?config(pkg, Config), State = ?config(state, Config), ?assertEqual({ok, true}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), Cache = ?config(cache_dir, Config), ?assert(filelib:is_regular(filename:join(Cache, <>))). @@ -116,7 +116,7 @@ good_cached(Config) -> ?assert(filelib:is_regular(CachedFile)), {ok, Content} = file:read_file(CachedFile), ?assertEqual({ok, true}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), {ok, Content} = file:read_file(CachedFile). badindexchk(Config) -> @@ -124,7 +124,7 @@ badindexchk(Config) -> {Pkg,Vsn} = ?config(pkg, Config), State = ?config(state, Config), ?assertMatch({bad_registry_checksum, _Path}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), %% The cached file is there for forensic purposes Cache = ?config(cache_dir, Config), ?assert(filelib:is_regular(filename:join(Cache, <>))). @@ -134,7 +134,7 @@ badpkg(Config) -> {Pkg,Vsn} = ?config(pkg, Config), State = ?config(state, Config), ?assertMatch({bad_download, _Path}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), %% The cached file is there for forensic purposes Cache = ?config(cache_dir, Config), ?assert(filelib:is_regular(filename:join(Cache, <>))). @@ -148,7 +148,7 @@ bad_to_good(Config) -> ?assert(filelib:is_regular(CachedFile)), {ok, Contents} = file:read_file(CachedFile), ?assertEqual({ok, true}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), %% Cache has refreshed ?assert({ok, Contents} =/= file:read_file(CachedFile)). @@ -161,7 +161,7 @@ good_disconnect(Config) -> ?assert(filelib:is_regular(CachedFile)), {ok, Content} = file:read_file(CachedFile), ?assertEqual({ok, true}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), {ok, Content} = file:read_file(CachedFile). bad_disconnect(Config) -> @@ -169,7 +169,7 @@ bad_disconnect(Config) -> {Pkg,Vsn} = ?config(pkg, Config), State = ?config(state, Config), ?assertEqual({fetch_fail, Pkg, Vsn}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn}, State)). + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)). pkgs_provider(Config) -> Config1 = rebar_test_utils:init_rebar_state(Config), diff --git a/test/rebar_test_utils.erl b/test/rebar_test_utils.erl index 23b0178..8c2d36d 100644 --- a/test/rebar_test_utils.erl +++ b/test/rebar_test_utils.erl @@ -149,21 +149,21 @@ expand_deps(git, [{Name, Vsn, Deps} | Rest]) -> Dep = {Name, Vsn, {git, "https://example.org/user/"++Name++".git", {tag, Vsn}}}, [{Dep, expand_deps(git, Deps)} | expand_deps(git, Rest)]; expand_deps(pkg, [{Name, Deps} | Rest]) -> - Dep = {pkg, Name, "0.0.0"}, + Dep = {pkg, Name, "0.0.0", undefined}, [{Dep, expand_deps(pkg, Deps)} | expand_deps(pkg, Rest)]; expand_deps(pkg, [{Name, Vsn, Deps} | Rest]) -> - Dep = {pkg, Name, Vsn}, + Dep = {pkg, Name, Vsn, undefined}, [{Dep, expand_deps(pkg, Deps)} | expand_deps(pkg, Rest)]; expand_deps(mixed, [{Name, Deps} | Rest]) -> Dep = if hd(Name) >= $a, hd(Name) =< $z -> - {pkg, string:to_upper(Name), "0.0.0"} + {pkg, string:to_upper(Name), "0.0.0", undefined} ; hd(Name) >= $A, hd(Name) =< $Z -> {Name, ".*", {git, "https://example.org/user/"++Name++".git", "master"}} end, [{Dep, expand_deps(mixed, Deps)} | expand_deps(mixed, Rest)]; expand_deps(mixed, [{Name, Vsn, Deps} | Rest]) -> Dep = if hd(Name) >= $a, hd(Name) =< $z -> - {pkg, string:to_upper(Name), Vsn} + {pkg, string:to_upper(Name), Vsn, undefined} ; hd(Name) >= $A, hd(Name) =< $Z -> {Name, Vsn, {git, "https://example.org/user/"++Name++".git", {tag, Vsn}}} end, @@ -177,7 +177,7 @@ expand_deps(mixed, [{Name, Vsn, Deps} | Rest]) -> flat_deps(Deps) -> flat_deps(Deps, [], []). flat_deps([], Src, Pkg) -> {Src, Pkg}; -flat_deps([{{pkg, Name, Vsn}, PkgDeps} | Rest], Src, Pkg) -> +flat_deps([{{pkg, Name, Vsn, undefined}, PkgDeps} | Rest], Src, Pkg) -> Current = {{iolist_to_binary(Name), iolist_to_binary(Vsn)}, top_level_deps(PkgDeps)}, {[], FlatPkgDeps} = flat_deps(PkgDeps), @@ -195,7 +195,7 @@ vsn_from_ref({git, _, {_, Vsn}}) -> Vsn; vsn_from_ref({git, _, Vsn}) -> Vsn. top_level_deps([]) -> []; -top_level_deps([{{pkg, Name, Vsn}, _} | Deps]) -> +top_level_deps([{{pkg, Name, Vsn, undefined}, _} | Deps]) -> [{list_to_atom(Name), Vsn} | top_level_deps(Deps)]; top_level_deps([{{Name, Vsn, Ref}, _} | Deps]) -> [{list_to_atom(Name), Vsn, Ref} | top_level_deps(Deps)]. @@ -306,7 +306,7 @@ check_results(AppDir, Expected, ProfileRun) -> case lists:keyfind(iolist_to_binary(Name), 1, Locks) of false -> error({lock_not_found, Name}); - {_LockName, {pkg, _, LockVsn}, _} -> + {_LockName, {pkg, _, LockVsn, _}, _} -> ?assertEqual(iolist_to_binary(Vsn), iolist_to_binary(LockVsn)); {_LockName, {_, _, {ref, LockVsn}}, _} -> @@ -318,7 +318,7 @@ check_results(AppDir, Expected, ProfileRun) -> case lists:keyfind(iolist_to_binary(Name), 1, Locks) of false -> error({lock_not_found, Name}); - {_LockName, {pkg, _, LockVsn}, _} -> + {_LockName, {pkg, _, LockVsn, _}, _} -> ?assertEqual(iolist_to_binary(Vsn), iolist_to_binary(LockVsn)); {_LockName, {_, _, {ref, LockVsn}}, _} -> @@ -329,7 +329,7 @@ check_results(AppDir, Expected, ProfileRun) -> case lists:keyfind(iolist_to_binary(Name), 1, Locks) of false -> error({lock_not_found, Name}); - {_LockName, {pkg, _, LockVsn}, _} -> + {_LockName, {pkg, _, LockVsn, _}, _} -> error({pkg_lock, {Name, LockVsn}}); {_LockName, {_, _, {ref, LockVsn}}, _} -> ?assertEqual(iolist_to_binary(Vsn), -- cgit v1.1 From be79259e324e66ac2f948aed186474cb06a2ea85 Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Tue, 24 May 2016 20:43:04 -0400 Subject: Test support for OTP-19 --- rebar.config | 1 + test/rebar_test_utils.erl | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/rebar.config b/rebar.config index ea5af27..7326b46 100644 --- a/rebar.config +++ b/rebar.config @@ -20,6 +20,7 @@ {"rebar/priv/templates/*", "_build/default/lib/"}]}. {erl_opts, [{platform_define, "^[0-9]+", namespaced_types}, + {platform_define, "^(19|2)", rand_only}, no_debug_info, warnings_as_errors]}. diff --git a/test/rebar_test_utils.erl b/test/rebar_test_utils.erl index 8c2d36d..7e62109 100644 --- a/test/rebar_test_utils.erl +++ b/test/rebar_test_utils.erl @@ -7,6 +7,13 @@ create_config/2, create_config/3, package_app/3]). -export([create_random_name/1, create_random_vsn/0, write_src_file/2]). +%% Pick the right random module +-ifdef(rand_only). +-define(random, rand). +-else. +-define(random, random). +-endif. + %%%%%%%%%%%%%% %%% Public %%% %%%%%%%%%%%%%% @@ -126,20 +133,24 @@ create_config(_AppDir, ConfFilename, Contents) -> %% @doc Util to create a random variation of a given name. create_random_name(Name) -> random_seed(), - Name ++ erlang:integer_to_list(random:uniform(1000000)). + Name ++ erlang:integer_to_list(?random:uniform(1000000)). %% @doc Util to create a random variation of a given version. create_random_vsn() -> random_seed(), - lists:flatten([erlang:integer_to_list(random:uniform(100)), - ".", erlang:integer_to_list(random:uniform(100)), - ".", erlang:integer_to_list(random:uniform(100))]). + lists:flatten([erlang:integer_to_list(?random:uniform(100)), + ".", erlang:integer_to_list(?random:uniform(100)), + ".", erlang:integer_to_list(?random:uniform(100))]). +-ifdef(rand_only). +random_seed() -> + %% the rand module self-seeds + ok. +-else. random_seed() -> <> = crypto:rand_bytes(12), random:seed({A,B,C}). - - +-endif. expand_deps(_, []) -> []; expand_deps(git, [{Name, Deps} | Rest]) -> -- cgit v1.1 From 9a7ede0196c78224c15d1e9c8d13bed84743dacf Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Tue, 24 May 2016 20:43:38 -0400 Subject: Fetch hashes from index prior to fetching --- src/rebar_app_utils.erl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/rebar_app_utils.erl b/src/rebar_app_utils.erl index 7028c32..f3b2962 100644 --- a/src/rebar_app_utils.erl +++ b/src/rebar_app_utils.erl @@ -182,7 +182,14 @@ update_source(AppInfo, {pkg, PkgName, PkgVsn, Hash}, State) -> _ -> {PkgName, PkgVsn} end, - AppInfo1 = rebar_app_info:source(AppInfo, {pkg, PkgName1, PkgVsn1, Hash}), + %% store the expected hash for the dependency + Hash1 = case Hash of + undefined -> % unknown, define the hash since we know the dep + rebar_packages:registry_checksum({pkg, PkgName1, PkgVsn1, Hash}, State); + _ -> % keep as is + Hash + end, + AppInfo1 = rebar_app_info:source(AppInfo, {pkg, PkgName1, PkgVsn1, Hash1}), Deps = rebar_packages:deps(PkgName1 ,PkgVsn1 ,State), -- cgit v1.1 From 351b757a0eb03e2d2e8714d0f5f447531446aa68 Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Tue, 24 May 2016 21:12:49 -0400 Subject: Validate checksums expected vs obtained --- src/rebar_fetch.erl | 6 ++++++ src/rebar_pkg_resource.erl | 17 ++++++++++------- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/rebar_fetch.erl b/src/rebar_fetch.erl index b80c125..47bfe1d 100644 --- a/src/rebar_fetch.erl +++ b/src/rebar_fetch.erl @@ -67,6 +67,12 @@ needs_update(AppDir, Source, State) -> format_error({bad_download, CachePath}) -> io_lib:format("Download of package does not match md5sum from server: ~s", [CachePath]); +format_error({unexpected_hash, CachePath, Expected, Found}) -> + io_lib:format("The checksum for package at ~s (~s) does not match the " + "checksum previously locked (~s). Either unlock or " + "upgrade the package, or make sure you fetched it from " + "the same index from which it was initially fetched.", + [CachePath, Found, Expected]); format_error({failed_extract, CachePath}) -> io_lib:format("Failed to extract package: ~s", [CachePath]); format_error({bad_etag, Source}) -> diff --git a/src/rebar_pkg_resource.erl b/src/rebar_pkg_resource.erl index 257df21..5817817 100644 --- a/src/rebar_pkg_resource.erl +++ b/src/rebar_pkg_resource.erl @@ -58,17 +58,20 @@ cached_download(TmpDir, CachePath, Pkg={pkg, Name, Vsn, _Hash}, Url, ETag, State serve_from_cache(TmpDir, CachePath, Pkg, State) -> {Files, Contents, Version, Meta} = extract(TmpDir, CachePath), case checksums(Pkg, Files, Contents, Version, Meta, State) of - {Chk, Chk, Chk} -> + {Chk, Chk, Chk, Chk} -> ok = erl_tar:extract({binary, Contents}, [{cwd, TmpDir}, compressed]), {ok, true}; - {_Bin, Chk, Chk} -> + {_Hash, Chk, Chk, Chk} -> + ?DEBUG("Expected hash ~p does not match checksums ~p", [_Hash, Chk]), + {unexpected_hash, CachePath, _Hash, Chk}; + {Chk, _Bin, Chk, Chk} -> ?DEBUG("Checksums: registry: ~p, pkg: ~p", [Chk, _Bin]), {failed_extract, CachePath}; - {Chk, _Reg, Chk} -> + {Chk, Chk, _Reg, Chk} -> ?DEBUG("Checksums: registry: ~p, pkg: ~p", [_Reg, Chk]), {bad_registry_checksum, CachePath}; - {_Bin, _Reg, _Tar} -> - ?DEBUG("Checksums: registry: ~p, pkg: ~p, meta: ~p", [_Reg, _Bin, _Tar]), + {_Hash, _Bin, _Reg, _Tar} -> + ?DEBUG("Checksums: expected: ~p, registry: ~p, pkg: ~p, meta: ~p", [_Hash, _Reg, _Bin, _Tar]), {bad_checksum, CachePath} end. @@ -92,13 +95,13 @@ extract(TmpDir, CachePath) -> {"metadata.config", Meta} = lists:keyfind("metadata.config", 1, Files), {Files, Contents, Version, Meta}. -checksums(Pkg, Files, Contents, Version, Meta, State) -> +checksums(Pkg={pkg, _Name, _Vsn, Hash}, Files, Contents, Version, Meta, State) -> Blob = <>, <> = crypto:hash(sha256, Blob), BinChecksum = list_to_binary(string:to_upper(lists:flatten(io_lib:format("~64.16.0b", [X])))), RegistryChecksum = rebar_packages:registry_checksum(Pkg, State), {"CHECKSUM", TarChecksum} = lists:keyfind("CHECKSUM", 1, Files), - {BinChecksum, RegistryChecksum, TarChecksum}. + {Hash, BinChecksum, RegistryChecksum, TarChecksum}. make_vsn(_) -> {error, "Replacing version of type pkg not supported."}. -- cgit v1.1 From 24c43040c9280220efd01096605980f8b28b5f33 Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Tue, 24 May 2016 21:17:59 -0400 Subject: Rebar3 with hash-locked deps --- rebar.lock | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/rebar.lock b/rebar.lock index 40463ac..38e22e5 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,10 +1,32 @@ -[{<<"bbmustache">>,{pkg,<<"bbmustache">>,<<"1.0.4">>},0}, - {<<"certifi">>,{pkg,<<"certifi">>,<<"0.4.0">>},0}, - {<<"cf">>,{pkg,<<"cf">>,<<"0.2.1">>},0}, - {<<"cth_readable">>,{pkg,<<"cth_readable">>,<<"1.2.2">>},0}, - {<<"erlware_commons">>,{pkg,<<"erlware_commons">>,<<"0.21.0">>},0}, - {<<"eunit_formatters">>,{pkg,<<"eunit_formatters">>,<<"0.3.1">>},0}, - {<<"getopt">>,{pkg,<<"getopt">>,<<"0.8.2">>},0}, - {<<"providers">>,{pkg,<<"providers">>,<<"1.6.0">>},0}, - {<<"relx">>,{pkg,<<"relx">>,<<"3.19.0">>},0}, - {<<"ssl_verify_hostname">>,{pkg,<<"ssl_verify_hostname">>,<<"1.0.5">>},0}]. +{"1.1.0",[{<<"bbmustache">>,{pkg,<<"bbmustache">>,<<"1.0.4">>},0}, + {<<"certifi">>,{pkg,<<"certifi">>,<<"0.4.0">>},0}, + {<<"cf">>,{pkg,<<"cf">>,<<"0.2.1">>},0}, + {<<"cth_readable">>,{pkg,<<"cth_readable">>,<<"1.2.2">>},0}, + {<<"erlware_commons">>,{pkg,<<"erlware_commons">>,<<"0.21.0">>},0}, + {<<"eunit_formatters">>,{pkg,<<"eunit_formatters">>,<<"0.3.1">>},0}, + {<<"getopt">>,{pkg,<<"getopt">>,<<"0.8.2">>},0}, + {<<"providers">>,{pkg,<<"providers">>,<<"1.6.0">>},0}, + {<<"relx">>,{pkg,<<"relx">>,<<"3.19.0">>},0}, + {<<"ssl_verify_hostname">>, + {pkg,<<"ssl_verify_hostname">>,<<"1.0.5">>}, + 0}]}. +[{pkg_hash,[{<<"bbmustache">>, + <<"7BA94F971C5AFD7B6617918A4BB74705E36CAB36EB84B19B6A1B7EE06427AA38">>}, + {<<"certifi">>, + <<"A7966EFB868B179023618D29A407548F70C52466BF1849B9E8EBD0E34B7EA11F">>}, + {<<"cf">>, + <<"69D0B1349FD4D7D4DC55B7F407D29D7A840BF9A1EF5AF529F1EBE0CE153FC2AB">>}, + {<<"cth_readable">>, + <<"983913A8E8572310B7EAF5F2631148B7D70B3C090D2120DCFE777A93AA4165FB">>}, + {<<"erlware_commons">>, + <<"A04433071AD7D112EDEFC75AC77719DD3E6753E697AC09428FC83D7564B80B15">>}, + {<<"eunit_formatters">>, + <<"7A6FC351EB5B873E2356B8852EB751E20C13A72FBCA03393CF682B8483509573">>}, + {<<"getopt">>, + <<"B17556DB683000BA50370B16C0619DF1337E7AF7ECBF7D64FBF8D1D6BCE3109B">>}, + {<<"providers">>, + <<"DB0E2F9043AE60C0155205FCD238D68516331D0E5146155E33D1E79DC452964A">>}, + {<<"relx">>, + <<"286DD5244B4786F56AAC75D5C8E2D1FB4CFD306810D4EC8548F3AE1B3AADB8F7">>}, + {<<"ssl_verify_hostname">>, + <<"2E73E068CD6393526F9FA6D399353D7C9477D6886BA005F323B592D389FB47BE">>}]}]. -- cgit v1.1 From 00e06c1999e0e6e1dafdc4d7c835c89b473b65aa Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Tue, 24 May 2016 21:44:20 -0400 Subject: Make tests pass Tests have broken as locks were expanded and auto-filled newer versions of lockfiles. This fixes them back. --- test/rebar_pkg_SUITE.erl | 14 +++++++------- test/rebar_pkg_alias_SUITE.erl | 6 +++--- test/rebar_upgrade_SUITE.erl | 9 ++++++++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/test/rebar_pkg_SUITE.erl b/test/rebar_pkg_SUITE.erl index 8003b10..7fdf55e 100644 --- a/test/rebar_pkg_SUITE.erl +++ b/test/rebar_pkg_SUITE.erl @@ -103,7 +103,7 @@ good_uncached(Config) -> {Pkg,Vsn} = ?config(pkg, Config), State = ?config(state, Config), ?assertEqual({ok, true}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, ?good_checksum}, State)), Cache = ?config(cache_dir, Config), ?assert(filelib:is_regular(filename:join(Cache, <>))). @@ -116,7 +116,7 @@ good_cached(Config) -> ?assert(filelib:is_regular(CachedFile)), {ok, Content} = file:read_file(CachedFile), ?assertEqual({ok, true}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, ?good_checksum}, State)), {ok, Content} = file:read_file(CachedFile). badindexchk(Config) -> @@ -124,7 +124,7 @@ badindexchk(Config) -> {Pkg,Vsn} = ?config(pkg, Config), State = ?config(state, Config), ?assertMatch({bad_registry_checksum, _Path}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, ?good_checksum}, State)), %% The cached file is there for forensic purposes Cache = ?config(cache_dir, Config), ?assert(filelib:is_regular(filename:join(Cache, <>))). @@ -134,7 +134,7 @@ badpkg(Config) -> {Pkg,Vsn} = ?config(pkg, Config), State = ?config(state, Config), ?assertMatch({bad_download, _Path}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, ?good_checksum}, State)), %% The cached file is there for forensic purposes Cache = ?config(cache_dir, Config), ?assert(filelib:is_regular(filename:join(Cache, <>))). @@ -148,7 +148,7 @@ bad_to_good(Config) -> ?assert(filelib:is_regular(CachedFile)), {ok, Contents} = file:read_file(CachedFile), ?assertEqual({ok, true}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, ?good_checksum}, State)), %% Cache has refreshed ?assert({ok, Contents} =/= file:read_file(CachedFile)). @@ -161,7 +161,7 @@ good_disconnect(Config) -> ?assert(filelib:is_regular(CachedFile)), {ok, Content} = file:read_file(CachedFile), ?assertEqual({ok, true}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)), + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, ?good_checksum}, State)), {ok, Content} = file:read_file(CachedFile). bad_disconnect(Config) -> @@ -169,7 +169,7 @@ bad_disconnect(Config) -> {Pkg,Vsn} = ?config(pkg, Config), State = ?config(state, Config), ?assertEqual({fetch_fail, Pkg, Vsn}, - rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, undefined}, State)). + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, ?good_checksum}, State)). pkgs_provider(Config) -> Config1 = rebar_test_utils:init_rebar_state(Config), diff --git a/test/rebar_pkg_alias_SUITE.erl b/test/rebar_pkg_alias_SUITE.erl index fef2310..8915357 100644 --- a/test/rebar_pkg_alias_SUITE.erl +++ b/test/rebar_pkg_alias_SUITE.erl @@ -55,7 +55,7 @@ diff_alias(Config) -> Config, RebarConfig, ["lock"], {ok, [{lock, "fakelib"},{dep, "fakelib"}]} ), - {ok, [LockData]} = file:consult(Lockfile), + {ok, [{_Vsn, LockData}|_]} = file:consult(Lockfile), ?assert(lists:any(fun({<<"fakelib">>,{pkg,<<"goodpkg">>,_},_}) -> true ; (_) -> false end, LockData)), %% An second run yields the same @@ -63,13 +63,13 @@ diff_alias(Config) -> Config, RebarConfig, ["lock"], {ok, [{lock, "fakelib"},{dep, "fakelib"}]} ), - {ok, [LockData]} = file:consult(Lockfile), + {ok, [{_Vsn, LockData}|_]} = file:consult(Lockfile), %% So does an upgrade rebar_test_utils:run_and_check( Config, RebarConfig, ["upgrade"], {ok, [{lock, "fakelib"},{dep, "fakelib"}]} ), - {ok, [LockData]} = file:consult(Lockfile). + {ok, [{_Vsn, LockData}|_]} = file:consult(Lockfile). diff_alias_vsn(Config) -> diff_alias(Config). diff --git a/test/rebar_upgrade_SUITE.erl b/test/rebar_upgrade_SUITE.erl index cfe1d8a..22fb14d 100644 --- a/test/rebar_upgrade_SUITE.erl +++ b/test/rebar_upgrade_SUITE.erl @@ -610,7 +610,14 @@ novsn_pkg(Config) -> rewrite_locks({ok, Expectations}, Config) -> AppDir = ?config(apps, Config), LockFile = filename:join([AppDir, "rebar.lock"]), - {ok, [Locks]} = file:consult(LockFile), + Locks = case ?config(deps_type, Config) of + git -> + {ok, [LockData]} = file:consult(LockFile), + LockData; + pkg -> + {ok, [{_Vsn, LockData}|_]} = file:consult(LockFile), + LockData + end, ExpLocks = [{list_to_binary(Name), Vsn} || {lock, Name, Vsn} <- Expectations], NewLocks = lists:foldl( -- cgit v1.1 From c15c31f35c46e8184dd76bc8463eb6f4ffd65978 Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Wed, 25 May 2016 10:58:50 -0400 Subject: Testing expected hash behaviour and errors in pkgs - also making sure unlocking works fine --- test/rebar_pkg_SUITE.erl | 38 +++++++++++++++++++++++++ test/rebar_test_utils.erl | 10 ++++--- test/rebar_unlock_SUITE.erl | 43 +++++++++++++++++++++++++++-- test/rebar_unlock_SUITE_data/pkg.rebar.lock | 32 +++++++++++++++++++++ 4 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 test/rebar_unlock_SUITE_data/pkg.rebar.lock diff --git a/test/rebar_pkg_SUITE.erl b/test/rebar_pkg_SUITE.erl index 7fdf55e..30cc0a8 100644 --- a/test/rebar_pkg_SUITE.erl +++ b/test/rebar_pkg_SUITE.erl @@ -11,6 +11,7 @@ -define(good_checksum, <<"1C6CE379D191FBAB41B7905075E0BF87CBBE23C77CECE775C5A0B786B2244C35">>). all() -> [good_uncached, good_cached, badindexchk, badpkg, + badhash_nocache, badhash_cache, bad_to_good, good_disconnect, bad_disconnect, pkgs_provider, find_highest_matching]. @@ -58,6 +59,19 @@ init_per_testcase(badpkg=Name, Config0) -> {pkg, {<<"badpkg">>, <<"1.0.0">>}} | Config0], mock_config(Name, Config); +init_per_testcase(badhash_nocache=Name, Config0) -> + Config = [{good_cache, false}, + {pkg, {<<"goodpkg">>, <<"1.0.0">>}} + | Config0], + mock_config(Name, Config); +init_per_testcase(badhash_cache=Name, Config0) -> + Pkg = {<<"goodpkg">>, <<"1.0.0">>}, + Config1 = [{good_cache, true}, + {pkg, Pkg} + | Config0], + Config = mock_config(Name, Config1), + copy_to_cache(Pkg, Config), + Config; init_per_testcase(bad_to_good=Name, Config0) -> Config1 = [{good_cache, false}, {pkg, {<<"goodpkg">>, <<"1.0.0">>}} @@ -139,6 +153,30 @@ badpkg(Config) -> Cache = ?config(cache_dir, Config), ?assert(filelib:is_regular(filename:join(Cache, <>))). +badhash_nocache(Config) -> + Tmp = ?config(tmp_dir, Config), + {Pkg,Vsn} = ?config(pkg, Config), + State = ?config(state, Config), + ?assertMatch({unexpected_hash, _Path, ?bad_checksum, ?good_checksum}, + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, ?bad_checksum}, State)), + %% The cached file is there for forensic purposes + Cache = ?config(cache_dir, Config), + ?assert(filelib:is_regular(filename:join(Cache, <>))). + +badhash_cache(Config) -> + Tmp = ?config(tmp_dir, Config), + {Pkg,Vsn} = ?config(pkg, Config), + Cache = ?config(cache_dir, Config), + State = ?config(state, Config), + CachedFile = filename:join(Cache, <>), + ?assert(filelib:is_regular(CachedFile)), + {ok, Content} = file:read_file(CachedFile), + ?assertMatch({unexpected_hash, _Path, ?bad_checksum, ?good_checksum}, + rebar_pkg_resource:download(Tmp, {pkg, Pkg, Vsn, ?bad_checksum}, State)), + %% The cached file is there still, unchanged. + ?assert(filelib:is_regular(CachedFile)), + ?assertEqual({ok, Content}, file:read_file(CachedFile)). + bad_to_good(Config) -> Tmp = ?config(tmp_dir, Config), {Pkg,Vsn} = ?config(pkg, Config), diff --git a/test/rebar_test_utils.erl b/test/rebar_test_utils.erl index 7e62109..8c177c9 100644 --- a/test/rebar_test_utils.erl +++ b/test/rebar_test_utils.erl @@ -317,9 +317,10 @@ check_results(AppDir, Expected, ProfileRun) -> case lists:keyfind(iolist_to_binary(Name), 1, Locks) of false -> error({lock_not_found, Name}); - {_LockName, {pkg, _, LockVsn, _}, _} -> + {_LockName, {pkg, _, LockVsn, Hash}, _} -> ?assertEqual(iolist_to_binary(Vsn), - iolist_to_binary(LockVsn)); + iolist_to_binary(LockVsn)), + ?assertNotEqual(undefined, Hash); {_LockName, {_, _, {ref, LockVsn}}, _} -> ?assertEqual(iolist_to_binary(Vsn), iolist_to_binary(LockVsn)) @@ -329,9 +330,10 @@ check_results(AppDir, Expected, ProfileRun) -> case lists:keyfind(iolist_to_binary(Name), 1, Locks) of false -> error({lock_not_found, Name}); - {_LockName, {pkg, _, LockVsn, _}, _} -> + {_LockName, {pkg, _, LockVsn, Hash}, _} -> ?assertEqual(iolist_to_binary(Vsn), - iolist_to_binary(LockVsn)); + iolist_to_binary(LockVsn)), + ?assertNotEqual(undefined, Hash); {_LockName, {_, _, {ref, LockVsn}}, _} -> error({source_lock, {Name, LockVsn}}) end diff --git a/test/rebar_unlock_SUITE.erl b/test/rebar_unlock_SUITE.erl index 31dca72..8dbdb3a 100644 --- a/test/rebar_unlock_SUITE.erl +++ b/test/rebar_unlock_SUITE.erl @@ -3,8 +3,14 @@ -include_lib("eunit/include/eunit.hrl"). -compile(export_all). -all() -> [unlock, unlock_all]. +all() -> [pkgunlock, unlock, unlock_all]. +init_per_testcase(pkgunlock, Config0) -> + Config = rebar_test_utils:init_rebar_state(Config0, "pkgunlock"), + Lockfile = filename:join(?config(apps, Config), "rebar.lock"), + ec_file:copy(filename:join(?config(data_dir, Config), "pkg.rebar.lock"), + Lockfile), + [{lockfile, Lockfile} | Config]; init_per_testcase(Case, Config0) -> Config = rebar_test_utils:init_rebar_state(Config0, atom_to_list(Case)), Lockfile = filename:join(?config(apps, Config), "rebar.lock"), @@ -15,6 +21,23 @@ init_per_testcase(Case, Config0) -> end_per_testcase(_, Config) -> Config. +pkgunlock(Config) -> + Locks = read_locks(Config), + Hashes = read_hashes(Config), + rebar_test_utils:run_and_check(Config, [], ["unlock", "fakeapp"], {ok, []}), + Locks = read_locks(Config), + Hashes = read_hashes(Config), + rebar_test_utils:run_and_check(Config, [], ["unlock", "bbmustache"], {ok, []}), + ?assertEqual(Locks -- ["bbmustache"], read_locks(Config)), + ?assertEqual(Hashes -- ["bbmustache"], read_hashes(Config)), + rebar_test_utils:run_and_check(Config, [], ["unlock", "cf,certifi"], {ok, []}), + ?assertEqual(Locks -- ["bbmustache","cf","certifi"], read_locks(Config)), + ?assertEqual(Hashes -- ["bbmustache","cf","certifi"], read_hashes(Config)), + rebar_test_utils:run_and_check(Config, [], ["unlock", string:join(Locks,",")], {ok, []}), + ?assertEqual({error, enoent}, read_locks(Config)), + ?assertEqual({error, enoent}, read_hashes(Config)), + ok. + unlock(Config) -> Locks = read_locks(Config), rebar_test_utils:run_and_check(Config, [], ["unlock", "fakeapp"], {ok, []}), @@ -35,6 +58,20 @@ unlock_all(Config) -> read_locks(Config) -> case file:consult(?config(lockfile, Config)) of - {ok, [Locks]} -> [binary_to_list(element(1,Lock)) || Lock <- Locks]; - Other -> Other + {ok, _} -> + Locks = rebar_config:consult_lock_file(?config(lockfile, Config)), + [binary_to_list(element(1,Lock)) || Lock <- Locks]; + Other -> + Other + end. + +read_hashes(Config) -> + case file:consult(?config(lockfile, Config)) of + {ok, [{_Vsn, _Locks},Props|_]} -> + Hashes = proplists:get_value(pkg_hash, Props, []), + [binary_to_list(element(1,Hash)) || Hash <- Hashes]; + {ok, [{_Vsn, _Locks}]} -> + []; + Other -> + Other end. diff --git a/test/rebar_unlock_SUITE_data/pkg.rebar.lock b/test/rebar_unlock_SUITE_data/pkg.rebar.lock new file mode 100644 index 0000000..38e22e5 --- /dev/null +++ b/test/rebar_unlock_SUITE_data/pkg.rebar.lock @@ -0,0 +1,32 @@ +{"1.1.0",[{<<"bbmustache">>,{pkg,<<"bbmustache">>,<<"1.0.4">>},0}, + {<<"certifi">>,{pkg,<<"certifi">>,<<"0.4.0">>},0}, + {<<"cf">>,{pkg,<<"cf">>,<<"0.2.1">>},0}, + {<<"cth_readable">>,{pkg,<<"cth_readable">>,<<"1.2.2">>},0}, + {<<"erlware_commons">>,{pkg,<<"erlware_commons">>,<<"0.21.0">>},0}, + {<<"eunit_formatters">>,{pkg,<<"eunit_formatters">>,<<"0.3.1">>},0}, + {<<"getopt">>,{pkg,<<"getopt">>,<<"0.8.2">>},0}, + {<<"providers">>,{pkg,<<"providers">>,<<"1.6.0">>},0}, + {<<"relx">>,{pkg,<<"relx">>,<<"3.19.0">>},0}, + {<<"ssl_verify_hostname">>, + {pkg,<<"ssl_verify_hostname">>,<<"1.0.5">>}, + 0}]}. +[{pkg_hash,[{<<"bbmustache">>, + <<"7BA94F971C5AFD7B6617918A4BB74705E36CAB36EB84B19B6A1B7EE06427AA38">>}, + {<<"certifi">>, + <<"A7966EFB868B179023618D29A407548F70C52466BF1849B9E8EBD0E34B7EA11F">>}, + {<<"cf">>, + <<"69D0B1349FD4D7D4DC55B7F407D29D7A840BF9A1EF5AF529F1EBE0CE153FC2AB">>}, + {<<"cth_readable">>, + <<"983913A8E8572310B7EAF5F2631148B7D70B3C090D2120DCFE777A93AA4165FB">>}, + {<<"erlware_commons">>, + <<"A04433071AD7D112EDEFC75AC77719DD3E6753E697AC09428FC83D7564B80B15">>}, + {<<"eunit_formatters">>, + <<"7A6FC351EB5B873E2356B8852EB751E20C13A72FBCA03393CF682B8483509573">>}, + {<<"getopt">>, + <<"B17556DB683000BA50370B16C0619DF1337E7AF7ECBF7D64FBF8D1D6BCE3109B">>}, + {<<"providers">>, + <<"DB0E2F9043AE60C0155205FCD238D68516331D0E5146155E33D1E79DC452964A">>}, + {<<"relx">>, + <<"286DD5244B4786F56AAC75D5C8E2D1FB4CFD306810D4EC8548F3AE1B3AADB8F7">>}, + {<<"ssl_verify_hostname">>, + <<"2E73E068CD6393526F9FA6D399353D7C9477D6886BA005F323B592D389FB47BE">>}]}]. -- cgit v1.1 From 760ffdc79d19ffff732915b2abaa81ecdbeeef91 Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Wed, 25 May 2016 13:25:34 -0400 Subject: Hide the expected hash on fetch output --- src/rebar_prv_install_deps.erl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/rebar_prv_install_deps.erl b/src/rebar_prv_install_deps.erl index 5e6aa4c..a8a7ea0 100644 --- a/src/rebar_prv_install_deps.erl +++ b/src/rebar_prv_install_deps.erl @@ -352,10 +352,14 @@ make_relative_to_root(State, Path) when is_list(Path) -> rebar_dir:make_relative_path(Path, Root). fetch_app(AppInfo, AppDir, State) -> - ?INFO("Fetching ~s (~p)", [rebar_app_info:name(AppInfo), rebar_app_info:source(AppInfo)]), + ?INFO("Fetching ~s (~p)", [rebar_app_info:name(AppInfo), + format_source(rebar_app_info:source(AppInfo))]), Source = rebar_app_info:source(AppInfo), true = rebar_fetch:download_source(AppDir, Source, State). +format_source({pkg, Name, Vsn, _Hash}) -> {pkg, Name, Vsn}; +format_source(Source) -> Source. + %% This is called after the dep has been downloaded and unpacked, if it hadn't been already. %% So this is the first time for newly downloaded apps that its .app/.app.src data can %% be read in an parsed. -- cgit v1.1 From 589eaf13e1de55695cea4f34719c22c4b6467734 Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Thu, 9 Jun 2016 21:30:42 -0400 Subject: Hashes in lockfile are diff friendly This reworks the version and hash printing in the lockfile to minimize diff changes: - the version is on its own line so that the locks are mostly the same aside from the last line - the hashes are each printed on one line with the package name for simpler diffing too. --- rebar.lock | 56 ++++++++++++++++++++++------------------------------ src/rebar_config.erl | 23 +++++++++++++++++++-- 2 files changed, 45 insertions(+), 34 deletions(-) diff --git a/rebar.lock b/rebar.lock index 38e22e5..4f133d6 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,32 +1,24 @@ -{"1.1.0",[{<<"bbmustache">>,{pkg,<<"bbmustache">>,<<"1.0.4">>},0}, - {<<"certifi">>,{pkg,<<"certifi">>,<<"0.4.0">>},0}, - {<<"cf">>,{pkg,<<"cf">>,<<"0.2.1">>},0}, - {<<"cth_readable">>,{pkg,<<"cth_readable">>,<<"1.2.2">>},0}, - {<<"erlware_commons">>,{pkg,<<"erlware_commons">>,<<"0.21.0">>},0}, - {<<"eunit_formatters">>,{pkg,<<"eunit_formatters">>,<<"0.3.1">>},0}, - {<<"getopt">>,{pkg,<<"getopt">>,<<"0.8.2">>},0}, - {<<"providers">>,{pkg,<<"providers">>,<<"1.6.0">>},0}, - {<<"relx">>,{pkg,<<"relx">>,<<"3.19.0">>},0}, - {<<"ssl_verify_hostname">>, - {pkg,<<"ssl_verify_hostname">>,<<"1.0.5">>}, - 0}]}. -[{pkg_hash,[{<<"bbmustache">>, - <<"7BA94F971C5AFD7B6617918A4BB74705E36CAB36EB84B19B6A1B7EE06427AA38">>}, - {<<"certifi">>, - <<"A7966EFB868B179023618D29A407548F70C52466BF1849B9E8EBD0E34B7EA11F">>}, - {<<"cf">>, - <<"69D0B1349FD4D7D4DC55B7F407D29D7A840BF9A1EF5AF529F1EBE0CE153FC2AB">>}, - {<<"cth_readable">>, - <<"983913A8E8572310B7EAF5F2631148B7D70B3C090D2120DCFE777A93AA4165FB">>}, - {<<"erlware_commons">>, - <<"A04433071AD7D112EDEFC75AC77719DD3E6753E697AC09428FC83D7564B80B15">>}, - {<<"eunit_formatters">>, - <<"7A6FC351EB5B873E2356B8852EB751E20C13A72FBCA03393CF682B8483509573">>}, - {<<"getopt">>, - <<"B17556DB683000BA50370B16C0619DF1337E7AF7ECBF7D64FBF8D1D6BCE3109B">>}, - {<<"providers">>, - <<"DB0E2F9043AE60C0155205FCD238D68516331D0E5146155E33D1E79DC452964A">>}, - {<<"relx">>, - <<"286DD5244B4786F56AAC75D5C8E2D1FB4CFD306810D4EC8548F3AE1B3AADB8F7">>}, - {<<"ssl_verify_hostname">>, - <<"2E73E068CD6393526F9FA6D399353D7C9477D6886BA005F323B592D389FB47BE">>}]}]. +{"1.1.0", +[{<<"bbmustache">>,{pkg,<<"bbmustache">>,<<"1.0.4">>},0}, + {<<"certifi">>,{pkg,<<"certifi">>,<<"0.4.0">>},0}, + {<<"cf">>,{pkg,<<"cf">>,<<"0.2.1">>},0}, + {<<"cth_readable">>,{pkg,<<"cth_readable">>,<<"1.2.2">>},0}, + {<<"erlware_commons">>,{pkg,<<"erlware_commons">>,<<"0.21.0">>},0}, + {<<"eunit_formatters">>,{pkg,<<"eunit_formatters">>,<<"0.3.1">>},0}, + {<<"getopt">>,{pkg,<<"getopt">>,<<"0.8.2">>},0}, + {<<"providers">>,{pkg,<<"providers">>,<<"1.6.0">>},0}, + {<<"relx">>,{pkg,<<"relx">>,<<"3.19.0">>},0}, + {<<"ssl_verify_hostname">>,{pkg,<<"ssl_verify_hostname">>,<<"1.0.5">>},0}]}. +[ +{pkg_hash,[ + {<<"bbmustache">>, <<"7BA94F971C5AFD7B6617918A4BB74705E36CAB36EB84B19B6A1B7EE06427AA38">>}, + {<<"certifi">>, <<"A7966EFB868B179023618D29A407548F70C52466BF1849B9E8EBD0E34B7EA11F">>}, + {<<"cf">>, <<"69D0B1349FD4D7D4DC55B7F407D29D7A840BF9A1EF5AF529F1EBE0CE153FC2AB">>}, + {<<"cth_readable">>, <<"983913A8E8572310B7EAF5F2631148B7D70B3C090D2120DCFE777A93AA4165FB">>}, + {<<"erlware_commons">>, <<"A04433071AD7D112EDEFC75AC77719DD3E6753E697AC09428FC83D7564B80B15">>}, + {<<"eunit_formatters">>, <<"7A6FC351EB5B873E2356B8852EB751E20C13A72FBCA03393CF682B8483509573">>}, + {<<"getopt">>, <<"B17556DB683000BA50370B16C0619DF1337E7AF7ECBF7D64FBF8D1D6BCE3109B">>}, + {<<"providers">>, <<"DB0E2F9043AE60C0155205FCD238D68516331D0E5146155E33D1E79DC452964A">>}, + {<<"relx">>, <<"286DD5244B4786F56AAC75D5C8E2D1FB4CFD306810D4EC8548F3AE1B3AADB8F7">>}, + {<<"ssl_verify_hostname">>, <<"2E73E068CD6393526F9FA6D399353D7C9477D6886BA005F323B592D389FB47BE">>}]} +]. diff --git a/src/rebar_config.erl b/src/rebar_config.erl index 031df8b..828c45d 100644 --- a/src/rebar_config.erl +++ b/src/rebar_config.erl @@ -82,10 +82,29 @@ write_lock_file(LockFile, Locks) -> file:write_file(LockFile, io_lib:format("~p.~n", [NewLocks])); _ -> file:write_file(LockFile, - io_lib:format("{~p,~p}.~n~p.~n", - [?CONFIG_VERSION, NewLocks, Attrs])) + io_lib:format("{~p,~n~p}.~n[~n~s~n].~n", + [?CONFIG_VERSION, NewLocks, + format_attrs(Attrs)])) end. +%% Attributes have a special formatting to ensure there's only one per +%% line in terms of pkg_hash, so we disturb source diffing as little +%% as possible. +format_attrs([]) -> []; +format_attrs([{pkg_hash, Vals}|T]) -> + [io_lib:format("{pkg_hash,[~n",[]), format_hashes(Vals), "]}", + maybe_comma(T) | format_attrs(T)]; +format_attrs([H|T]) -> + [io_lib:format("~p~s", [H, maybe_comma(T)]) | format_attrs(T)]. + +format_hashes([]) -> []; +format_hashes([{Pkg,Hash}|T]) -> + [" {", io_lib:format("~p",[Pkg]), ", ", io_lib:format("~p", [Hash]), "}", + maybe_comma(T) | format_hashes(T)]. + +maybe_comma([]) -> ""; +maybe_comma([_|_]) -> io_lib:format(",~n", []). + read_attrs(_Vsn, Locks, Attrs) -> %% Beta copy does not know how to expand attributes, but %% is ready to support it. -- cgit v1.1 From 71df9bf1411c04e2f7dae7e9f0352180664b9365 Mon Sep 17 00:00:00 2001 From: Fred Hebert Date: Fri, 10 Jun 2016 08:43:13 -0400 Subject: Only display old version warning once This uses the env variable as a global store for variables. It's not the cleanest thing, but it sounded nicer than pdicts. --- src/rebar_config.erl | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/rebar_config.erl b/src/rebar_config.erl index 828c45d..9e13d46 100644 --- a/src/rebar_config.erl +++ b/src/rebar_config.erl @@ -58,6 +58,9 @@ consult_lock_file(File) -> [Locks] when is_list(Locks) -> % beta lock file read_attrs(beta, Locks, []); [{Vsn, Locks}|Attrs] when is_list(Locks) -> % versioned lock file + %% Because this is the first version of rebar3 to introduce a lock + %% file, all versionned lock files with a different versions have + %% to be newer. case Vsn of ?CONFIG_VERSION -> ok; @@ -65,14 +68,24 @@ consult_lock_file(File) -> %% Make sure the warning below is to be shown whenever a version %% newer than the current one is being used, as we can't parse %% all the contents of the lock file properly. - ?WARN("Rebar3 detected a lock file from a newer version. " - "It will be loaded in compatibility mode, but important " - "information may be missing or lost. It is recommended to " - "upgrade Rebar3.", []) + warn_vsn_once() end, read_attrs(Vsn, Locks, Attrs) end. +warn_vsn_once() -> + Warn = application:get_env(rebar, warn_config_vsn) =/= {ok, false}, + application:set_env(rebar, warn_config_vsn, false), + case Warn of + false -> ok; + true -> + ?WARN("Rebar3 detected a lock file from a newer version. " + "It will be loaded in compatibility mode, but important " + "information may be missing or lost. It is recommended to " + "upgrade Rebar3.", []) + end. + + write_lock_file(LockFile, Locks) -> {NewLocks, Attrs} = write_attrs(Locks), %% Write locks in the beta format, at least until it's been long -- cgit v1.1