This commit is contained in:
Craig Everett 2018-05-31 16:46:44 +09:00
parent 7062ffe747
commit 014909f79b
8 changed files with 332 additions and 385 deletions

View File

@ -31,4 +31,6 @@ log(Level, Format, Args) ->
warning -> "[WARNING]";
error -> "[ERROR]"
end,
io:format("~s ~p: " ++ Format ++ "~n", [Tag, self() | Args]).
Out = io_lib:format("~s ~p: " ++ Format ++ "~n", [Tag, self() | Args]),
UTF8 = unicode:characters_to_binary(Out),
io:format(UTF8).

View File

@ -30,7 +30,8 @@
key_id/0, key_name/0, key_data/0,
user_id/0, user_name/0, contact_info/0, user_data/0,
lower0_9/0, label/0,
package_meta/0]).
package_meta/0,
outcome/0]).
-include("zx_logger.hrl").
@ -65,6 +66,11 @@
deps := [package_id()],
type := app | lib}.
-type outcome() :: ok
| {error, Reason :: atom()}
| {error, Code :: non_neg_integer()}
| {error, Info :: string(), Code :: non_neg_integer()}.
%%% Command Dispatch
@ -89,114 +95,103 @@ do(["runlocal" | ArgV]) ->
run_local(ArgV);
do(["init", "app", PackageString]) ->
ok = compatibility_check([unix]),
ok = zx_local:initialize(app, PackageString),
halt(0);
done(zx_local:initialize(app, PackageString));
do(["init", "lib", PackageString]) ->
ok = compatibility_check([unix]),
ok = zx_local:initialize(lib, PackageString),
halt(0);
done(zx_local:initialize(lib, PackageString));
do(["install", PackageFile]) ->
ok = zx_local:assimilate(PackageFile),
halt(0);
done(zx_local:assimilate(PackageFile));
do(["set", "dep", PackageString]) ->
ok = zx_local:set_dep(PackageString),
halt(0);
done(zx_local:set_dep(PackageString));
do(["set", "version", VersionString]) ->
ok = compatibility_check([unix]),
ok = zx_local:set_version(VersionString),
halt(0);
done(zx_local:set_version(VersionString));
do(["verup", Level]) ->
ok = compatibility_check([unix]),
ok = zx_local:verup(Level),
halt(0);
done(zx_local:verup(Level));
do(["list", "realms"]) ->
ok = zx_local:list_realms(),
halt(0);
done(zx_local:list_realms());
do(["list", "packages", Realm]) ->
ok = start(),
ok = zx_local:list_packages(Realm),
halt(0);
done(zx_local:list_packages(Realm));
do(["list", "versions", PackageName]) ->
ok = start(),
ok = zx_local:list_versions(PackageName),
halt(0);
done(zx_local:list_versions(PackageName));
do(["add", "realm", RealmFile]) ->
ok = zx_local:add_realm(RealmFile),
halt(0);
done(zx_local:add_realm(RealmFile));
do(["drop", "dep", PackageString]) ->
PackageID = zx_lib:package_id(PackageString),
ok = zx_local:drop_dep(PackageID),
halt(0);
done(zx_local:drop_dep(PackageID));
do(["package"]) ->
{ok, TargetDir} = file:get_cwd(),
zx_local:package(TargetDir);
do(["package", TargetDir]) ->
case filelib:is_dir(TargetDir) of
true ->
ok = zx_local:package(TargetDir),
halt(0);
false ->
ok = log(error, "Target directory ~tp does not exist!", [TargetDir]),
halt(22)
true -> done(zx_local:package(TargetDir));
false -> done({error, "Target directory does not exist", 22})
end;
do(["dialyze"]) ->
ok = zx_local:dialyze(),
halt(0);
done(zx_local:dialyze());
do(["create", "user", Realm, Name]) ->
ok = zx_local:create_user(Realm, Name),
halt(0);
done(zx_local:create_user(Realm, Name));
do(["create", "keypair"]) ->
ok = zx_local:grow_a_pair(),
halt(0);
done(zx_local:grow_a_pair());
do(["drop", "key", Realm, KeyName]) ->
ok = zx_local:drop_key({Realm, KeyName}),
halt(0);
done(zx_local:drop_key({Realm, KeyName}));
do(["create", "plt"]) ->
ok = zx_local:create_plt(),
halt(0);
done(zx_local:create_plt());
do(["create", "realm"]) ->
ok = zx_local:create_realm(),
halt(0);
done(zx_local:create_realm());
do(["create", "realmfile", Realm]) ->
ok = zx_local:create_realmfile(Realm, "."),
halt(0);
done(zx_local:create_realmfile(Realm, "."));
do(["takeover", Realm]) ->
ok = zx_local:takeover(Realm),
halt(0);
done(zx_local:takeover(Realm));
do(["abdicate", Realm]) ->
ok = zx_local:abdicate(Realm),
halt(0);
done(zx_local:abdicate(Realm));
do(["drop", "realm", Realm]) ->
ok = zx_local:drop_realm(Realm),
halt(0);
done(zx_local:drop_realm(Realm));
do(["list", "pending", PackageName]) ->
zx_auth:list_pending(PackageName);
done(zx_auth:list_pending(PackageName));
do(["list", "resigns", Realm]) ->
zx_auth:list_resigns(Realm);
done(zx_auth:list_resigns(Realm));
do(["submit", PackageFile]) ->
zx_auth:submit(PackageFile);
done(zx_auth:submit(PackageFile));
do(["review", PackageString]) ->
zx_auth:review(PackageString);
done(zx_auth:review(PackageString));
do(["approve", PackageString]) ->
PackageID = zx_lib:package_id(PackageString),
zx_auth:approve(PackageID);
done(zx_auth:approve(PackageID));
do(["reject", PackageString]) ->
PackageID = zx_lib:package_id(PackageString),
zx_auth:reject(PackageID);
done(zx_auth:reject(PackageID));
do(["accept", PackageString]) ->
zx_auth:accept(PackageString);
done(zx_auth:accept(PackageString));
do(["add", "packager", Package, UserName]) ->
zx_auth:add_packager(Package, UserName);
done(zx_auth:add_packager(Package, UserName));
do(["add", "maintainer", Package, UserName]) ->
zx_auth:add_maintainer(Package, UserName);
done(zx_auth:add_maintainer(Package, UserName));
do(["add", "sysop", Package, UserName]) ->
zx_auth:add_sysop(Package, UserName);
done(zx_auth:add_sysop(Package, UserName));
do(["add", "package", PackageName]) ->
zx_auth:add_package(PackageName);
done(zx_auth:add_package(PackageName));
do(_) ->
usage_exit(22).
-spec done(outcome()) -> no_return().
done(ok) ->
halt(0);
done({error, Reason}) when is_atom(Reason) ->
ok = log(error, "Operation failed with: ~tp", [Reason]),
halt(1);
done({error, Code}) when is_integer(Code) ->
halt(Code);
done({error, Info, Code}) ->
ok = log(error, Info),
halt(Code).
-spec compatibility_check(Platforms) -> ok | no_return()
when Platforms :: unix | win32.
%% @private
@ -335,7 +330,7 @@ run(Identifier, RunArgs) ->
end,
{ok, PackageID} = ensure_installed(FuzzyID),
ok = build(PackageID),
Dir = zx_lib:package_dir(PackageID),
Dir = zx_lib:path(lib, PackageID),
{ok, Meta} = zx_lib:read_project_meta(Dir),
prepare(PackageID, Meta, Dir, RunArgs).
@ -511,7 +506,7 @@ fetch_one(PackageID) ->
%% input was found, or no match was found at all.
resolve_installed_version({Realm, Name, Version}) ->
PackageDir = filename:join(["lib", Realm, Name]),
PackageDir = zx_lib:path(lib, Realm, Name),
case filelib:is_dir(PackageDir) of
true -> resolve_installed_version(PackageDir, Version);
false -> not_found
@ -545,10 +540,10 @@ tuplize(String, Acc) ->
%% - The package is not already installed
%% - If this function crashes it will completely halt the system
install(PackageID) ->
install(PackageID = {Realm, Name, _}) ->
{ok, PackageString} = zx_lib:package_string(PackageID),
ok = log(info, "Installing ~ts", [PackageString]),
ZrpFile = filename:join("zsp", zx_lib:namify_zsp(PackageID)),
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),
@ -557,7 +552,7 @@ install(PackageID) ->
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
ok = ensure_package_dirs(PackageID),
PackageDir = filename:join("lib", PackageString),
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}]),
@ -570,34 +565,24 @@ install(PackageID) ->
build(PackageID) ->
{ok, CWD} = file:get_cwd(),
ok = file:set_cwd(zx_lib:package_dir(PackageID)),
ok = file:set_cwd(zx_lib:path(lib, PackageID)),
ok = zx_lib:build(),
file:set_cwd(CWD).
%%% Directory & File Management
-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 = {Realm, Name, _}) ->
Package = {Realm, Name},
PackageHome = zx_lib:package_dir(PackageID),
PackageData = zx_lib:package_dir("var", Package),
PackageConf = zx_lib:package_dir("etc", Package),
Dirs = [PackageHome, PackageData, PackageConf],
ok = lists:foreach(fun zx_lib:force_dir/1, Dirs),
log(info, "Created dirs:~n\t~ts~n\t~ts~n\t~ts", Dirs).
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).
%%% Usage
-spec usage_exit(Code) -> no_return()
when Code :: integer().
%% @private

