Merge branch 'dev' into 'master'

Dev

See merge request zxq9/zx!6
This commit is contained in:
Craig Everett 2018-06-05 03:05:28 +00:00
commit a1236630cd
9 changed files with 572 additions and 539 deletions

View File

@ -1,5 +0,0 @@
{realm,"otpr"}.
{username,"zxq9"}.
{realmname,"Craig Everett"}.
{contact_info,{"email","zxq9@zxq9.com"}}.
{keys,["zxq9-root"]}.

View File

@ -9,4 +9,4 @@
zx_conn,
zx_lib,
zx_net]},
{mod, {zx, []}}]}.
{mod, {zx, none}}]}.

View File

@ -64,7 +64,7 @@
type := app | lib}.
-type outcome() :: ok
| {error, Reason :: atom()}
| {error, Reason :: term()}
| {error, Code :: non_neg_integer()}
| {error, Info :: string(), Code :: non_neg_integer()}.
@ -101,7 +101,8 @@ do(["list", "deps"]) ->
do(["list", "deps", PackageString]) ->
done(zx_local:list_deps(PackageString));
do(["install", PackageFile]) ->
done(zx_local:assimilate(PackageFile));
ok = start(),
done(zx_daemon:install(PackageFile));
do(["set", "dep", PackageString]) ->
done(zx_local:set_dep(PackageString));
do(["set", "version", VersionString]) ->
@ -229,7 +230,8 @@ compatibility_check(Platforms) ->
%% @equiv application:ensure_started(zx).
start() ->
application:ensure_started(zx).
ok = application:ensure_started(zx),
zx_daemon:init_connections().
-spec stop() -> ok | {error, Reason :: term()}.
@ -249,14 +251,14 @@ stop() ->
-spec start(StartType, StartArgs) -> Result
when StartType :: normal,
StartArgs :: [],
StartArgs :: none,
Result :: {ok, pid()}.
%% @private
%% Application callback. Not to be called directly.
start(normal, []) ->
start(normal, none) ->
ok = application:ensure_started(inets),
zx_daemon:start_link().
zx_sup:start_link().
-spec stop(term()) -> ok.
@ -331,7 +333,7 @@ run(Identifier, RunArgs) ->
end,
{ok, PackageID} = ensure_installed(FuzzyID),
ok = build(PackageID),
Dir = zx_lib:path(lib, PackageID),
Dir = zx_lib:ppath(lib, PackageID),
{ok, Meta} = zx_lib:read_project_meta(Dir),
prepare(PackageID, Meta, Dir, RunArgs).
@ -371,34 +373,32 @@ prepare(PackageID, Meta, Dir, RunArgs) ->
Deps = maps:get(deps, Meta),
NotInstalled = fun(P) -> not filelib:is_dir(zx_lib:ppath(lib, P)) end,
Needed = lists:filter(NotInstalled, Deps),
Pending = lists:map(fun zx_daemon:fetch/1, Needed),
case await_fetches(Pending) of
ok ->
ok = lists:foreach(fun install/1, Needed),
ok = lists:foreach(fun build/1, Needed),
execute(Type, PackageID, Meta, Dir, RunArgs);
{error, Errors} ->
error_exit("Failed package fetches: ~tp", [Errors], ?LINE)
end.
execute(Type, PackageID, Meta, Dir, RunArgs).
await_fetches([]) -> ok;
await_fetches(Pending) -> await_fetches(Pending, []).
-spec install(PackageString :: string()) -> zx:outcome().
%% @private
%% Installs a package from upstream.
install(PackageString) ->
{ok, ID} = zx_daemon:install(PackageString),
install(PackageString, ID).
await_fetches([], []) ->
ok;
await_fetches([], Errors) ->
{error, Errors};
await_fetches(Pending, Errors) ->
{NewPending, NewErrors} =
install(PackageString, ID) ->
receive
{z_reply, ID, ok} ->
{lists:delete(ID, Pending), Errors};
{z_reply, ID, {error, Package, Reason}} ->
{lists:delete(ID, Pending), [{Package, Reason} | Errors]}
end,
await_fetches(NewPending, NewErrors).
{z_result, ID, done} ->
ok;
{z_result, ID, {hops, Count}} ->
ok = log(info, "~ts ~w hops away.", [PackageString, Count]),
install(PackageString, ID);
{z_result, ID, {error, Reason}} ->
{error, Reason, 1}
after 60000 ->
{error, timeout, 62}
end.
-spec execute(Type, PackageID, Meta, Dir, RunArgs) -> no_return()
@ -516,7 +516,7 @@ resolve_installed_version({Realm, Name, Version}) ->
resolve_installed_version(PackageDir, Version) ->
DirStrings = filelib:wildcard("*", PackageDir),
Versions = lists:fold(fun tuplize/2, [], DirStrings),
Versions = lists:foldl(fun tuplize/2, [], DirStrings),
zx_lib:find_latest_compatible(Version, Versions).
@ -527,58 +527,26 @@ tuplize(String, Acc) ->
end.
%%% Package utilities
-spec install(package_id()) -> ok.
%% @private
%% Install a package from the cache into the local system.
%% Before calling this function it must be known that:
%% - The zsp file is in the cache
%% - The zsp file is valid
%% - This function will only be called on startup by the launch process
%% - The package is not already installed
%% - If this function crashes it will completely halt the system
install(PackageID = {Realm, Name, _}) ->
{ok, PackageString} = zx_lib:package_string(PackageID),
ok = log(info, "Installing ~ts", [PackageString]),
ZrpFile = filename:join(zx_lib:path(zsp, Realm, Name), zx_lib:namify_zsp(PackageID)),
Files = zx_lib:extract_zsp_or_die(ZrpFile),
TgzFile = zx_lib:namify_tgz(PackageID),
{TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files),
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin),
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
ok = ensure_package_dirs(PackageID),
PackageDir = zx_lib:path(lib, PackageID),
ok = zx_lib:force_dir(PackageDir),
ok = zx_key:verify(TgzData, Signature, PubKey),
ok = erl_tar:extract({binary, TgzData}, [compressed, {cwd, PackageDir}]),
log(info, "~ts installed", [PackageString]).
-spec build(package_id()) -> ok.
%% @private
%% Given an AppID, build the project from source and add it to the current lib path.
build(PackageID) ->
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(zx_lib:path(lib, PackageID)),
ok = file:set_cwd(zx_lib:ppath(lib, PackageID)),
ok = zx_lib:build(),
file:set_cwd(CWD).
-spec ensure_package_dirs(package_id()) -> ok.
%% @private
%% Procedure to guarantee that directory locations necessary for the indicated app to
%% run have been created or halt execution.
ensure_package_dirs(PackageID) ->
Dirs = [zx_lib:path(D, PackageID) || D <- [etc, var, tmp, log, lib]],
lists:foreach(fun zx_lib:force_dir/1, Dirs).
%% FIXME
%-spec ensure_package_dirs(package_id()) -> ok.
%%% @private
%%% Procedure to guarantee that directory locations necessary for the indicated app to
%%% run have been created or halt execution.
%
%ensure_package_dirs(PackageID) ->
% Dirs = [zx_lib:ppath(D, PackageID) || D <- [etc, var, tmp, log, lib]],
% lists:foreach(fun zx_lib:force_dir/1, Dirs).
@ -642,10 +610,9 @@ usage() ->
" zx reject PackageID~n"
" zx add key Realm KeyName~n"
" zx get key Realm KeyName~n"
" zx rem key Realm KeyName~n"
" zx create user~n"
" zx create userfiles Realm UserName~n"
" zx create keypair Realm~n"
" zx create keypair~n"
" zx export user UserID~n"
" zx import user ZdufFile~n"
"~n"
@ -658,7 +625,6 @@ usage() ->
" zx accept PackageID~n"
" zx create realm~n"
" zx create realmfile Realm~n"
" zx create sysop~n"
"~n"
"Where~n"
" PackageID :: A string of the form Realm-Name[-Version]~n"