View File

@ -396,7 +396,7 @@ request_zsp(Socket, PackageID) ->
receive_zsp(Socket, PackageID) ->
receive
{tcp, Socket, Bin} ->
ZrpPath = filename:join("zsp", zx_lib:namify_zsp(PackageID)),
ZrpPath = zx_lib:zsp_path(PackageID),
ok = file:write_file(ZrpPath, Bin),
ok = zx_net:send(Socket, ok),
log(info, "Wrote ~ts", [ZrpPath]);
@ -520,7 +520,7 @@ terminate() ->
%% sourced, but exit with an error if it cannot locate or acquire the package.
%
%ensure_dep(Socket, PackageID) ->
% ZrpFile = filename:join("zsp", namify_zsp(PackageID)),
% ZrpFile = zx_lib:zsp_path(PackageID),
% ok =
% case filelib:is_regular(ZrpFile) of
% true -> ok;

View File

@ -172,7 +172,7 @@
{meta = none :: none | zx:package_meta(),
home = none :: none | file:filename(),
argv = none :: none | [string()],
sys_conf = zx_conf_sys:load() :: zx_conf_sys:data(),
sys_conf = zx_sys_conf:load() :: zx_sys_conf:data(),
id = 0 :: id(),
actions = [] :: [request()],
requests = maps:new() :: requests(),
@ -1366,8 +1366,7 @@ cx_load() ->
%% where any number of wild things might be going on in the user's filesystem).
cx_populate() ->
Home = zx_lib:zomp_dir(),
Pattern = filename:join(Home, "*.realm"),
Pattern = filename:join([zx_lib:path(etc), "*", "realm.conf"]),
case filelib:wildcard(Pattern) of
[] -> {error, no_realms};
RealmFiles -> {ok, cx_populate(RealmFiles, [])}
@ -1481,7 +1480,7 @@ cx_write_cache({Realm,
-spec cx_cache_file(zx:realm()) -> file:filename().
cx_cache_file(Realm) ->
filename:join(zx_lib:zomp_dir(), Realm ++ ".cache").
filename:join(zx_lib:path(var, Realm), "realm.cache").
-spec cx_realms(conn_index()) -> [zx:realms()].

View File

@ -12,7 +12,7 @@
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").
-export([ensure_keypair/1, have_public_key/1, have_private_key/1,
-export([ensure_keypair/1, have_key/2, keypath/2,
prompt_keygen/0, generate_rsa/1,
load/2, verify/3]).
@ -26,42 +26,45 @@
%% Check if both the public and private key based on KeyID exists.
ensure_keypair(KeyID = {Realm, KeyName}) ->
case {have_public_key(KeyID), have_private_key(KeyID)} of
case {have_key(public, KeyID), have_key(private, KeyID)} of
{true, true} ->
true;
{false, true} ->
Message = "Public key for ~tp/~tp cannot be found",
Message = "Public key ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]),
halt(1);
{true, false} ->
Message = "Private key for ~tp/~tp cannot be found",
Message = "Private key ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]),
halt(1);
{false, false} ->
Message = "Key pair for ~tp/~tp cannot be found",
Message = "Key pair ~tp/~tp cannot be found",
ok = log(error, Message, [Realm, KeyName]),
halt(1)
end.
-spec have_public_key(zx:key_id()) -> boolean().
-spec have_key(Type, KeyID) -> boolean()
when Type :: public | private,
KeyID :: zx:key_id().
%% @private
%% Determine whether the public key indicated by KeyID is in the keystore.
%% Determine whether the indicated key is present.
have_public_key({Realm, KeyName}) ->
PublicKeyFile = KeyName ++ ".pub.der",
PublicKeyPath = filename:join([zx_lib:zomp_dir(), "key", Realm, PublicKeyFile]),
filelib:is_regular(PublicKeyPath).
have_key(Type, KeyID) ->
filelib:is_regular(keypath(Type, KeyID)).
-spec have_private_key(zx:key_id()) -> boolean().
-spec keypath(Type, KeyID) -> Path
when Type :: public | private,
KeyID :: zx:key_id(),
Path :: file:filename().
%% @private
%% Determine whether the private key indicated by KeyID is in the keystore.
%% Given KeyID, return the path to the key type indicated.
have_private_key({Realm, KeyName}) ->
PrivateKeyFile = KeyName ++ ".key.der",
PrivateKeyPath = filename:join([zx_lib:zomp_dir(), "key", Realm, PrivateKeyFile]),
filelib:is_regular(PrivateKeyPath).
keypath(public, {Realm, KeyName}) ->
filename:join(zx_lib:path(key, Realm), KeyName ++ ".pub.der");
keypath(private, {Realm, KeyName}) ->
filename:join(zx_lib:path(key, Realm), KeyName ++ ".key.der").
@ -111,12 +114,10 @@ prompt_keygen() ->
%% filenames derived from Prefix.
%% NOTE: The current version of this command is likely to only work on a unix system.
generate_rsa({Realm, KeyName}) ->
KeyDir = filename:join([zx_lib:zomp_dir(), "key", Realm]),
ok = zx_lib:force_dir(KeyDir),
PemFile = filename:join(KeyDir, KeyName ++ ".pub.pem"),
KeyFile = filename:join(KeyDir, KeyName ++ ".key.der"),
PubFile = filename:join(KeyDir, KeyName ++ ".pub.der"),
generate_rsa(KeyID = {Realm, KeyName}) ->
PemFile = filename:join(zx_lib:path(key, Realm), KeyName ++ ".pub.pem"),
KeyFile = keypath(private, KeyID),
PubFile = keypath(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),
@ -223,22 +224,17 @@ openssl() ->
when Type :: private | public,
KeyID :: zx:key_id(),
Result :: {ok, DecodedKey :: term()}
| {error, Reason :: term()}.
| {error, Reason :: file:posix()}.
%% @private
%% Hide the details behind reading and loading DER encoded RSA key files.
load(Type, {Realm, KeyName}) ->
{DerType, Path} =
load(Type, KeyID) ->
DerType =
case Type of
private ->
KeyDer = KeyName ++ ".key.der",
P = filename:join([zx_lib:zomp_dir(), "key", Realm, KeyDer]),
{'RSAPrivateKey', P};
public ->
PubDer = KeyName ++ ".pub.der",
P = filename:join([zx_lib:zomp_dir(), "key", Realm, PubDer]),
{'RSAPublicKey', P}
private -> 'RSAPrivateKey';
public -> 'RSAPublicKey'
end,
Path = keypath(Type, KeyID),
ok = log(info, "Loading key from file ~ts", [Path]),
case file:read_file(Path) of
{ok, Bin} -> {ok, public_key:der_decode(DerType, Bin)};

View File

@ -18,20 +18,21 @@
path/1, path/2, path/3, path/4, ppath/2,
force_dir/1, mktemp_dir/1,
list_realms/0,
hosts_cache_file/1, get_prime/1, realm_meta/1,
get_prime/1, realm_meta/1,
read_project_meta/0, read_project_meta/1, read_package_meta/1,
write_project_meta/1, write_project_meta/2,
write_terms/2,
write_terms/2, exec_shell/1,
valid_lower0_9/1, valid_label/1, valid_version/1,
string_to_version/1, version_to_string/1,
package_id/1, package_string/1,
package_dir/1, package_dir/2,
namify_zsp/1, namify_tgz/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]).
rm_rf/1, rm/1,
b_to_t/1, b_to_ts/1]).
-include("zx_logger.hrl").
@ -184,14 +185,6 @@ mktemp_dir({Realm, Name}) ->
end.
-spec hosts_cache_file(zx:realm()) -> file:filename().
%% @private
%% Given a Realm name, construct a realm's .hosts filename and return it.
hosts_cache_file(Realm) ->
filename:join(zomp_dir(), Realm ++ ".hosts").
-spec list_realms() -> [zx:realm()].
%% @private
%% Check the filesystem for etc/[Realm Name]/realm.conf files.
@ -247,7 +240,8 @@ read_project_meta() ->
-spec read_project_meta(Dir) -> Result
when Dir :: file:filename(),
Result :: {ok, zx:package_meta()}
| {error, file:posix()}.
| {error, file:posix()}
| {error, file:posix(), non_neg_integer()}.
%% @private
%% Read the `zomp.meta' file from the indicated directory, if possible.
@ -256,9 +250,10 @@ read_project_meta(Dir) ->
case file:consult(Path) of
{ok, Meta} ->
{ok, maps:from_list(Meta)};
{error, enoent} ->
{error, "No project zomp.meta file. Wrong directory? Not initialized?", 2};
Error ->
ok = log(error, "Failed to open \"zomp.meta\" with ~tp", [Error]),
ok = log(error, "Wrong directory?"),
ok = log(error, "Read from zomp.meta failed with: ~tp", [Error]),
Error
end.
@ -268,10 +263,8 @@ read_project_meta(Dir) ->
Result :: {ok, zx:package_meta()}
| {error, file:posix()}.
read_package_meta({Realm, Name, Version}) ->
{ok, VersionString} = version_to_string(Version),
Path = filename:join([zomp_dir(), "lib", Realm, Name, VersionString]),
read_project_meta(Path).
read_package_meta(PackageID) ->
read_project_meta(path(lib, PackageID)).
-spec write_project_meta(Meta) -> Result
@ -319,6 +312,21 @@ write_terms(Filename, List) ->
file:write_file(Filename, Text).
-spec exec_shell(CMD) -> ok
when CMD :: string().
%% @private
%% Print the output of an os:cmd/1 event only if there is any.
exec_shell(CMD) ->
case os:cmd(CMD) of
"" ->
ok;
Out ->
Trimmed = string:trim(Out, trailing, "\r\n"),
log(info, "os:cmd(~tp) -> ~ts", [CMD, Trimmed])
end.
-spec valid_lower0_9(string()) -> boolean().
%% @private
%% Check whether a provided string is a valid lower0_9.
@ -570,31 +578,6 @@ package_string(_) ->
{error, invalid_package_id}.
-spec package_dir(zx:package_id()) -> file:filename().
%% @private
%% Returns the path to a package installation. Crashes if PackageID is not a valid
%% identitifer or if the version is incomplete (it is not possible to create a path
%% to a partial version number).
package_dir({Realm, Name, Version = {X, Y, Z}})
when is_integer(X), is_integer(Y), is_integer(Z) ->
{ok, PackageDir} = package_string({Realm, Name}),
{ok, VersionDir} = version_to_string(Version),
filename:join([zomp_dir(), "lib", PackageDir, VersionDir]).
-spec package_dir(Prefix, Package) -> PackageDataDir
when Prefix :: string(),
Package :: zx:package(),
PackageDataDir :: file:filename().
%% @private
%% Create an absolute path to an application directory prefixed by the inclued argument.
package_dir(Prefix, {Realm, Name}) ->
PackageString = package_string({Realm, Name, {z, z, z}}),
filename:join([zomp_dir(), Prefix, PackageString]).
-spec namify_zsp(PackageID) -> ZrpFileName
when PackageID :: zx:package_id(),
ZrpFileName :: file:filename().
@ -626,6 +609,12 @@ namify(PackageID, Suffix) ->
PackageString ++ "." ++ Suffix.
-spec zsp_path(zx:package_id()) -> file:filename().
zsp_path(PackageID) ->
filename:join(path(zsp, element(1, PackageID)), namify_zsp(PackageID)).
-spec find_latest_compatible(Version, Versions) -> Result
when Version :: zx:version(),
Versions :: [zx:version()],
@ -670,26 +659,24 @@ latest_compatible(Version, Versions) ->
%% True to its name, tells whether a package's install directory is found.
installed(PackageID) ->
filelib:is_dir(package_dir(PackageID)).
filelib:is_dir(path(lib, PackageID)).
-spec realm_conf(Realm) -> RealmFileName
-spec realm_conf(Realm) -> Path
when Realm :: string(),
RealmFileName :: file:filename().
Path :: file:filename().
%% @private
%% Take a realm name, and return the name of the realm filename that would result.
%% Given a realm name return the path to its conf file.
realm_conf(Realm) ->
Realm ++ ".realm".
filename:join(path(etc, Realm), "realm.conf").
-spec load_realm_conf(Realm) -> Result
when Realm :: zx:realm(),
Result :: {ok, RealmConf}
| {error, Reason},
RealmConf :: list(),
RealmConf :: map(),
Reason :: badarg
| terminated
| system_limit
@ -699,13 +686,13 @@ realm_conf(Realm) ->
%% Load the config for the given realm or halt with an error.
load_realm_conf(Realm) ->
Path = filename:join(path(etc, Realm), "realm.conf"),
Path = realm_conf(Realm),
case file:consult(Path) of
{ok, C} ->
C;
{error, enoent} ->
ok = log(warning, "Realm ~tp is not configured.", [Realm]),
halt(1)
{ok, maps:from_list(C)};
Error ->
ok = log(warning, "Loading realm conf ~ts failed with: ~tp", [Path, Error]),
Error
end.
@ -792,6 +779,30 @@ rm(Path) ->
end.
-spec b_to_t(binary()) -> {ok, term()} | error.
%% @private
%% A wrapper for the binary_to_term/1 BIF to hide the try..catch mess in the places we
%% don't want to crash on funky input.
b_to_t(Binary) ->
try
binary_to_term(Binary)
catch
error:badarg -> error
end.
-spec b_to_ts(binary()) -> {ok, term()} | error.
%% A wrapper for the binary_to_term/2 BIF to hide the try..catch mess in the places we
%% don't want to crash on funky input.
b_to_ts(Binary) ->
try
binary_to_term(Binary, [safe])
catch
error:badarg -> error
end.
%%% Error exits

View File

@ -25,7 +25,7 @@
%%% Functions
-spec initialize(Type, PackageString) -> ok
-spec initialize(Type, PackageString) -> zx:outcome()
when Type :: app | lib,
PackageString :: string().
%% @private
@ -33,32 +33,35 @@
%% and interaction with the user to determine a few details.
initialize(Type, RawPackageString) ->
ok =
case filelib:is_file("zomp.meta") of
false -> ok;
true -> error_exit("This project is already Zompified.", ?LINE)
end,
PackageID = {Realm, Name, _} =
false -> initialize2(Type, RawPackageString);
true -> {error, "This project is already Zompified.", 17}
end.
initialize2(Type, RawPackageString) ->
case zx_lib:package_id(RawPackageString) of
{ok, {R, N, {z, z, z}}} ->
{R, N, {0, 1, 0}};
initialize3(Type, {R, N, {0, 1, 0}});
{ok, {R, N, {X, z, z}}} ->
{R, N, {X, 0, 0}};
initialize3(Type, {R, N, {X, 0, 0}});
{ok, {R, N, {X, Y, z}}} ->
{R, N, {X, Y, 0}};
initialize3(Type, {R, N, {X, Y, 0}});
{ok, ID} ->
ID;
initialize3(Type, ID);
{error, invalid_package_string} ->
error_exit("Invalid package string: ~tp", [RawPackageString], ?LINE)
end,
{error, "Invalid package string.", 22}
end.
initialize3(Type, PackageID) ->
case package_exists(PackageID) of
false ->
Prefix = solicit_prefix(),
initialize(Type, PackageID, Prefix);
true ->
PackageName = zx_lib:package_string({Realm, Name, {z, z, z}}),
Message = "Package ~tp already exists. Try another.",
error_exit(Message, [PackageName], ?LINE)
Message = "Package already exists. Try another.",
{error, Message, 17}
end.
@ -161,6 +164,7 @@ solicit_prefix() ->
end
end.
-spec update_source_vsn(zx:version()) -> ok.
%% @private
%% Use grep to tell us which files have a `-vsn' attribute and which don't.
@ -177,8 +181,8 @@ update_source_vsn(Version) ->
SubF = "sed -i 's/-vsn(.*$/-vsn(\"~s\")./' $(grep -l '^-vsn(' src/*)",
Add = lists:flatten(io_lib:format(AddF, [VersionString])),
Sub = lists:flatten(io_lib:format(SubF, [VersionString])),
ok = exec_shell(Add),
ok = exec_shell(Sub),
_ = os:cmd(Add),
_ = os:cmd(Sub),
log(info, "Source version attributes set").
@ -240,22 +244,7 @@ initialize_app_file({_, Name, Version}, AppStart) ->
zx_lib:write_terms(AppFile, [AppProfile]).
-spec exec_shell(CMD) -> ok
when CMD :: string().
%% @private
%% Print the output of an os:cmd/1 event only if there is any.
exec_shell(CMD) ->
case os:cmd(CMD) of
"" ->
ok;
Out ->
Trimmed = string:trim(Out, trailing, "\r\n"),
log(info, "os:cmd(~tp) ->~n~ts", [CMD, Trimmed])
end.
-spec assimilate(PackageFile) -> ok
-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
@ -272,21 +261,23 @@ assimilate(PackageFile) ->
{TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files),
{KeyID, Signature} = maps:get(sig, Meta),
{ok, PubKey} = zx_key:load(public, KeyID),
ok =
case public_key:verify(TgzData, sha512, Signature, PubKey) of
true ->
ZrpPath = filename:join("zsp", zx_lib:namify_zsp(PackageID)),
file:copy(PackageFile, ZrpPath);
ok = file:copy(PackageFile, zx_lib:zsp_path(PackageID)),
assimilate2(CWD, PackageID);
false ->
error_exit("Bad package signature: ~ts", [PackageFile], ?LINE)
end,
{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_dep(Identifier :: string()) -> ok.
-spec set_dep(Identifier :: string()) -> zx:outcome().
%% @private
%% Set a specific dependency in the current project. If the project currently has a
%% dependency on the same package then the version of that dependency is updated to
@ -296,14 +287,12 @@ assimilate(PackageFile) ->
set_dep(Identifier) ->
{ok, {Realm, Name, FuzzyVersion}} = zx_lib:package_id(Identifier),
Version =
case FuzzyVersion of
{X, Y, Z} when is_integer(X), is_integer(Y), is_integer(Z) ->
{X, Y, Z};
set_dep({Realm, Name}, {X, Y, Z});
_ ->
error_exit("Incomplete version tuple: ~tp", [FuzzyVersion])
end,
set_dep({Realm, Name}, Version).
{error, "Incompelte version tuple.", 22}
end.
-spec set_dep(zx:package(), zx:version()) -> ok.
@ -313,12 +302,8 @@ set_dep({Realm, Name}, Version) ->
{ok, Meta} = zx_lib:read_project_meta(),
Deps = maps:get(deps, Meta),
case lists:member(PackageID, Deps) of
true ->
{ok, PackageString} = zx_lib:package_string(PackageID),
ok = log(info, "~ts is already a dependency", [PackageString]),
halt(0);
false ->
set_dep(PackageID, Deps, Meta)
true -> ok;
false -> set_dep(PackageID, Deps, Meta)
end.
@ -351,27 +336,25 @@ set_dep(PackageID = {Realm, Name, NewVersion}, Deps, Meta) ->
zx_lib:write_project_meta(NewMeta).
-spec set_version(VersionString) -> ok
-spec set_version(VersionString) -> zx:outcome()
when VersionString :: string().
%% @private
%% Convert a version string to a new version, sanitizing it in the process and returning
%% a reasonable error message on bad input.
set_version(VersionString) ->
NewVersion =
case zx_lib:string_to_version(VersionString) of
{ok, {_, _, z}} ->
Message = "'set version' arguments must be complete, ex: 1.2.3",
error_exit(Message, ?LINE);
{ok, Version} ->
Version;
{error, invalid_version_string} ->
Message = "Invalid version string: ~tp",
error_exit(Message, [VersionString], ?LINE)
end,
{error, Message, 22};
{ok, {_, _, z}} ->
Message = "'set version' arguments must be complete, ex: 1.2.3",
{error, Message, 22};
{ok, NewVersion} ->
{ok, Meta} = zx_lib:read_project_meta(),
{Realm, Name, OldVersion} = maps:get(package_id, Meta),
update_version(Realm, Name, OldVersion, NewVersion, Meta).
update_version(Realm, Name, OldVersion, NewVersion, Meta)
end.
-spec update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> ok
@ -390,25 +373,31 @@ set_version(VersionString) ->
update_version(Realm, Name, OldVersion, NewVersion, OldMeta) ->
PackageID = {Realm, Name, NewVersion},
NewMeta = maps:put(package_id, PackageID, OldMeta),
ok = zx_lib:write_project_meta(NewMeta),
case zx_lib:write_project_meta(NewMeta) of
ok ->
ok = update_source_vsn(NewVersion),
ok = update_app_vsn(Name, NewVersion),
{ok, OldVS} = zx_lib:version_to_string(OldVersion),
{ok, NewVS} = zx_lib:version_to_string(NewVersion),
log(info, "Version changed from ~s to ~s.", [OldVS, NewVS]).
log(info, "Version changed from ~s to ~s.", [OldVS, NewVS]);
Error ->
ok = log(error, "Write to zomp.meta failed with: ~tp", [Error]),
Error
end.
-spec list_realms() -> ok.
%% @private
%% List all currently configured realms. The definition of a "configured realm" is a
%% realm for which a .realm file exists in $ZOMP_HOME. The realms will be printed to
%% stdout and the program will exit.
%% List all currently configured realms.
list_realms() ->
lists:foreach(fun(R) -> io:format("~ts~n", [R]) end, zx_lib:list_realms()).
case zx_lib:list_realms() of
[] -> io:format("Oh noes! No realms are configured!~n");
Realms -> lists:foreach(fun(R) -> io:format("~ts~n", [R]) end, Realms)
end.
-spec list_packages(zx:realm()) -> ok.
-spec list_packages(zx:realm()) -> zx:outcome().
%% @private
%% Contact the indicated realm and query it for a list of registered packages and print
%% them to stdout.
@ -416,62 +405,59 @@ list_realms() ->
list_packages(Realm) ->
case zx_daemon:list_packages(Realm) of
{ok, []} ->
log(info, "Realm ~tp has no packages available.", [Realm]);
io:format("Realm ~tp has no packages available.~n", [Realm]);
{ok, Packages} ->
Print = fun({R, N}) -> io:format("~ts-~ts~n", [R, N]) end,
lists:foreach(Print, Packages);
{error, bad_realm} ->
error_exit("Bad realm name.", ?LINE);
{error, no_realm} ->
error_exit("Realm \"~ts\" is not configured.", ?LINE);
{error, "Unconfigured realm or bad realm name.", 22};
{error, network} ->
Message = "Network issues are preventing connection to the realm.",
error_exit(Message, ?LINE)
{error, Message, 101}
end.
-spec list_versions(PackageName :: string()) -> ok.
-spec list_versions(PackageName :: string()) -> zx:outcome().
%% @private
%% List the available versions of the package indicated. The user enters a string-form
%% package name (such as "otpr-zomp") and the return values will be full package strings
%% of the form "otpr-zomp-1.2.3", one per line printed to stdout.
list_versions(PackageName) ->
Package = {Realm, Name} =
case zx_lib:package_id(PackageName) of
{ok, {R, N, {z, z, z}}} ->
{R, N};
list_versions(PackageString) ->
case zx_lib:package_id(PackageString) of
{ok, PackageID} ->
list_versions2(PackageID);
{error, invalid_package_string} ->
error_exit("~tp is not a valid package name.", [PackageName], ?LINE)
end,
case zx_daemon:list_versions(Package) of
{ok, []} ->
Message = "Package ~ts has no versions available.",
log(info, Message, [PackageName]);
{ok, Versions} ->
Print =
fun(Version) ->
{ok, PackageString} = zx_lib:package_string({Realm, Name, Version}),
io:format("~ts~n", [PackageString])
end,
lists:foreach(Print, Versions);
{error, bad_realm} ->
error_exit("Bad realm name.", ?LINE);
{error, bad_package} ->
error_exit("Bad package name.", ?LINE);
{error, network} ->
Message = "Network issues are preventing connection to the realm.",
error_exit(Message, ?LINE)
{error, "Invalid package name.", 22}
end.
-spec add_realm(Path) -> ok
list_versions2(PackageID) ->
case zx_daemon:list_versions(PackageID) of
{ok, []} ->
io:format("No versions available.~n");
{ok, Versions} ->
Print =
fun(Version) ->
{ok, VersionString} = zx_lib:version_to_string(Version),
io:format("~ts~n", [VersionString])
end,
lists:foreach(Print, Versions);
{error, bad_realm} ->
{error, "Bad realm name.", 22};
{error, bad_package} ->
{error, "Bad package name.", 22};
{error, network} ->
Message = "Network issues are preventing connection to the realm.",
{error, Message, 101}
end.
-spec add_realm(Path) -> zx:outcome()
when Path :: file:filename().
%% @private
%% Add a .realm file to $ZOMP_HOME from a location in the filesystem.
%% Print the SHA512 of the .realm file for the user so they can verify that the file
%% is authentic. This implies, of course, that .realm maintainers are going to
%% post SHA512 sums somewhere visible.
%% Configure the system for a new realm by interpreting a .zrf file.
%% Also log the SHA512 of the .zrf for the user.
add_realm(Path) ->
case file:read_file(Path) of
@ -479,13 +465,30 @@ add_realm(Path) ->
Digest = crypto:hash(sha512, Data),
Text = integer_to_list(binary:decode_unsigned(Digest, big), 16),
ok = log(info, "SHA512 of ~ts: ~ts", [Path, Text]),
add_realm(Path, Data);
add_realm2(Data);
{error, enoent} ->
ok = log(warning, "FAILED: ~ts does not exist.", [Path]),
halt(1);
{error, "Realm bundle (.zrf) does not exist.", 2};
{error, eisdir} ->
ok = log(warning, "FAILED: ~ts is a directory, not a realm file.", [Path]),
halt(1)
{error, "Path is a directory, not a .zrf file.", 21}
end.
-spec add_realm2(Data) -> zx:outcome()
when Data :: binary().
add_realm2(Data) ->
case zx_lib:b_to_t(Data) of
{ok, {RealmConf, KeyDER}} ->
Realm = maps:get(realm, RealmConf),
ok = make_realm_dirs(Realm),
ConfPath = zx_lib:realm_conf(Realm),
zx_lib:write_terms(ConfPath, maps:to_list(RealmConf)),
KeyName = maps:get(key, RealmConf),
KeyPath = zx_lib:keypath(public, {Realm, KeyName}),
ok = file:write_file(KeyPath, KeyDER),
log(info, "Added realm ~tp.", [Realm]);
error ->
{error, "Invalid .zrf file.", 84}
end.
@ -516,7 +519,7 @@ drop_dep(PackageID) ->
end.
-spec verup(Level) -> ok
-spec verup(Level) -> zx:outcome()
when Level :: string().
%% @private
%% Convert input string arguments to acceptable atoms for use in update_version/1.
@ -527,7 +530,7 @@ verup("patch") -> version_up(patch);
verup(_) -> zx:usage_exit(22).
-spec version_up(Level) -> ok
-spec version_up(Level) -> zx:outcome()
when Level :: major
| minor
| patch.
@ -538,13 +541,13 @@ verup(_) -> zx:usage_exit(22).
%% read for some reason.
version_up(Arg) ->
Meta =
case zx_lib:read_project_meta() of
{ok, M} -> M;
Error -> error_exit("verup failed with: ~tp", [Error], ?LINE)
end,
{ok, Meta} ->
PackageID = maps:get(package_id, Meta),
version_up(Arg, PackageID, Meta).
version_up(Arg, PackageID, Meta);
Error ->
Error
end.
-spec version_up(Level, PackageID, Meta) -> ok
@ -582,7 +585,7 @@ package(TargetDir) ->
ok = log(info, "Packaging ~ts", [TargetDir]),
{ok, Meta} = zx_lib:read_project_meta(TargetDir),
{Realm, _, _} = maps:get(package_id, Meta),
KeyDir = filename:join([zx_lib:zomp_dir(), "key", Realm]),
KeyDir = zx_lib:path(key, Realm),
ok = zx_lib:force_dir(KeyDir),
Pattern = KeyDir ++ "/*.key.der",
case [filename:basename(F, ".key.der") || F <- filelib:wildcard(Pattern)] of
@ -712,12 +715,11 @@ dialyze() ->
true -> log(info, "Using PLT: ~tp", [PLT]);
false -> build_plt()
end,
TmpDir = filename:join(zx_lib:zomp_dir(), "tmp"),
Me = escript:script_name(),
EvilTwin = filename:join(TmpDir, filename:basename(Me ++ ".erl")),
EvilTwin = filename:join(zx_lib:path(tmp), filename:basename(Me ++ ".erl")),
ok = log(info, "Temporarily reconstructing ~tp as ~tp", [Me, EvilTwin]),
Sed = io_lib:format("sed 's/^#!.*$//' ~s > ~s", [Me, EvilTwin]),
ok = exec_shell(Sed),
ok = zx_lib:exec_shell(Sed),
ok = case dialyzer:run([{init_plt, PLT}, {from, src_code}, {files, [EvilTwin]}]) of
[] ->
io:format("Dialyzer found no errors and returned no warnings! Yay!~n");
@ -729,17 +731,14 @@ dialyze() ->
file:delete(EvilTwin).
-spec grow_a_pair() -> ok.
-spec grow_a_pair() -> zx:outcome().
%% @private
%% Execute the key generation procedure for 16k RSA keys once and then terminate.
grow_a_pair() ->
ok = file:set_cwd(zx_lib:zomp_dir()),
KeyID = zx_key:prompt_keygen(),
case zx_key:generate_rsa(KeyID) of
ok -> ok;
Error -> error_exit("grow_a_pair/0 error: ~tp", [Error], ?LINE)
end.
zx_key:generate_rsa(KeyID).
-spec drop_key(zx:key_id()) -> ok.
@ -749,9 +748,7 @@ grow_a_pair() ->
%% error exit value (this instruction is idempotent if used in shell scripts).
drop_key({Realm, KeyName}) ->
ok = file:set_cwd(zx_lib:zomp_dir()),
KeyGlob = KeyName ++ ".{key,pub},der",
Pattern = filename:join([zx_lib:zomp_dir(), "key", Realm, KeyGlob]),
Pattern = filename:join(zx_lib:path(key, Realm), KeyName ++ ".{key,pub}.der"),
case filelib:wildcard(Pattern) of
[] ->
log(warning, "Key ~ts/~ts not found", [Realm, KeyName]);
@ -981,7 +978,6 @@ create_realm(Realm, Address, Port, UserName, Email, RealName) ->
{realmname, RealName},
{contact_info, {"email", Email}},
{keys, [KeyName]}],
ok = make_realm_dirs(Realm),
RealmConfPath = filename:join(zx_lib:path(etc, Realm), "realm.conf"),
ok = zx_lib:write_terms(RealmConfPath, RealmConf),
UserConfPath = filename:join(zx_lib:path(etc, Realm), UserName ++ ".user"),
@ -1015,9 +1011,9 @@ create_realm(Realm, Address, Port, UserName, Email, RealName) ->
%% Checks for remnants of a realm.
realm_exists(Realm) ->
Dirs = [etc, var, tmp, log, key, zsp, lib],
Check = fun(D) -> filelib:is_file(zx_lib:path(D)) end,
Managed = lists:member(Realm, proplists:get_value(managed, zx_lib:read_sys_conf())),
Managed = lists:member(Realm, zx_sys_conf:managed(zx_sys_conf:load())),
Dirs = [zx_lib:path(D, Realm) || D <- [etc, var, tmp, log, key, zsp, lib]],
Check = fun(D) -> filelib:is_file(D) end,
Found = lists:any(Check, Dirs),
Managed or Found.
@ -1028,8 +1024,8 @@ realm_exists(Realm) ->
%% or be certain some other way that the file:make_sure/1 calls will succeed.
make_realm_dirs(Realm) ->
Dirs = [etc, var, tmp, log, key, zsp, lib],
Make = fun(D) -> ok = file:make_dir(zx_lib:path(D, Realm)) end,
Dirs = [zx_lib:path(D, Realm) || D <- [etc, var, tmp, log, key, zsp, lib]],
Make = fun(D) -> ok = file:make_dir(D) end,
lists:foreach(Make, Dirs).
@ -1061,22 +1057,6 @@ create_realmfile(Realm, Dir) ->
log(info, "Realm conf file written to ~ts", [ZRF]).
-spec add_realm(Path, Data) -> ok
when Path :: file:filename(),
Data :: binary().
add_realm(Path, Data) ->
case erl_tar:extract({binary, Data}, [compressed, {cwd, zx_lib:zomp_dir()}]) of
ok ->
{Realm, _} = string:take(filename:basename(Path), ".", true),
log(info, "Realm ~ts is now visible to this system.", [Realm]);
{error, invalid_tar_checksum} ->
error_exit("~ts is not a valid realm file.", [Path], ?LINE);
{error, eof} ->
error_exit("~ts is not a valid realm file.", [Path], ?LINE)
end.
-spec drop_realm(zx:realm()) -> ok.
drop_realm(Realm) ->
@ -1108,9 +1088,9 @@ drop_realm(Realm) ->
%% the realm exists, of course.
takeover(Realm) ->
SysConf = zx_conf_sys:load(),
case zx_conf_sys:add_managed(Realm, SysConf) of
{ok, NewConf} -> zx_conf_sys:save(NewConf);
SysConf = zx_sys_conf:load(),
case zx_sys_conf:add_managed(Realm, SysConf) of
{ok, NewConf} -> zx_sys_conf:save(NewConf);
{error, unconfigured} -> log(error, "Cannot take over an unconfigured realm.")
end.
@ -1118,34 +1098,8 @@ takeover(Realm) ->
-spec abdicate(zx:realm()) -> ok.
abdicate(Realm) ->
SysConf = zx_conf_sys:load(),
case zx_conf_sys:rem_managed(Realm, SysConf) of
{ok, NewConf} -> zx_conf_sys:save(NewConf);
SysConf = zx_sys_conf:load(),
case zx_sys_conf:rem_managed(Realm, SysConf) of
{ok, NewConf} -> zx_sys_conf:save(NewConf);
{error, unmanaged} -> log(error, "Cannot abdicate an unmanaged realm.")
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).

View File

@ -1,5 +1,5 @@
%%% @doc
%%% zx_conf_sys: An interface to etc/sys.conf
%%% zx_sys_conf: An interface to etc/sys.conf
%%%
%%% It may seem overkill to write an interface module for a config file that only tracks
%%% five things, but scattering this all around the project is just a bit too l33t for
@ -13,7 +13,7 @@
%%% Bad configuration data causes a reset to defaults so that the system can function.
%%% @end
-module(zx_conf_sys).
-module(zx_sys_conf).
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
-license("GPL-3.0").