View File

@ -91,26 +91,25 @@ list_resigns(Realm) ->
end.
-spec submit(PackageFile) -> no_return()
when PackageFile :: file:filename().
-spec submit(ZspPath :: file:filename()) -> zx:outcome().
%% @private
%% Submit a package to the appropriate "prime" server for the given realm.
submit(PackageFile) ->
Files = zx_lib:extract_zsp_or_die(PackageFile),
{ok, PackageData} = file:read_file(PackageFile),
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin),
{Realm, Package, Version} = maps:get(package_id, Meta),
{ok, Socket} = connect_auth(Realm),
ok = send(Socket, {submit, {Realm, Package, Version}}),
submit(ZspPath) ->
{ok, ZspBin} = file:read_file(ZspPath),
<<SigSize:24, Sig:SigSize/binary, Signed/binary>> = ZspBin,
<<MetaSize:16, MetaBin:MetaSize/binary, _/binary>> = Signed,
{ok, {PackageID, SigKeyName, _}} = zx_lib:b_to_ts(MetaBin),
{ok, PubKey} = zx_key:load(public, {element(1, PackageID), SigKeyName}),
true = zx_key:verify(Signed, Sig, PubKey),
{ok, Socket} = connect_auth(element(1, PackageID)),
ok = send(Socket, {submit, PackageID}),
ok = recv_or_die(Socket),
ok = gen_tcp:send(Socket, PackageData),
ok = log(info, "Done sending contents of ~tp", [PackageFile]),
ok = gen_tcp:send(Socket, ZspBin),
ok = log(info, "Done sending contents of ~tp", [ZspPath]),
Outcome = recv_or_die(Socket),
log(info, "Response: ~tp", [Outcome]),
ok = disconnect(Socket),
halt(0).
disconnect(Socket).
review(PackageString) ->

View File

@ -10,7 +10,7 @@
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([start_conn/2]).
-export([start_conn/1]).
-export([start_link/0]).
-export([init/1]).
@ -18,9 +18,8 @@
%%% Interface Functions
-spec start_conn(Host, Serial) -> Result
-spec start_conn(Host) -> Result
when Host :: zx:host(),
Serial :: zx:serial(),
Result :: {ok, pid()}
| {error, Reason},
Reason :: term().
@ -28,8 +27,8 @@
%% Start an upstream connection handler.
%% (Should only be called from zx_conn).
start_conn(Host, Serial) ->
supervisor:start_child(?MODULE, [Host, Serial]).
start_conn(Host) ->
supervisor:start_child(?MODULE, [Host]).

View File

@ -147,10 +147,10 @@
-export([pass_meta/3,
subscribe/1, unsubscribe/1,
list/0, list/1, list/2, list/3, latest/1,
fetch/1, verify_key/1,
verify_key/1, fetch/1, install/1,
pending/1, packagers/1, maintainers/1, sysops/1]).
-export([report/1, result/2, notify/2]).
-export([start_link/0, stop/0]).
-export([start_link/0, init_connections/0, stop/0]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
code_change/3, terminate/2]).
@ -188,14 +188,12 @@
-record(rmeta,
{revision = 0 :: non_neg_integer(),
serial = 0 :: non_neg_integer(),
{serial = 0 :: non_neg_integer(),
prime = {"zomp.tsuriai.jp", 11311} :: zx:host(),
private = [] :: [zx:host()],
mirrors = queue:new() :: queue:queue(zx:host()),
realm_keys = [] :: [zx:key_meta()],
package_keys = [] :: [zx:key_meta()],
sysops = [] :: [zx:sysop_meta()],
key = [] :: zx:key_name(),
sysop = none :: zx:user_name(),
assigned = none :: none | pid(),
available = [] :: [pid()]}).
@ -449,25 +447,6 @@ latest({Realm, Name, Version}) ->
request({latest, Realm, Name, Version}).
-spec fetch(PackageID) -> {ok, RequestID}
when PackageID :: zx:package_id(),
RequestID :: integer().
%% @doc
%% Ensure a package is available locally, or queue it for download otherwise.
%% Returns a request ID which will be returned in a message with the result from an
%% upstream zomp node. Crashes the caller on an illegal realm name, package name or
%% version tuple.
%%
%% Response messages are of the type `result()' where the third element is of the
%% type `fetch_result()'.
fetch({Realm, Name, Version}) ->
true = zx_lib:valid_lower0_9(Realm),
true = zx_lib:valid_lower0_9(Name),
true = zx_lib:valid_version(Version),
request({fetch, Realm, Name, Version}).
-spec verify_key(KeyID) -> {ok, RequestID}
when KeyID :: zx:key_id(),
RequestID :: id().
@ -485,6 +464,28 @@ verify_key({Realm, KeyName}) ->
request({verify_key, Realm, KeyName}).
-spec install(Path :: file:filename()) -> zx:outcome().
%% @doc
%% Install a package from a local file.
install(Path) ->
gen_server:call(?MODULE, {install, Path}).
-spec fetch(PackageString :: string()) -> {ok, id()}.
%% @doc
%% Install the specified package. This returns an id() that will be referenced
%% in a later response message.
fetch(PackageString) ->
case zx_lib:package_id(PackageString) of
{ok, PackageID} ->
gen_server:call(?MODULE, {fetch, PackageID});
{error, invalid_package_string} ->
{error, "Invalid package string.", 22}
end.
-spec pending(Package) -> {ok, RequestID}
when Package :: zx:package(),
RequestID :: id().
@ -557,7 +558,7 @@ sysops(Realm) ->
%% Private function to wrap the necessary bits up.
request(Action) ->
gen_server:call(?MODULE, {request, self(), Action}).
gen_server:call(?MODULE, {request, Action}).
@ -610,63 +611,13 @@ start_link() ->
%% TODO: Implement lockfile checking and master lock acquisition.
init(none) ->
Blank = blank_state(),
{ok, MX, CX} = init_connections(),
State = Blank#s{mx = MX, cx = CX},
{ok, State}.
{ok, #s{}}.
-spec blank_state() -> state().
%% @private
%% Used to generate a correct, but exactly empty state.
%% Useful mostly for testing and validation, though also actually used in the program.
blank_state() ->
#s{}.
-spec init_connections() -> {ok, MX, CX}
when MX :: monitor_index(),
CX :: conn_index().
%% @private
%% Starting from a stateless condition, recruit and resolve all realm relevant data,
%% populate host caches, and initiate connections to required realms. On completion
%% return a populated MX and CX to the caller. Should only ever be called by init/1.
%% Returns an `ok' tuple to disambiguate it from pure functions *and* to leave an
%% obvious place to populate error returns in the future if desired.
-spec init_connections() -> ok.
init_connections() ->
CX = cx_load(),
MX = mx_new(),
Realms = cx_realms(CX),
init_connections(Realms, MX, CX).
-spec init_connections(Realms, MX, CX) -> {ok, NewMX, NewCX}
when Realms :: [zx:realm()],
MX :: monitor_index(),
CX :: conn_index(),
NewMX :: monitor_index(),
NewCX :: conn_index().
init_connections([Realm | Realms], MX, CX) ->
{ok, Hosts, NextCX} = cx_next_hosts(3, Realm, CX),
MaybeAttempt =
fun(Host, {M, C}) ->
case cx_maybe_add_attempt(Host, Realm, C) of
not_connected ->
{ok, Pid} = zx_conn:start(Host),
NewM = mx_add_monitor(Pid, attempt, M),
NewC = cx_add_attempt(Pid, Host, Realm, C),
{NewM, NewC};
{ok, NewC} ->
{M, NewC}
end
end,
{NewMX, NewCX} = lists:foldl(MaybeAttempt, {MX, NextCX}, Hosts),
init_connections(Realms, NewMX, NewCX);
init_connections([], MX, CX) ->
{ok, MX, CX}.
gen_server:cast(?MODULE, init_connections).
@ -689,12 +640,24 @@ stop() ->
handle_call({request, list}, _, State = #s{cx = CX}) ->
Realms = cx_realms(CX),
{reply, {ok, Realms}, State};
handle_call({request, Requestor, Action}, From, State = #s{id = ID}) ->
handle_call({request, Action}, From, State = #s{id = ID}) ->
NewID = ID + 1,
_ = gen_server:reply(From, {ok, NewID}),
Requestor = element(1, From),
NextState = do_request(Requestor, Action, State#s{id = NewID}),
NewState = eval_queue(NextState),
{noreply, NewState};
handle_call({fetch, PackageID}, From, State = #s{id = ID}) ->
NewID = ID + 1,
_ = gen_server:reply(From, {ok, NewID}),
Requestor = element(1, From),
NextState = do_fetch(PackageID, Requestor, State#s{id = NewID}),
NewState = eval_queue(NextState),
{noreply, NewState};
handle_call({install, Path}, _, State) ->
Result = do_import_zsp(Path),
NewState = eval_queue(State),
{reply, Result, NewState};
handle_call(Unexpected, From, State) ->
ok = log(warning, "Unexpected call ~tp: ~tp", [From, Unexpected]),
{noreply, State}.
@ -726,6 +689,9 @@ handle_cast({notify, Conn, Package, Update}, State) ->
ok = do_notify(Conn, Package, Update, State),
NewState = eval_queue(State),
{noreply, NewState};
handle_cast(init_connections, State) ->
NewState = init_connections(State),
{noreply, NewState};
handle_cast(stop, State) ->
{stop, normal, State};
handle_cast(Unexpected, State) ->
@ -913,6 +879,49 @@ dequeue(Pending) ->
end.
-spec init_connections(State) -> NewState
when State :: state(),
NewState :: state().
%% @private
%% Starting from a stateless condition, recruit and resolve all realm relevant data,
%% populate host caches, and initiate connections to required realms. On completion
%% return a populated MX and CX to the caller. Should only ever be called by init/1.
%% Returns an `ok' tuple to disambiguate it from pure functions *and* to leave an
%% obvious place to populate error returns in the future if desired.
init_connections(State = #s{mx = MX, cx = CX}) ->
Realms = cx_realms(CX),
{ok, NewMX, NewCX} = init_connections(Realms, MX, CX),
State#s{mx = NewMX, cx = NewCX}.
-spec init_connections(Realms, MX, CX) -> {ok, NewMX, NewCX}
when Realms :: [zx:realm()],
MX :: monitor_index(),
CX :: conn_index(),
NewMX :: monitor_index(),
NewCX :: conn_index().
init_connections([Realm | Realms], MX, CX) ->
{ok, Hosts, NextCX} = cx_next_hosts(3, Realm, CX),
MaybeAttempt =
fun(Host, {M, C}) ->
case cx_maybe_add_attempt(Host, Realm, C) of
not_connected ->
{ok, Pid} = zx_conn:start(Host),
NewM = mx_add_monitor(Pid, attempt, M),
NewC = cx_add_attempt(Pid, Host, Realm, C),
{NewM, NewC};
{ok, NewC} ->
{M, NewC}
end
end,
{NewMX, NewCX} = lists:foldl(MaybeAttempt, {MX, NextCX}, Hosts),
init_connections(Realms, NewMX, NewCX);
init_connections([], MX, CX) ->
{ok, MX, CX}.
-spec ensure_connections(Realms, MX, CX) -> {NewMX, NewCX}
when Realms :: [zx:realm()],
MX :: monitor_index(),
@ -968,11 +977,14 @@ reassign_conns([], CX, Unassigned) ->
do_result(ID, Result, State = #s{requests = Requests, dropped = Dropped, mx = MX}) ->
{NewDropped, NewRequests, NewMX} =
case maps:take(ID, Requests) of
{Request, NextRequests} ->
{Request, Rest} when element(1, element(2, Request)) == fetch ->
{NextMX, NextR} = handle_fetch_result(ID, Result, Request, Rest, MX),
{Dropped, NextR, NextMX};
{Request, Rest} ->
Requestor = element(1, Request),
Requestor ! {z_result, ID, Result},
NextMX = mx_del_monitor(Requestor, {requestor, ID}, MX),
{Dropped, NextRequests, NextMX};
{Dropped, Rest, NextMX};
error ->
NextDropped = handle_orphan_result(ID, Result, Dropped),
{NextDropped, Requests, MX}
@ -980,6 +992,24 @@ do_result(ID, Result, State = #s{requests = Requests, dropped = Dropped, mx = MX
State#s{requests = NewRequests, dropped = NewDropped, mx = NewMX}.
handle_fetch_result(ID, {done, Bin}, {Requestor, _}, Requests, MX) ->
Result =
case do_import_package(Bin) of
ok -> done;
Error -> Error
end,
Requestor ! {z_result, ID, Result},
NextMX = mx_del_monitor(Requestor, {requestor, ID}, MX),
{NextMX, Requests};
handle_fetch_result(ID, Hops = {hops, _}, Request = {Requestor, _}, Requests, MX) ->
Requestor ! {z_result, ID, Hops},
{MX, maps:put(ID, Request, Requests)};
handle_fetch_result(ID, Outcome, {Requestor, _}, Requests, MX) ->
Requestor ! {z_result, ID, Outcome},
NextMX = mx_del_monitor(Requestor, {requestor, ID}, MX),
{NextMX, Requests}.
-spec handle_orphan_result(ID, Result, Dropped) -> NewDropped
when ID :: id(),
Result :: result(),
@ -1208,6 +1238,112 @@ drop_requests(ReqIDs, Dropped, Requests) ->
lists:fold(Partition, {Dropped, Requests}, ReqIDs).
-spec do_fetch(PackageID, Requestor, State) -> NewState
when PackageID :: zx:package_id(),
Requestor :: pid(),
State :: state(),
NewState :: state().
%% @private
%% Provide a chance to bypass if the package is in cache.
do_fetch(PackageID, Requestor, State = #s{id = ID}) ->
Path = zx_lib:zsp_path(PackageID),
case file:read_file(Path) of
{ok, Bin} ->
case do_import_package(Bin) of
ok ->
Requestor ! {z_result, ID, ok},
{ok, State};
Error ->
Requestor ! {z_result, ID, Error},
{ok, State}
end;
{error, enoent} ->
{Realm, Name, Version} = PackageID,
Action = {fetch, Realm, Name, Version},
do_request(Requestor, Action, State);
Error ->
Requestor ! {z_result, ID, Error}
end.
-spec do_import_zsp(file:filename()) -> zx:outcome().
%% @private
%% Dealing with data from the (probably local) filesystem can fail in a bajillion ways
%% and spring memory leaks if one tries to get too clever. So I'm sidestepping all the
%% madness with a "try++" here by spawning a suicidal helper.
do_import_zsp(Path) ->
{Pid, Mon} = spawn_monitor(fun() -> import_from_path(Path) end),
receive
{Pid, Outcome} ->
true = demonitor(Mon, [flush]),
Outcome;
{'DOWN', Pid, process, Mon, Info} ->
{error, Info}
after 5000 ->
{error, timeout}
end.
do_import_package(Bin) ->
{Pid, Mon} = spawn_monitor(fun() -> import_package(Bin) end),
receive
{Pid, Outcome} ->
true = demonitor(Mon, [flush]),
Outcome;
{'DOWN', Pid, process, Mon, Info} ->
{error, Info}
after 5000 ->
{error, timeout}
end.
-spec import_from_path(ZspPath) -> no_return()
when ZspPath :: file:filename().
%% @private
%% The happy path of .zsp installation.
%% Must NEVER be executed by the zx_daemon directly.
%% More generally, there are a few phases:
%% 1- Loading the binary to extract the PackageID
%% 2- Checking the signature
%% 3- Moving the file to the cache
%% 4- Wiping the destination directory
%% 5- Extracting the TarGz to the destination
%% Some combination of functions should make these steps happen in a way that isn't
%% totally ridiculous, OR the bullet should just be bitten an allow for the
%% redundant lines here and there in different package management functions.
%%
%% Use cases are:
%% - Install a missing package from upstream
%% - Install a missing package from the local cache
%% - Reinstall a package from the local cache
%% - Import a package to the cache from the local filesystem and install it
%%
%% The Correct Approach as determine by The Royal Me is that I'm going to accept the
%% redundant code in the short-term because the data format is already decided.
%% If a place to get more fancy with the phases becomes really obvious after writing
%% identicalish segements of functions a few places then I'll break things apart.
import_from_path(ZspPath) ->
{ok, Bin} = file:read_file(ZspPath),
import_package(Bin).
import_package(Bin = <<Size:24, Sig:Size/binary, Signed/binary>>) ->
<<MetaSize:16, MetaBin:MetaSize/binary, TarGZ/binary>> = Signed,
{PackageID, SigKeyName, _} = zx_lib:b_to_ts(MetaBin),
{ok, PubKey} = zx_key:load(public, {element(1, PackageID), SigKeyName}),
true = zx_key:verify(Signed, Sig, PubKey),
ok = file:write_file(zx_lib:zsp_path(PackageID), Bin),
Destination = zx_lib:ppath(lib, PackageID),
ok = filelib:ensure_dir(Destination),
ok = zx_lib:rm_rf(Destination),
ok = file:make_dir(Destination),
Result = erl_tar:extract({binary, TarGZ}, [{cwd, Destination}, compressed]),
zx_daemon ! {self(), Result}.
%%% Monitor Index ADT Interface Functions
@ -1366,36 +1502,30 @@ cx_load() ->
%% where any number of wild things might be going on in the user's filesystem).
cx_populate() ->
Pattern = filename:join([zx_lib:path(etc), "*", "realm.conf"]),
case filelib:wildcard(Pattern) of
[] -> {error, no_realms};
RealmFiles -> {ok, cx_populate(RealmFiles, [])}
end.
Realms = zx_lib:list_realms(),
CX = lists:foldl(fun cx_populate/2, [], Realms),
{ok, CX}.
-spec cx_populate(RealmFiles, Realms) -> NewRealms
when RealmFiles :: file:filename(),
Realms :: [{zx:realm(), realm_meta()}],
NewRealms :: [{zx:realm(), realm_meta()}].
-spec cx_populate(Realms, CX) -> NewCX
when Realms :: [zx:realm()],
CX :: [{zx:realm(), realm_meta()}],
NewCX :: [{zx:realm(), realm_meta()}].
%% @private
%% Pack an initially empty conn_index() with realm meta and host cache data.
%% Should not halt on a corrupted, missing, malformed, etc. realm file but will log
%% any loading errors.
cx_populate([File | Files], Realms) ->
NewRealms =
case file:consult(File) of
cx_populate(Realm, CX) ->
case zx_lib:load_realm_conf(Realm) of
{ok, Meta} ->
Realm = cx_load_realm_meta(Meta),
[Realm | Realms];
Record = cx_load_realm_meta(Meta),
[{Realm, Record} | CX];
{error, Reason} ->
Message = "Loading realm file ~tp failed with: ~tp. Skipping...",
ok = log(warning, Message, [File, Reason]),
Realms
end,
cx_populate(Files, NewRealms);
cx_populate([], Realms) ->
Realms.
Message = "Loading realm ~tp failed with: ~tp. Skipping...",
ok = log(warning, Message, [Realm, Reason]),
CX
end.
-spec cx_load_realm_meta(Meta) -> Result
@ -1405,20 +1535,12 @@ cx_populate([], Realms) ->
%% This function MUST adhere to the realmfile definition found at.
cx_load_realm_meta(Meta) ->
{realm, Realm} = lists:keyfind(realm, 1, Meta),
{revision, Revision} = lists:keyfind(revision, 1, Meta),
{prime, Prime} = lists:keyfind(prime, 1, Meta),
{realm_keys, RealmKeys} = lists:keyfind(realm_keys, 1, Meta),
{package_keys, PackageKeys} = lists:keyfind(packge_keys, 1, Meta),
{sysops, Sysops} = lists:keyfind(sysops, 1, Meta),
Realm = maps:get(realm, Meta),
Basic =
#rmeta{revision = Revision,
prime = Prime,
realm_keys = RealmKeys,
package_keys = PackageKeys,
sysops = Sysops},
Complete = cx_load_cache(Realm, Basic),
{Realm, Complete}.
#rmeta{prime = maps:get(prime, Meta),
sysop = maps:get(sysop, Meta),
key = maps:get(key, Meta)},
cx_load_cache(Realm, Basic).
-spec cx_load_cache(Realm, Basic) -> Complete
@ -1542,7 +1664,7 @@ cx_next_host(Meta = #rmeta{prime = Prime, private = Private, mirrors = Mirrors})
{ok, Next, Meta#rmeta{mirrors = NewMirrors}};
{empty, Mirrors} ->
Enqueue = fun(H, Q) -> queue:in(H, Q) end,
NewMirrors = lists:foldl(Enqueue, Private, Mirrors),
NewMirrors = lists:foldl(Enqueue, Mirrors, Private),
{prime, Prime, Meta#rmeta{mirrors = NewMirrors}}
end.

View File

@ -21,7 +21,7 @@
%%% Functions
-spec ensure_keypair(zx:key_id()) -> true | no_return().
-spec ensure_keypair(zx:key_id()) -> zx:outcome().
%% @private
%% Check if both the public and private key based on KeyID exists.
@ -30,17 +30,17 @@ ensure_keypair(KeyID = {Realm, KeyName}) ->
{true, true} ->
true;
{false, true} ->
Message = "Public key ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]),
halt(1);
Format = "Public key ~tp/~tp cannot be found",
Message = io_lib:format(Format, [Realm, KeyName]),
{error, Message, 2};
{true, false} ->
Message = "Private key ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]),
halt(1);
Format = "Private key ~tp/~tp cannot be found",
Message = io_lib:format(Format, [Realm, KeyName]),
{error, Message, 2};
{false, false} ->
Message = "Key pair ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]),
halt(1)
Format = "Key pair ~tp/~tp cannot be found",
Message = io_lib:format(Format, [Realm, KeyName]),
{error, Message, 2}
end.
@ -76,31 +76,19 @@ path(private, {Realm, KeyName}) ->
prompt_keygen() ->
Message =
"~n Enter a name for your new keys.~n~n"
" Valid names must start with a lower-case letter, and can include~n"
" only lower-case letters, numbers, and periods, but no series of~n"
" consecutive periods. (That is: [a-z0-9\\.])~n~n"
" To designate the key as realm-specific, enter the realm name and~n"
" key name separated by a space.~n~n"
" Example: some.realm my.key~n",
"~nKEY NAME~n"
"Enter a name for your new key pair.~n"
"Valid names must start with a lower-case letter, and can include "
"only lower-case letters, numbers, and underscores, but no series of "
"consecutive underscores. (That is: [a-z0-9_])~n"
" Example: my_key~n",
ok = io:format(Message),
Input = zx_tty:get_input(),
{Realm, KeyName} =
case string:lexemes(Input, " ") of
[R, K] -> {R, K};
[K] -> {"otpr", K}
end,
case {zx_lib:valid_lower0_9(Realm), zx_lib:valid_label(KeyName)} of
{true, true} ->
{Realm, KeyName};
{false, true} ->
ok = io:format("Bad realm name ~tp. Try again.~n", [Realm]),
prompt_keygen();
{true, false} ->
ok = io:format("Bad key name ~tp. Try again.~n", [KeyName]),
prompt_keygen();
{false, false} ->
ok = io:format("NUTS! Both key and realm names are illegal. Try again.~n"),
case zx_lib:valid_lower0_9(Input) of
true ->
Input;
false ->
ok = io:format("Bad key name ~tp. Try again.~n", [Input]),
prompt_keygen()
end.
@ -108,19 +96,30 @@ prompt_keygen() ->
-spec generate_rsa(KeyID) -> Result
when KeyID :: zx:key_id(),
Result :: ok
| {error, keygen_fail}.
| {error, Reason},
Reason :: keygen_fail
| exists.
%% @private
%% Generate an RSA keypair and write them in der format to the current directory, using
%% filenames derived from Prefix.
%% NOTE: The current version of this command is likely to only work on a unix system.
generate_rsa(KeyID = {Realm, KeyName}) ->
PemFile = filename:join(zx_lib:path(key, Realm), KeyName ++ ".pub.pem"),
BaseName = filename:join(zx_lib:path(key, Realm), KeyName),
Pattern = BaseName ++ ".*.der",
case filelib:wildcard(Pattern) of
[] -> generate_rsa(KeyID, BaseName);
_ -> {error, exists}
end.
generate_rsa(KeyID, BaseName) ->
PemFile = BaseName ++ ".pub.pem",
KeyFile = path(private, KeyID),
PubFile = path(public, KeyID),
ok = lists:foreach(fun zx_lib:halt_if_exists/1, [PemFile, KeyFile, PubFile]),
ok = log(info, "Generating ~p and ~p. Please be patient...", [KeyFile, PubFile]),
ok = gen_p_key(KeyFile),
case gen_p_key(KeyFile) of
ok ->
ok = der_to_pem(KeyFile, PemFile),
{ok, PemBin} = file:read_file(PemFile),
[PemData] = public_key:pem_decode(PemBin),
@ -135,24 +134,35 @@ generate_rsa(KeyID = {Realm, KeyName}) ->
ok = lists:foreach(fun file:delete/1, [PemFile, KeyFile, PubFile]),
ok = log(error, "Something has gone wrong."),
{error, keygen_fail}
end;
{error, no_ssl} ->
ok = log(error, "OpenSSL not found."),
{error, keygen_fail}
end.
-spec gen_p_key(KeyFile) -> ok
when KeyFile :: file:filename().
-spec gen_p_key(KeyFile) -> Result
when KeyFile :: file:filename(),
Result :: ok
| {error, no_ssl}.
%% @private
%% Format an openssl shell command that will generate proper 16k RSA keys.
gen_p_key(KeyFile) ->
case openssl() of
{ok, OpenSSL} ->
Command =
io_lib:format("~ts genpkey"
" -algorithm rsa"
" -out ~ts"
" -outform DER"
" -pkeyopt rsa_keygen_bits:16384",
[openssl(), KeyFile]),
[OpenSSL, KeyFile]),
Out = os:cmd(Command),
io:format(Out).
io:format(Out);
Error ->
Error
end.
-spec der_to_pem(KeyFile, PemFile) -> ok
@ -167,6 +177,8 @@ gen_p_key(KeyFile) ->
%% way.
der_to_pem(KeyFile, PemFile) ->
case openssl() of
{ok, OpenSSL} ->
Command =
io_lib:format("~ts rsa"
" -inform DER"
@ -174,9 +186,12 @@ der_to_pem(KeyFile, PemFile) ->
" -outform PEM"
" -pubout"
" -out ~ts",
[openssl(), KeyFile, PemFile]),
[OpenSSL, KeyFile, PemFile]),
Out = os:cmd(Command),
io:format(Out).
io:format(Out);
Error ->
Error
end.
-spec check_key(KeyFile, PubFile) -> Result
@ -196,11 +211,14 @@ check_key(KeyFile, PubFile) ->
public_key:verify(TestMessage, sha512, Signature, Pub).
-spec openssl() -> Executable | no_return()
when Executable :: file:filename().
-spec openssl() -> Result
when Result :: {ok, Executable}
| {error, no_ssl},
Executable :: file:filename().
%% @private
%% Attempt to locate the installed openssl executable for use in shell commands.
%% Halts execution with an error message if the executable cannot be found.
%% TODO: Determine whether it is even worth it to perform this check VS restricting
%% os:cmd/1 directed zx_key functions by platform.
openssl() ->
OpenSSL =
@ -208,16 +226,15 @@ openssl() ->
{unix, _} -> "openssl";
{win32, _} -> "openssl.exe"
end,
ok =
case os:find_executable(OpenSSL) of
false ->
ok = log(error, "OpenSSL could not be found in this system's PATH."),
ok = log(error, "Install OpenSSL and then retry."),
error_exit("Missing system dependenct: OpenSSL", ?LINE);
{error, no_ssl};
Path ->
log(info, "OpenSSL executable found at: ~ts", [Path])
end,
OpenSSL.
log(info, "OpenSSL executable found at: ~ts", [Path]),
{ok, OpenSSL}
end.
-spec load(Type, KeyID) -> Result
@ -242,43 +259,13 @@ load(Type, KeyID) ->
end.
-spec verify(Data, Signature, PubKey) -> ok | no_return()
-spec verify(Data, Signature, PubKey) -> boolean()
when Data :: binary(),
Signature :: binary(),
PubKey :: public_key:rsa_public_key().
%% @private
%% Verify the RSA Signature of some Data against the given PubKey or halt execution.
%% This function always assumes sha512 is the algorithm being used.
%% Should only ever be called by the initial launch process.
%% Curry out the choice of algorithm. This will probably disappear in a few more
%% versions as the details of sha512 and RSA gradually give way to the Brave New World.
verify(Data, Signature, PubKey) ->
case public_key:verify(Data, sha512, Signature, PubKey) of
true -> ok;
false -> error_exit("Bad package signature!", ?LINE)
end.
%%% Error exits
-spec error_exit(Error, Line) -> no_return()
when Error :: term(),
Line :: non_neg_integer().
%% @private
%% Format an error message in a way that makes it easy to locate.
error_exit(Error, Line) ->
error_exit(Error, [], Line).
-spec error_exit(Format, Args, Line) -> no_return()
when Format :: string(),
Args :: [term()],
Line :: non_neg_integer().
%% @private
%% Format an error message in a way that makes it easy to locate.
error_exit(Format, Args, Line) ->
File = filename:basename(?FILE),
ok = log(error, "~ts:~tp: " ++ Format, [File, Line | Args]),
halt(1).
public_key:verify(Data, sha512, Signature, PubKey).

View File

@ -25,11 +25,9 @@
valid_lower0_9/1, valid_label/1, valid_version/1,
string_to_version/1, version_to_string/1,
package_id/1, package_string/1,
namify_zsp/1, namify_tgz/1,
zsp_path/1,
namify_zsp/1, zsp_path/1,
find_latest_compatible/2, installed/1,
realm_conf/1, load_realm_conf/1,
extract_zsp_or_die/1, halt_if_exists/1,
build/0,
rm_rf/1, rm/1,
b_to_t/1, b_to_ts/1]).
@ -582,37 +580,17 @@ package_string(_) ->
when PackageID :: zx:package_id(),
ZrpFileName :: file:filename().
%% @private
%% Map an PackageID to its correct .zsp package file name.
%% Map a PackageID to its correct .zsp package file name.
namify_zsp(PackageID) -> namify(PackageID, "zsp").
-spec namify_tgz(PackageID) -> TgzFileName
when PackageID :: zx:package_id(),
TgzFileName :: file:filename().
%% @private
%% Map an PackageID to its correct gzipped tarball source bundle filename.
namify_tgz(PackageID) -> namify(PackageID, "tgz").
-spec namify(PackageID, Suffix) -> FileName
when PackageID :: zx:package_id(),
Suffix :: string(),
FileName :: file:filename().
%% @private
%% Converts an PackageID to a canonical string, then appends the provided
%% filename Suffix.
namify(PackageID, Suffix) ->
namify_zsp(PackageID) ->
{ok, PackageString} = package_string(PackageID),
PackageString ++ "." ++ Suffix.
PackageString ++ ".zsp".
-spec zsp_path(zx:package_id()) -> file:filename().
zsp_path(PackageID) ->
filename:join(path(zsp, element(1, PackageID)), namify_zsp(PackageID)).
zsp_path(PackageID = {Realm, _, _}) ->
filename:join(path(zsp, Realm), namify_zsp(PackageID)).
-spec find_latest_compatible(Version, Versions) -> Result
@ -683,7 +661,7 @@ realm_conf(Realm) ->
| file:posix()
| {Line :: integer(), Mod :: module(), Cause :: term()}.
%% @private
%% Load the config for the given realm or halt with an error.
%% Load the config for the given realm.
load_realm_conf(Realm) ->
Path = realm_conf(Realm),
@ -696,41 +674,6 @@ load_realm_conf(Realm) ->
end.
-spec extract_zsp_or_die(FileName) -> Files | no_return()
when FileName :: file:filename(),
Files :: [{file:filename(), binary()}].
%% @private
%% Extract a zsp archive, if possible. If not possible, halt execution with as accurate
%% an error message as can be managed.
extract_zsp_or_die(FileName) ->
case erl_tar:extract(FileName, [memory]) of
{ok, Files} ->
Files;
{error, {FileName, enoent}} ->
Message = "Can't find file ~ts.",
error_exit(Message, [FileName], ?LINE);
{error, invalid_tar_checksum} ->
Message = "~ts is not a valid zsp archive.",
error_exit(Message, [FileName], ?LINE);
{error, Reason} ->
Message = "Extracting package file failed with: ~160tp.",
error_exit(Message, [Reason], ?LINE)
end.
-spec halt_if_exists(file:filename()) -> ok | no_return().
%% @private
%% A helper function to guard against overwriting an existing file. Halts execution if
%% the file is found to exist.
halt_if_exists(Path) ->
case filelib:is_file(Path) of
true -> error_exit("~ts already exists! Halting.", [Path], ?LINE);
false -> ok
end.
-spec build() -> ok.
%% @private
%% Run any local `zxmake' script needed by the project for non-Erlang code (if present),
@ -755,6 +698,7 @@ build() ->
-spec rm_rf(file:filename()) -> ok | {error, file:posix()}.
%% @private
%% Recursively remove files and directories. Equivalent to `rm -rf'.
%% Does not return an error on a nonexistant path.
rm_rf(Path) ->
case filelib:is_dir(Path) of
@ -764,7 +708,10 @@ rm_rf(Path) ->
ok = lists:foreach(fun rm/1, Contents),
file:del_dir(Path);
false ->
file:delete(Path)
case filelib:is_regular(Path) of
true -> file:delete(Path);
false -> ok
end
end.
@ -803,18 +750,3 @@ b_to_ts(Binary) ->
catch
error:badarg -> error
end.
%%% Error exits
-spec error_exit(Format, Args, Line) -> no_return()
when Format :: string(),
Args :: [term()],
Line :: non_neg_integer().
%% @private
%% Format an error message in a way that makes it easy to locate.
error_exit(Format, Args, Line) ->
File = filename:basename(?FILE),
ok = log(error, "~ts:~tp: " ++ Format, [File, Line | Args]),
halt(1).

View File

@ -10,7 +10,7 @@
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([initialize/2, assimilate/1, set_version/1,
-export([initialize/2, set_version/1,
list_realms/0, list_packages/1, list_versions/1,
set_dep/1, list_deps/0, list_deps/1, drop_dep/1, verup/1, package/1,
add_realm/1, drop_realm/1,
@ -73,14 +73,14 @@ initialize3(Type, PackageID) ->
case package_exists(PackageID) of
false ->
Prefix = ask_prefix(),
initialize(Type, PackageID, Prefix);
initialize4(Type, PackageID, Prefix);
true ->
Message = "Package already exists. Try another.",
{error, Message, 17}
end.
initialize(app, PackageID, Prefix) ->
initialize4(app, PackageID, Prefix) ->
Instructions =
"The OTP application controller has to know what module to call start/2 on to "
"start your program.~n"
@ -93,23 +93,23 @@ initialize(app, PackageID, Prefix) ->
ok = io:format(Instructions),
case zx_tty:get_input() of
"" ->
initialize(lib, PackageID, Prefix, none);
initialize5(lib, PackageID, Prefix, none);
String ->
case zx_lib:valid_lower0_9(String) of
true ->
AppStart = {list_to_atom(String), []},
initialize(app, PackageID, Prefix, AppStart);
initialize5(app, PackageID, Prefix, AppStart);
false ->
Message = "The name \"~ts\" doesn't seem valid. Try \"[a-z_]*\".~n",
ok = io:format(Message, String),
initialize(app, PackageID, Prefix)
initialize4(app, PackageID, Prefix)
end
end;
initialize(lib, PackageID, Prefix) ->
initialize(lib, PackageID, Prefix, none).
initialize4(lib, PackageID, Prefix) ->
initialize5(lib, PackageID, Prefix, none).
initialize(Type, PackageID, Prefix, AppStart) ->
initialize5(Type, PackageID, Prefix, AppStart) ->
ok = update_source_vsn(element(3, PackageID)),
ok = initialize_app_file(PackageID, AppStart),
MetaList =
@ -268,39 +268,6 @@ initialize_app_file({_, Name, Version}, AppStart) ->
zx_lib:write_terms(AppFile, [AppProfile]).
-spec assimilate(PackageFile) -> zx:outcome()
when PackageFile :: file:filename().
%% @private
%% Receives a path to a file containing package data, examines it, and copies it to a
%% canonical location under a canonical name.
assimilate(PackageFile) ->
Files = zx_lib:extract_zsp_or_die(PackageFile),
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(zx_lib:zomp_dir()),
{"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files),
Meta = binary_to_term(MetaBin),
PackageID = maps:get(package_id, Meta),
TgzFile = zx_lib:namify_tgz(PackageID),
{TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files),
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
case public_key:verify(TgzData, sha512, Signature, PubKey) of
true ->
ok = file:copy(PackageFile, zx_lib:zsp_path(PackageID)),
assimilate2(CWD, PackageID);
false ->
{error, "Bad package signature.", 1}
end.
assimilate2(CWD, PackageID) ->
ok = file:set_cwd(CWD),
Message = "~ts is now locally available.",
{ok, PackageString} = zx_lib:package_string(PackageID),
log(info, Message, [PackageString]).
-spec set_version(VersionString) -> zx:outcome()
when VersionString :: string().
%% @private
@ -483,11 +450,11 @@ set_dep2(PackageID) ->
Deps = maps:get(deps, Meta),
case lists:member(PackageID, Deps) of
true -> ok;
false -> set_dep(PackageID, Deps, Meta)
false -> set_dep3(PackageID, Deps, Meta)
end.
-spec set_dep(PackageID, Deps, Meta) -> ok
-spec set_dep3(PackageID, Deps, Meta) -> ok
when PackageID :: zx:package_id(),
Deps :: [zx:package_id()],
Meta :: [term()].
@ -497,7 +464,7 @@ set_dep2(PackageID) ->
%% such a dependency is not already present. Then write the project meta back to its
%% file and exit.
set_dep(PackageID = {Realm, Name, NewVersion}, Deps, Meta) ->
set_dep3(PackageID = {Realm, Name, NewVersion}, Deps, Meta) ->
ExistingPackageIDs = fun({R, N, _}) -> {R, N} == {Realm, Name} end,
NewDeps =
case lists:partition(ExistingPackageIDs, Deps) of
@ -642,18 +609,35 @@ version_up(patch, {Realm, Name, OldVersion = {Major, Minor, Patch}}, OldMeta) ->
%% @private
%% Turn a target project directory into a package, prompting the user for appropriate
%% key selection or generation actions along the way.
%% TODO: Add user selection in the case more than one user with private keys exists.
package(TargetDir) ->
ok = log(info, "Packaging ~ts", [TargetDir]),
{ok, Meta} = zx_lib:read_project_meta(TargetDir),
{Realm, _, _} = maps:get(package_id, Meta),
case list_users(Realm) of
[] ->
UserName = create_user(#user_data{realm = Realm}),
package(TargetDir, Meta, UserName);
[UserName] ->
ok = log(info, "Signing as user ~tp", [UserName]),
package(TargetDir, Meta, UserName);
UserNames ->
UserName = zx_tty:select_string(UserNames),
package(TargetDir, Meta, UserName)
end.
package(TargetDir, Meta, UserName) ->
{Realm, _, _} = maps:get(package_id, Meta),
KeyDir = zx_lib:path(key, Realm),
ok = zx_lib:force_dir(KeyDir),
Pattern = KeyDir ++ "/*.key.der",
Pattern = filename:join(KeyDir, UserName ++ ".*.key.der"),
case [filename:basename(F, ".key.der") || F <- filelib:wildcard(Pattern)] of
[] ->
ok = log(info, "Need to generate key"),
KeyID = zx_key:prompt_keygen(),
ok = log(info, "No private keys found. Need to generate a keypair."),
KeyTag = zx_key:prompt_keygen(),
KeyName = UserName ++ "." ++ KeyTag,
KeyID = {Realm, KeyName},
ok = zx_key:generate_rsa(KeyID),
package(KeyID, TargetDir);
[KeyName] ->
@ -662,13 +646,15 @@ package(TargetDir) ->
package(KeyID, TargetDir);
KeyNames ->
KeyName = zx_tty:select_string(KeyNames),
package({Realm, KeyName}, TargetDir)
package(TargetDir, Meta, UserName, {Realm, KeyName})
end.
-spec package(KeyID, TargetDir) -> no_return()
when KeyID :: zx:key_id(),
TargetDir :: file:filename().
-spec package(TargetDir, Meta, UserName, KeyID) -> zx:outcome()
when TargetDir :: file:filename(),
Meta :: zx:meta(),
UserName :: zx:user_name(),
KeyID :: zx:key_id().
%% @private
%% Accept a KeyPrefix for signing and a TargetDir containing a project to package and
%% build a zsp package file ready to be submitted to a repository.
@ -678,33 +664,42 @@ package(KeyID, TargetDir) ->
PackageID = maps:get(package_id, Meta),
true = element(1, PackageID) == element(1, KeyID),
{ok, PackageString} = zx_lib:package_string(PackageID),
ZrpFile = PackageString ++ ".zsp",
TgzFile = PackageString ++ ".tgz",
ok = zx_lib:halt_if_exists(ZrpFile),
ZspFile = PackageString ++ ".zsp",
case filelib:is_regular(ZspFile) of
true -> {error, "Package file already exists. Aborting", 17};
false -> package(KeyID, TargetDir, PackageID, ZspFile)
end.
package(KeyID, TargetDir, PackageID, ZspFile) ->
ok = remove_binaries(TargetDir),
CrashDump = filename:join(TargetDir, "erl_crash.dump"),
ok =
case filelib:is_regular(CrashDump) of
true -> file:delete(CrashDump);
false -> ok
end,
{ok, Everything} = file:list_dir(TargetDir),
DotFiles = filelib:wildcard(".*", TargetDir),
Ignores = ["lib" | DotFiles],
Targets = lists:subtract(Everything, Ignores),
Targets = lists:subtract(Everything, DotFiles),
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(TargetDir),
ok = zx_lib:build(),
Modules =
[filename:basename(M, ".beam") || M <- filelib:wildcard("*.beam", "ebin")],
ok = remove_binaries("."),
ok = erl_tar:create(filename:join(CWD, TgzFile), Targets, [compressed]),
ok = file:set_cwd(CWD),
Grep = "grep -oP '^-module\\(\\K[^)]+' src/* | cut -d: -f2",
Modules = string:lexemes(os:cmd(Grep), "\n"),
TarGzPath = filename:join(zx_lib:path(tmp), ZspFile ++ ".tgz"),
ok = erl_tar:create(TarGzPath, Targets, [compressed]),
{ok, TgzBin} = file:read_file(TarGzPath),
ok = file:delete(TarGzPath),
MetaBin = term_to_binary({PackageID, element(2, KeyID), Modules}),
MetaSize = byte_size(MetaBin),
SignMe = <<MetaSize:16, MetaBin:MetaSize/binary, TgzBin/binary>>,
{ok, Key} = zx_key:load(private, KeyID),
{ok, TgzBin} = file:read_file(TgzFile),
Sig = public_key:sign(TgzBin, sha512, Key),
Add = fun({K, V}, M) -> maps:put(K, V, M) end,
FinalMeta = lists:foldl(Add, Meta, [{modules, Modules}, {sig, {KeyID, Sig}}]),
ok = file:write_file("zomp.meta", term_to_binary(FinalMeta)),
ok = erl_tar:create(ZrpFile, ["zomp.meta", TgzFile]),
ok = file:delete(TgzFile),
ok = file:delete("zomp.meta"),
ok = log(info, "Wrote archive ~ts", [ZrpFile]),
halt(0).
Sig = public_key:sign(SignMe, sha512, Key),
SigSize = byte_size(Sig),
ZspData = <<SigSize:24, Sig:SigSize/binary, SignMe/binary>>,
ok = file:set_cwd(CWD),
ok = file:write_file(ZspFile, ZspData),
log(info, "Wrote archive ~ts", [ZspFile]).
-spec remove_binaries(TargetDir) -> ok
@ -715,32 +710,18 @@ package(KeyID, TargetDir) ->
remove_binaries(TargetDir) ->
Beams = filelib:wildcard("**/*.{beam,ez}", TargetDir),
case [filename:join(TargetDir, Beam) || Beam <- Beams] of
[] ->
ok;
ToDelete ->
ok = log(info, "Removing: ~tp", [ToDelete]),
lists:foreach(fun file:delete/1, ToDelete)
end.
ToDelete = [filename:join(TargetDir, Beam) || Beam <- Beams],
lists:foreach(fun file:delete/1, ToDelete).
-spec create_plt() -> no_return().
%% @private
%% Generate a fresh PLT file that includes most basic core applications needed to
%% make a resonable estimate of a type system, write the name of the PLT to stdout,
%% and exit.
create_plt() ->
ok = build_plt(),
halt(0).
-spec build_plt() -> ok.
-spec create_plt() -> ok.
%% @private
%% Build a general plt file for Dialyzer based on the core Erland distro.
%% TODO: Make a per-package + dependencies version of this.
%% TODO: PLT build options.
%% TODO: Meaningful failure messages.
build_plt() ->
create_plt() ->
PLT = default_plt(),
Template =
"dialyzer --build_plt"
@ -775,7 +756,7 @@ dialyze() ->
ok =
case filelib:is_regular(PLT) of
true -> log(info, "Using PLT: ~tp", [PLT]);
false -> build_plt()
false -> create_plt()
end,
Me = escript:script_name(),
EvilTwin = filename:join(zx_lib:path(tmp), filename:basename(Me ++ ".erl")),
@ -799,8 +780,43 @@ dialyze() ->
grow_a_pair() ->
ok = file:set_cwd(zx_lib:zomp_dir()),
KeyID = zx_key:prompt_keygen(),
zx_key:generate_rsa(KeyID).
case zx_lib:list_realms() of
[] ->
{error, "No realms configured.", 61};
[Realm] ->
grow_a_pair(Realm);
Realms ->
Realm = zx_tty:select_string(Realms),
grow_a_pair(Realm)
end.
grow_a_pair(Realm) ->
Pattern = zx_lib:path(etc, Realm) ++ "*.user",
case [filename:basename(F, ".user") || F <- filelib:wildcard(Pattern)] of
[] ->
{ok, UserName} = create_user(#user_data{realm = Realm}),
grow_a_pair(UserName);
[UserName] ->
grow_a_pair(Realm, UserName);
UserNames ->
UserName = zx_tty:select_string(UserNames),
grow_a_pair(Realm, UserName)
end.
grow_a_pair(Realm, UserName) ->
KeyTag = zx_key:prompt_keygen(),
KeyName = UserName ++ "." ++ KeyTag,
case zx_key:generate_rsa({Realm, KeyName}) of
ok ->
ok;
{error, exists} ->
ok = io:format("That key name already exists. Try something different.~n"),
grow_a_pair(Realm, UserName);
Error ->
Error
end.
-spec drop_key(zx:key_id()) -> ok.
@ -1043,13 +1059,14 @@ create_sysop(U = #user_data{username = UserName,
end.
-spec create_user() -> zx:outcome().
-spec create_user() -> ok.
create_user() ->
create_user(#user_data{}).
UserName = create_user(#user_data{}),
log(info, "User ~ts created.", [UserName]).
-spec create_user(#user_data{}) -> zx:outcome().
-spec create_user(#user_data{}) -> zx:user_name().
create_user(U = #user_data{realm = none}) ->
case pick_realm() of
@ -1078,11 +1095,17 @@ create_user(U = #user_data{realm = Realm,
"Press a number to select something to change, or [ENTER] to accept.~n",
ok = io:format(Instructions, [Realm, UserName, RealName, Email]),
case zx_tty:get_input() of
"1" -> create_user(U#user_data{realm = none});
"2" -> create_user(U#user_data{username = none});
"3" -> create_user(U#user_data{realname = none});
"4" -> create_user(U#user_data{contact_info = none});
"" -> store_user(U);
"1" ->
create_user(U#user_data{realm = none});
"2" ->
create_user(U#user_data{username = none});
"3" ->
create_user(U#user_data{realname = none});
"4" ->
create_user(U#user_data{contact_info = none});
"" ->
ok = store_user(U),
{ok, UserName};
_ ->
ok = io:format("~nArglebargle, glop-glyf!?!~n~n"),
create_user(U)
@ -1108,6 +1131,15 @@ store_user(#user_data{realm = Realm,
log(info, "User ~tp created.", [{Realm, UserName}]).
-spec list_users(Realm) -> UserNames
when Realm :: zx:realm(),
UserNames :: [zx:user_name()].
list_users(Realm) ->
Pattern = filename:join(zx_lib:path(etc, Realm), "*.user"),
[filename:basename(UN, ".user") || UN <- filelib:wildcard(Pattern)].
-spec gen_keys(Realm, KeyNames) -> ok
when Realm :: zx:realm(),
KeyNames :: [zx:key_names()].
@ -1229,16 +1261,17 @@ make_realm_dirs(Realm) ->
lists:foreach(Make, Dirs).
-spec configure_zomp() -> ok.
configure_zomp() ->
ZompSettings =
[{node, 16},
{vampire, 16},
{leaf, 256},
{listen_port, 11311},
{public_port, 11311}],
io:format("~tp~n", [ZompSettings]).
%% FIXME
%-spec configure_zomp() -> ok.
%
%configure_zomp() ->
% ZompSettings =
% [{node, 16},
% {vampire, 16},
% {leaf, 256},
% {listen_port, 11311},
% {public_port, 11311}],
% io:format("~tp~n", [ZompSettings]).
-spec create_realmfile(Realm, Dir) -> ok