From 98f4b6bdf39dd518c060d606eca73d38f76c6d75 Mon Sep 17 00:00:00 2001 From: Craig Everett Date: Tue, 29 May 2018 22:16:11 +0900 Subject: [PATCH] So much foo --- .gitignore | 1 + zomp/lib/otpr/zx/0.1.0/src/zx.erl | 1385 +++-------------------- zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl | 50 +- zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl | 12 +- zomp/lib/otpr/zx/0.1.0/src/zx_key.erl | 38 +- zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl | 78 +- zomp/lib/otpr/zx/0.1.0/src/zx_local.erl | 1177 +++++++++++++++++++ zomp/zx | 2 +- zomp/zx.cmd | 2 +- zx_dev | 10 +- 10 files changed, 1501 insertions(+), 1254 deletions(-) create mode 100644 zomp/lib/otpr/zx/0.1.0/src/zx_local.erl diff --git a/.gitignore b/.gitignore index bbcd1d5..aa7b2ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .eunit deps +tester *.o *.beam *.plt diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx.erl b/zomp/lib/otpr/zx/0.1.0/src/zx.erl index 977b688..1d9fd9c 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx.erl @@ -19,10 +19,10 @@ -license("GPL-3.0"). --export([start/1]). +-export([run/0, run/1]). -export([subscribe/1, unsubscribe/0]). -export([start/2, stop/1, stop/0]). --export([error_exit/2, error_exit/3]). +-export([usage_exit/1]). -export_type([serial/0, package_id/0, package/0, realm/0, name/0, version/0, identifier/0, @@ -69,99 +69,130 @@ %%% Command Dispatch --spec start(Args) -> no_return() +-spec run() -> no_return(). + +run() -> + run([]). + + +-spec run(Args) -> no_return() when Args :: [string()]. %% Dispatch work functions based on the nature of the input arguments. -start(["help"]) -> +run(["help"]) -> usage_exit(0); -start(["run", PackageString | Args]) -> +run(["run", PackageString | Args]) -> ok = start(), run(PackageString, Args); -start(["runlocal" | ArgV]) -> +run(["runlocal" | ArgV]) -> ok = start(), run_local(ArgV); -start(["init", "app", PackageString]) -> - initialize(app, PackageString); -start(["init", "lib", PackageString]) -> - initialize(lib, PackageString); -start(["install", PackageFile]) -> +run(["init", "app", PackageString]) -> + ok = compatibility_check([unix]), + zx_local:initialize(app, PackageString); +run(["init", "lib", PackageString]) -> + ok = compatibility_check([unix]), + zx_local:initialize(lib, PackageString); +run(["install", PackageFile]) -> + zx_local:assimilate(PackageFile); +run(["set", "dep", PackageString]) -> + zx_local:set_dep(PackageString); +run(["set", "version", VersionString]) -> + ok = compatibility_check([unix]), + zx_local:set_version(VersionString); +run(["verup", Level]) -> + ok = compatibility_check([unix]), + zx_local:verup(Level); +run(["list", "realms"]) -> + zx_loca:list_realms(); +run(["list", "packages", Realm]) -> ok = start(), - assimilate(PackageFile); -start(["set", "dep", PackageString]) -> - set_dep(PackageString); -start(["set", "version", VersionString]) -> - set_version(VersionString); -start(["list", "realms"]) -> - list_realms(); -start(["list", "packages", Realm]) -> + zx_local:list_packages(Realm); +run(["list", "versions", PackageName]) -> ok = start(), - list_packages(Realm); -start(["list", "versions", PackageName]) -> - ok = start(), - list_versions(PackageName); -start(["add", "realm", RealmFile]) -> - add_realm(RealmFile); -start(["drop", "dep", PackageString]) -> + zx_local:list_versions(PackageName); +run(["add", "realm", RealmFile]) -> + zx_local:add_realm(RealmFile); +run(["drop", "dep", PackageString]) -> PackageID = zx_lib:package_id(PackageString), - drop_dep(PackageID); -start(["drop", "key", KeyID]) -> - drop_key(KeyID); -start(["drop", "realm", Realm]) -> - drop_realm(Realm); -start(["verup", Level]) -> - verup(Level); -start(["package"]) -> + zx_local:drop_dep(PackageID); +run(["drop", "key", Realm, KeyName]) -> + zx_key:drop({Realm, KeyName}); +run(["drop", "realm", Realm]) -> + zx_local:drop_realm(Realm); +run(["package"]) -> {ok, TargetDir} = file:get_cwd(), - package(TargetDir); -start(["package", TargetDir]) -> + zx_local:package(TargetDir); +run(["package", TargetDir]) -> case filelib:is_dir(TargetDir) of true -> - package(TargetDir); + zx_local:package(TargetDir); false -> ok = log(error, "Target directory ~tp does not exist!", [TargetDir]), halt(22) end; -start(["dialyze"]) -> - dialyze(); -start(["create", "user", Realm, Name]) -> - create_user(Realm, Name); -start(["create", "keypair"]) -> - zx_key:create_keypair(); -start(["create", "plt"]) -> - create_plt(); -start(["create", "realm"]) -> - create_realm(); -start(["create", "realmfile", Realm]) -> - create_realmfile(Realm); -start(["list", "pending", PackageName]) -> +run(["dialyze"]) -> + zx_local:dialyze(); +run(["create", "user", Realm, Name]) -> + zx_local:create_user(Realm, Name); +run(["create", "keypair"]) -> + zx_key:grow_a_pair(); +run(["create", "plt"]) -> + zx_local:create_plt(); +run(["create", "realm"]) -> + zx_local:create_realm(); +run(["create", "realmfile", Realm]) -> + zx_local:create_realmfile(Realm); +run(["list", "pending", PackageName]) -> zx_auth:list_pending(PackageName); -start(["list", "resigns", Realm]) -> +run(["list", "resigns", Realm]) -> zx_auth:list_resigns(Realm); -start(["submit", PackageFile]) -> +run(["submit", PackageFile]) -> zx_auth:submit(PackageFile); -start(["review", PackageString]) -> +run(["review", PackageString]) -> zx_auth:review(PackageString); -start(["approve", PackageString]) -> +run(["approve", PackageString]) -> PackageID = zx_lib:package_id(PackageString), zx_auth:approve(PackageID); -start(["reject", PackageString]) -> +run(["reject", PackageString]) -> PackageID = zx_lib:package_id(PackageString), zx_auth:reject(PackageID); -start(["accept", PackageString]) -> +run(["accept", PackageString]) -> zx_auth:accept(PackageString); -start(["add", "packager", Package, UserName]) -> +run(["add", "packager", Package, UserName]) -> zx_auth:add_packager(Package, UserName); -start(["add", "maintainer", Package, UserName]) -> +run(["add", "maintainer", Package, UserName]) -> zx_auth:add_maintainer(Package, UserName); -start(["add", "sysop", Package, UserName]) -> +run(["add", "sysop", Package, UserName]) -> zx_auth:add_sysop(Package, UserName); -start(["add", "package", PackageName]) -> +run(["add", "package", PackageName]) -> zx_auth:add_package(PackageName); -start(_) -> +run(_) -> usage_exit(22). +-spec compatibility_check(Platforms) -> ok | no_return() + when Platforms :: unix | win32. +%% @private +%% Some commands only work on specific platforms because they leverage some specific +%% aspect on that platform, but not common to all. ATM this is mostly developer +%% commands that leverage things universal to *nix/posix shells but not Windows. +%% If equivalent procedures are written in Erlang then these restrictions can be +%% avoided -- but it is unclear whether there are any Erlang developers even using +%% Windows, so for now this is the bad version of the solution. + +compatibility_check(Platforms) -> + {Family, Name} = os:type(), + case lists:member(Family, Platforms) of + true -> + ok; + false -> + Message = "Unfortunately this command is not available on ~tp ~tp", + ok = log(error, Message, [Family, Name]), + halt(0) + end. + + %%% Application Start/Stop @@ -299,9 +330,9 @@ run(Identifier, RunArgs) -> %% and use zx commands to add or drop dependencies made available via zomp. run_local(RunArgs) -> - Meta = zx_lib:read_project_meta(), + {ok, Meta} = zx_lib:read_project_meta(), PackageID = maps:get(package_id, Meta), - ok = build(), + ok = zx_lib:build(), {ok, Dir} = file:get_cwd(), ok = file:set_cwd(zx_lib:zomp_dir()), ok = start(), @@ -388,154 +419,6 @@ end. -%%% Project initialization - --spec initialize(Type, PackageString) -> no_return() - when Type :: app | lib, - PackageString :: string(). -%% @private -%% Initialize an application in the local directory based on the PackageID provided. -%% This function does not care about the name of the current directory and leaves -%% providing a complete, proper and accurate PackageID. -%% This function will check the current `lib/' directory for zomp-style dependencies. -%% If this is not the intended function or if there are non-compliant directory names -%% in `lib/' then the project will need to be rearranged to become zomp compliant or -%% the `deps' section of the resulting meta file will need to be manually updated. - -initialize(Type, PackageString) -> - PackageID = - case zx_lib:package_id(PackageString) of - {ok, ID} -> - ID; - {error, invalid_package_string} -> - error_exit("Invalid package string.", ?LINE) - end, - ok = log(info, "Initializing ~s...", [PackageString]), - MetaList = - [{package_id, PackageID}, - {deps, []}, - {type, Type}], - Meta = maps:from_list(MetaList), - ok = zx_lib:write_project_meta(Meta), - ok = log(info, "Project ~tp initialized.", [PackageString]), - Message = - "NOTICE:~n" - " This project is currently listed as having no dependencies.~n" - " If this is not true then run `zx set dep DepID` for each current dependency.~n" - " (run `zx help` for more information on usage)~n", - ok = io:format(Message), - halt(0). - - - -%%% Add a package from a local file - --spec assimilate(PackageFile) -> PackageID - when PackageFile :: file:filename(), - PackageID :: package_id(). -%% @private -%% Receives a path to a file containing package data, examines it, and copies it to a -%% canonical location under a canonical name, returning the PackageID of the package -%% contents. - -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), - ok = - case public_key:verify(TgzData, sha512, Signature, PubKey) of - true -> - ZrpPath = filename:join("zsp", zx_lib:namify_zsp(PackageID)), - file:copy(PackageFile, ZrpPath); - false -> - error_exit("Bad package signature: ~ts", [PackageFile], ?LINE) - end, - ok = file:set_cwd(CWD), - Message = "~ts is now locally available.", - {ok, PackageString} = zx_lib:package_string(PackageID), - ok = log(info, Message, [PackageString]), - halt(0). - - - -%%% Set dependency - --spec set_dep(Identifier :: string()) -> no_return(). -%% @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 -%% reflect that in the PackageString argument. The AppString is permitted to be -%% incomplete. Incomplete elements of the VersionString (if included) will default to -%% the latest version available at the indicated level. - -set_dep(Identifier) -> - {ok, {Realm, Name, FuzzyVersion}} = zx_lib:package_id(Identifier), - Version = - case FuzzyVersion of - {z, z, z} -> - ok = start(), - {ok, V} = zx_daemon:query_latest({Realm, Name}), - V; - {X, Y, Z} when is_integer(X), is_integer(Y), is_integer(Z) -> - {X, Y, Z}; - _ -> - ok = start(), - {ok, V} = zx_daemon:query_latest({Realm, Name, FuzzyVersion}), - V - end, - set_dep({Realm, Name}, Version). - - -set_dep({Realm, Name}, Version) -> - PackageID = {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) - end. - - --spec set_dep(PackageID, Deps, Meta) -> no_return() - when PackageID :: package_id(), - Deps :: [package_id()], - Meta :: [term()]. -%% @private -%% Given the PackageID, list of Deps and the current contents of the project Meta, add -%% or update Deps to include (or update) Deps to reflect a dependency on PackageID, if -%% 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) -> - ExistingPackageIDs = fun({R, N, _}) -> {R, N} == {Realm, Name} end, - NewDeps = - case lists:partition(ExistingPackageIDs, Deps) of - {[{Realm, Name, OldVersion}], Rest} -> - Message = "Updating dep ~ts to ~ts", - {ok, OldPS} = zx_lib:package_string({Realm, Name, OldVersion}), - {ok, NewPS} = zx_lib:package_string({Realm, Name, NewVersion}), - ok = log(info, Message, [OldPS, NewPS]), - [PackageID | Rest]; - {[], Deps} -> - {ok, PackageString} = zx_lib:package_string(PackageID), - ok = log(info, "Adding dep ~ts", [PackageString]), - [PackageID | Deps] - end, - NewMeta = maps:put(deps, NewDeps, Meta), - ok = zx_lib:write_project_meta(NewMeta), - halt(0). - -spec ensure_installed(PackageID) -> Result | no_return() when PackageID :: package_id(), @@ -605,887 +488,6 @@ tuplize(String, Acc) -> -%%% Set version - --spec set_version(VersionString) -> no_return() - 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, - {ok, Meta} = zx_lib:read_project_meta(), - {Realm, Name, OldVersion} = maps:get(package_id, Meta), - update_version(Realm, Name, OldVersion, NewVersion, Meta). - - --spec update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> no_return() - when Realm :: realm(), - Name :: name(), - OldVersion :: version(), - NewVersion :: version(), - OldMeta :: package_meta(). -%% @private -%% Update a project's `zomp.meta' file by either incrementing the indicated component, -%% or setting the version number to the one specified in VersionString. -%% This part of the procedure updates the meta and does the final write, if the write -%% turns out to be possible. If successful it will indicate to the user what was -%% changed. - -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), - OldVS = zx_lib:version_to_string(OldVersion), - NewVS = zx_lib:version_to_string(NewVersion), - ok = log(info, "Version changed from ~s to ~s.", [OldVS, NewVS]), - halt(0). - - - -%%% List Functions - --spec list_realms() -> no_return(). -%% @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_realms() -> - ok = lists:foreach(fun(R) -> io:format("~ts~n", [R]) end, zx_lib:list_realms()), - halt(0). - - --spec list_packages(realm()) -> no_return(). -%% @private -%% Contact the indicated realm and query it for a list of registered packages and print -%% them to stdout. - -list_packages(Realm) -> - ok = start(), - case zx_daemon:list_packages(Realm) of - {ok, []} -> - ok = log(info, "Realm ~tp has no packages available.", [Realm]), - halt(0); - {ok, Packages} -> - Print = fun({R, N}) -> io:format("~ts-~ts~n", [R, N]) end, - ok = lists:foreach(Print, Packages), - halt(0); - {error, bad_realm} -> - error_exit("Bad realm name.", ?LINE); - {error, no_realm} -> - error_exit("Realm \"~ts\" is not configured.", ?LINE); - {error, network} -> - Message = "Network issues are preventing connection to the realm.", - error_exit(Message, ?LINE) - end. - - --spec list_versions(PackageName :: string()) -> no_return(). -%% @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}; - {error, invalid_package_string} -> - error_exit("~tp is not a valid package name.", [PackageName], ?LINE) - end, - ok = start(), - case zx_daemon:list_versions(Package) of - {ok, []} -> - Message = "Package ~ts has no versions available.", - ok = log(info, Message, [PackageName]), - halt(0); - {ok, Versions} -> - Print = - fun(Version) -> - {ok, PackageString} = zx_lib:package_string({Realm, Name, Version}), - io:format("~ts~n", [PackageString]) - end, - ok = lists:foreach(Print, Versions), - halt(0); - {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) - end. - - - -%%% Add realm - --spec add_realm(Path) -> no_return() - 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. - -add_realm(Path) -> - case file:read_file(Path) of - {ok, Data} -> - 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); - {error, enoent} -> - ok = log(warning, "FAILED: ~ts does not exist.", [Path]), - halt(1); - {error, eisdir} -> - ok = log(warning, "FAILED: ~ts is a directory, not a realm file.", [Path]), - halt(1) - end. - - --spec add_realm(Path, Data) -> no_return() - 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), - ok = log(info, "Realm ~ts is now visible to this system.", [Realm]), - halt(0); - {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. - - - -%%% Drop dependency - --spec drop_dep(package_id()) -> no_return(). -%% @private -%% Remove the indicate dependency from the local project's zomp.meta record. - -drop_dep(PackageID) -> - {ok, PackageString} = zx_lib:package_string(PackageID), - {ok, Meta} = zx_lib:read_project_meta(), - Deps = maps:get(deps, Meta), - case lists:member(PackageID, Deps) of - true -> - NewDeps = lists:delete(PackageID, Deps), - NewMeta = maps:put(deps, NewDeps, Meta), - ok = zx_lib:write_project_meta(NewMeta), - Message = "~ts removed from dependencies.", - ok = log(info, Message, [PackageString]), - halt(0); - false -> - ok = log(info, "~ts not found in dependencies.", [PackageString]), - halt(0) - end. - - - -%%% Drop key - --spec drop_key(key_id()) -> no_return(). -%% @private -%% Given a KeyID, remove the related public and private keys from the keystore, if they -%% exist. If not, exit with a message that no keys were found, but do not return an -%% 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]), - case filelib:wildcard(Pattern) of - [] -> - ok = log(warning, "Key ~ts/~ts not found", [Realm, KeyName]), - halt(0); - Files -> - ok = lists:foreach(fun file:delete/1, Files), - ok = log(info, "Keyset ~ts/~ts removed", [Realm, KeyName]), - halt(0) - end. - - -%%% Drop realm - --spec drop_realm(realm()) -> no_return(). - -drop_realm(Realm) -> - ok = file:set_cwd(zx_lib:zomp_dir()), - RealmConf = zx_lib:realm_conf(Realm), - case filelib:is_regular(RealmConf) of - true -> - Message = - "~n" - " WARNING: Are you SURE you want to remove realm ~ts?~n" - " (Only \"Y\" will confirm this action.)~n", - ok = io:format(Message, [Realm]), - case zx_tty:get_input() of - "Y" -> - ok = file:delete(RealmConf), - ok = drop_prime(Realm), - ok = clear_keys(Realm), - ok = log(info, "All traces of realm ~ts have been removed."), - halt(0); - _ -> - ok = log(info, "Aborting."), - halt(0) - end; - false -> - ok = log(warning, "Realm conf ~ts not found.", [RealmConf]), - clear_keys(Realm) - end. - - --spec drop_prime(realm()) -> ok. - -drop_prime(Realm) -> - Path = "zomp.conf", - case file:consult(Path) of - {ok, Conf} -> - {managed, Primes} = lists:keyfind(managed, 1, Conf), - NewPrimes = lists:delete(Realm, Primes), - NewConf = lists:keystore(managed, 1, Primes, {managed, NewPrimes}), - ok = zx_lib:write_terms(Path, NewConf), - log(info, "Ensuring ~ts is not a prime in ~ts", [Realm, Path]); - {error, enoent} -> - ok - end. - - --spec clear_keys(realm()) -> ok. - -clear_keys(Realm) -> - KeyDir = filename:join([zx_lib:zomp_dir(), "key", Realm]), - case filelib:is_dir(KeyDir) of - true -> rm_rf(KeyDir); - false -> log(warning, "Keydir ~ts not found", [KeyDir]) - end. - - - -%%% Update version - --spec verup(Level) -> no_return() - when Level :: string(). -%% @private -%% Convert input string arguments to acceptable atoms for use in update_version/1. - -verup("major") -> version_up(major); -verup("minor") -> version_up(minor); -verup("patch") -> version_up(patch); -verup(_) -> usage_exit(22). - - --spec version_up(Level) -> no_return() - when Level :: major - | minor - | patch. -%% @private -%% Update a project's `zomp.meta' file by either incrementing the indicated component, -%% or setting the version number to the one specified in VersionString. -%% This part of the procedure guards for the case when the zomp.meta file cannot be -%% read for some reason. - -version_up(Arg) -> - {ok, Meta} = zx_lib:read_project_meta(), - PackageID = maps:get(package_id, Meta), - version_up(Arg, PackageID, Meta). - - --spec version_up(Level, PackageID, Meta) -> no_return() - when Level :: major - | minor - | patch - | version(), - PackageID :: package_id(), - Meta :: [{atom(), term()}]. -%% @private -%% Update a project's `zomp.meta' file by either incrementing the indicated component, -%% or setting the version number to the one specified in VersionString. -%% This part of the procedure does the actual update calculation, to include calling to -%% convert the VersionString (if it is passed) to a `version()' type and check its -%% validity (or halt if it is a bad string). - -version_up(major, {Realm, Name, OldVersion = {Major, _, _}}, OldMeta) -> - NewVersion = {Major + 1, 0, 0}, - update_version(Realm, Name, OldVersion, NewVersion, OldMeta); -version_up(minor, {Realm, Name, OldVersion = {Major, Minor, _}}, OldMeta) -> - NewVersion = {Major, Minor + 1, 0}, - update_version(Realm, Name, OldVersion, NewVersion, OldMeta); -version_up(patch, {Realm, Name, OldVersion = {Major, Minor, Patch}}, OldMeta) -> - NewVersion = {Major, Minor, Patch + 1}, - update_version(Realm, Name, OldVersion, NewVersion, OldMeta). - - - -%%% Package generation - --spec package(TargetDir) -> no_return() - when TargetDir :: file:filename(). -%% @private -%% Turn a target project directory into a package, prompting the user for appropriate -%% key selection or generation actions along the way. - -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]), - ok = zx_lib:force_dir(KeyDir), - Pattern = KeyDir ++ "/*.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, _, _} = zx_key:generate_rsa(KeyID), - package(KeyID, TargetDir); - [KeyName] -> - KeyID = {Realm, KeyName}, - ok = log(info, "Using key: ~ts/~ts", [Realm, KeyName]), - package(KeyID, TargetDir); - KeyNames -> - KeyName = zx_tty:select_string(KeyNames), - package({Realm, KeyName}, TargetDir) - end. - - --spec package(KeyID, TargetDir) -> no_return() - when KeyID :: key_id(), - TargetDir :: file:filename(). -%% @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. - -package(KeyID, TargetDir) -> - {ok, Meta} = zx_lib:read_project_meta(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), - ok = remove_binaries(TargetDir), - {ok, Everything} = file:list_dir(TargetDir), - DotFiles = filelib:wildcard(".*", TargetDir), - Ignores = ["lib" | DotFiles], - Targets = lists:subtract(Everything, Ignores), - {ok, CWD} = file:get_cwd(), - ok = file:set_cwd(TargetDir), - ok = 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), - {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). - - --spec remove_binaries(TargetDir) -> ok - when TargetDir :: file:filename(). -%% @private -%% Procedure to delete all .beam and .ez files from a given directory starting at -%% TargetDir. Called as part of the pre-packaging sanitization procedure. - -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. - - - -%%% Generate PLT - --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. -%% @private -%% Build a general plt file for Dialyzer based on the core Erland distro. -%% TODO: Make a per-package + dependencies version of this. - -build_plt() -> - PLT = default_plt(), - Template = - "dialyzer --build_plt" - " --output_plt ~ts" - " --apps asn1 reltool wx common_test crypto erts eunit inets" - " kernel mnesia public_key sasl ssh ssl stdlib", - Command = io_lib:format(Template, [PLT]), - Message = - "Generating PLT file and writing to: ~tp~n" - " There will be a list of \"unknown functions\" in the final output.~n" - " Don't panic. This is normal. Turtles all the way down, after all...", - ok = log(info, Message, [PLT]), - ok = log(info, "This may take a while. Patience is a virtue."), - Out = os:cmd(Command), - log(info, Out). - - --spec default_plt() -> file:filename(). - -default_plt() -> - filename:join(zx_lib:zomp_dir(), "basic.plt"). - - - -%%% Dialyze - --spec dialyze() -> no_return(). -%% @private -%% Preps a copy of this script for typechecking with Dialyzer. -%% TODO: Create a package_id() based version of this to handle dialyzation of complex -%% projects. - -dialyze() -> - PLT = default_plt(), - ok = - case filelib:is_regular(PLT) of - 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")), - ok = log(info, "Temporarily reconstructing ~tp as ~tp", [Me, EvilTwin]), - Sed = io_lib:format("sed 's/^#!.*$//' ~s > ~s", [Me, EvilTwin]), - "" = os:cmd(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"); - Warnings -> - Mine = [dialyzer:format_warning({Tag, {Me, Line}, Msg}) - || {Tag, {_, Line}, Msg} <- Warnings], - lists:foreach(fun io:format/1, Mine) - end, - ok = file:delete(EvilTwin), - halt(0). - - - -%%% Create Realm & Sysop - --spec create_user(realm(), user_name()) -> no_return(). -%% @private -%% Validate the realm and username provided, prompt the user to either select a keypair -%% to use or generate a new one, and bundle a .zuser file for conveyance of the user -%% data and his relevant keys (for import into an existing zomp server via `add' -%% command like "add packager", "add maintainer" and "add sysop". - -create_user(Realm, Username) -> - Message = "Would be generating a user file for {~160tp, ~160to}.", - ok = log(info, Message, [Realm, Username]), - halt(0). - - --spec create_realm() -> no_return(). -%% @private -%% Prompt the user to input the information necessary to create a new zomp realm, -%% package the data appropriately for the server and deliver the final keys and -%% realm file to the user. - -create_realm() -> - Instructions = - "~n" - " Enter a name for your new realm.~n" - " Names can contain only lower-case letters, numbers and the underscore.~n" - " Names must begin with a lower-case letter.~n", - ok = io:format(Instructions), - Realm = zx_tty:get_input(), - case zx_lib:valid_lower0_9(Realm) of - true -> - RealmFile = filename:join(zx_lib:zomp_dir(), Realm ++ ".realm"), - case filelib:is_regular(RealmFile) of - false -> - create_realm(Realm); - true -> - ok = io:format("That realm already exists. Be more original.~n"), - create_realm() - end; - false -> - ok = io:format("Bad realm name \"~ts\". Try again.~n", [Realm]), - create_realm() - end. - - --spec create_realm(Realm) -> no_return() - when Realm :: realm(). - -create_realm(Realm) -> - ExAddress = prompt_external_address(), - create_realm(Realm, ExAddress). - - --spec prompt_external_address() -> Result - when Result :: inet:hostname() | inet:ip_address(). - -prompt_external_address() -> - Message = external_address_prompt(), - ok = io:format(Message), - case zx_tty:get_input() of - "" -> - ok = io:format("You need to enter an address.~n"), - prompt_external_address(); - String -> - parse_address(String) - end. - - --spec external_address_prompt() -> string(). - -external_address_prompt() -> - "~n" - " Enter a static, valid hostname or IPv4 or IPv6 address at which this host " - "can be reached from the public internet (or internal network if it will never " - "need to be reached from the internet).~n" - " DO NOT INCLUDE A PORT NUMBER IN THIS STEP~n". - - --spec parse_address(string()) -> inet:hostname() | inet:ip_address(). - -parse_address(String) -> - case inet:parse_address(String) of - {ok, Address} -> Address; - {error, einval} -> String - end. - - --spec create_realm(Realm, ExAddress) -> no_return() - when Realm :: realm(), - ExAddress :: inet:hostname() | inet:ip_address(). - -create_realm(Realm, ExAddress) -> - Message = - "~n" - " Enter the public (external) port number at which this service should be " - "available. (This might be different from the local port number if you are " - "forwarding ports or have a complex network layout.)~n", - ok = io:format(Message), - ExPort = prompt_port_number(11311), - create_realm(Realm, ExAddress, ExPort). - - --spec create_realm(Realm, ExAddress, ExPort) -> no_return() - when Realm :: realm(), - ExAddress :: inet:hostname() | inet:ip_address(), - ExPort :: inet:port_number(). - -create_realm(Realm, ExAddress, ExPort) -> - Message = - "~n" - " Enter the local (internal/LAN) port number at which this service should be " - "available. (This might be different from the public port visible from the " - "internet if you are port forwarding or have a complex network layout.)~n", - ok = io:format(Message), - InPort = prompt_port_number(11311), - create_realm(Realm, ExAddress, ExPort, InPort). - - --spec prompt_port_number(Current) -> Result - when Current :: inet:port_number(), - Result :: inet:port_number(). - -prompt_port_number(Current) -> - Instructions = - " A valid port is any number from 1 to 65535." - " [Press enter to accept the current setting: ~tw]~n", - ok = io:format(Instructions, [Current]), - case zx_tty:get_input() of - "" -> - Current; - S -> - try - case list_to_integer(S) of - Port when 16#ffff >= Port, Port > 0 -> - Port; - Illegal -> - Whoops = "Whoops! ~tw is out of bounds (1~65535). Try again.~n", - ok = io:format(Whoops, [Illegal]), - prompt_port_number(Current) - end - catch error:badarg -> - ok = io:format("~tp is not a port number. Try again...", [S]), - prompt_port_number(Current) - end - end. - - --spec create_realm(Realm, ExAddress, ExPort, InPort) -> no_return() - when Realm :: realm(), - ExAddress :: inet:hostname() | inet:ip_address(), - ExPort :: inet:port_number(), - InPort :: inet:port_number(). - -create_realm(Realm, ExAddress, ExPort, InPort) -> - Instructions = - "~n" - " Enter a username for the realm sysop.~n" - " Names can contain only lower-case letters, numbers and the underscore.~n" - " Names must begin with a lower-case letter.~n", - ok = io:format(Instructions), - UserName = zx_tty:get_input(), - case zx_lib:valid_lower0_9(UserName) of - true -> - create_realm(Realm, ExAddress, ExPort, InPort, UserName); - false -> - ok = io:format("Bad username ~tp. Try again.~n", [UserName]), - create_realm(Realm, ExAddress, ExPort, InPort) - end. - - --spec create_realm(Realm, ExAddress, ExPort, InPort, UserName) -> no_return() - when Realm :: realm(), - ExAddress :: inet:hostname() | inet:ip_address(), - ExPort :: inet:port_number(), - InPort :: inet:port_number(), - UserName :: string(). - -create_realm(Realm, ExAddress, ExPort, InPort, UserName) -> - Instructions = - "~n" - " Enter an email address for the realm sysop.~n" - " Valid email address rules apply though the checking done here is quite " - "minimal. Check the address you enter carefully. The only people who will " - "suffer from an invalid address are your users.~n", - ok = io:format(Instructions), - Email = zx_tty:get_input(), - [User, Host] = string:lexemes(Email, "@"), - case {zx_lib:valid_lower0_9(User), zx_lib:valid_label(Host)} of - {true, true} -> - create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email); - {false, true} -> - Message = "The user part of the email address seems invalid. Try again.~n", - ok = io:format(Message), - create_realm(Realm, ExAddress, ExPort, InPort, UserName); - {true, false} -> - Message = "The host part of the email address seems invalid. Try again.~n", - ok = io:format(Message), - create_realm(Realm, ExAddress, ExPort, InPort, UserName); - {false, false} -> - Message = "This email address seems like its totally bonkers. Try again.~n", - ok = io:format(Message), - create_realm(Realm, ExAddress, ExPort, InPort, UserName) - end. - - --spec create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email) -> - no_return() - when Realm :: realm(), - ExAddress :: inet:hostname() | inet:ip_address(), - ExPort :: inet:port_number(), - InPort :: inet:port_number(), - UserName :: string(), - Email :: string(). - -create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email) -> - Instructions = - "~n" - " Enter the real name (or whatever name people recognize) for the sysop.~n" - " There are no rules for this one. Any valid UTF-8 printables are legal.~n", - ok = io:format(Instructions), - RealName = zx_tty:get_input(), - create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email, RealName). - - --spec create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email, RealName) -> no_return() - when Realm :: realm(), - ExAddress :: inet:hostname() | inet:ip_address(), - ExPort :: inet:port_number(), - InPort :: inet:port_number(), - UserName :: string(), - Email :: string(), - RealName :: string(). - -create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email, RealName) -> - ok = io:format("~nGenerating keys. This might take a while, so settle in...~n"), - {ok, RealmKey, RealmPub} = zx_key:generate_rsa({Realm, Realm ++ ".1.realm"}), - {ok, PackageKey, PackagePub} = zx_key:generate_rsa({Realm, Realm ++ ".1.package"}), - {ok, SysopKey, SysopPub} = zx_key:generate_rsa({Realm, UserName ++ ".1"}), - ok = log(info, "Generated 16k RSA pair ~ts ~ts", [RealmKey, RealmPub]), - ok = log(info, "Generated 16k RSA pair ~ts ~ts", [PackageKey, PackagePub]), - ok = log(info, "Generated 16k RSA pair ~ts ~ts", [SysopKey, SysopPub]), - - Timestamp = calendar:now_to_universal_time(erlang:timestamp()), - - {ok, RealmPubData} = file:read_file(RealmPub), - RealmPubRecord = - {{Realm, filename:basename(RealmPub, ".pub.der")}, - realm, - {realm, Realm}, - crypto:hash(sha512, RealmPubData), - Timestamp}, - {ok, PackagePubData} = file:read_file(PackagePub), - PackagePubRecord = - {{Realm, filename:basename(PackagePub, ".pub.der")}, - package, - {realm, Realm}, - crypto:hash(sha512, PackagePubData), - Timestamp}, - UserRecord = - {{Realm, UserName}, - [filename:basename(SysopPub, ".pub.der")], - Email, - RealName}, - RealmSettings = - [{realm, Realm}, - {revision, 0}, - {prime, {ExAddress, ExPort}}, - {private, []}, - {mirrors, []}, - {sysops, [UserRecord]}, - {realm_keys, [RealmPubRecord]}, - {package_keys, [PackagePubRecord]}], - ZompSettings = - [{managed, [Realm]}, - {external_address, ExAddress}, - {external_port, ExPort}, - {internal_port, InPort}], - - {ok, CWD} = file:get_cwd(), - {ok, TempDir} = mktemp_dir("zomp"), - ok = file:set_cwd(TempDir), - KeyDir = filename:join("key", Realm), - ok = filelib:ensure_dir(KeyDir), - ok = file:make_dir(KeyDir), - KeyCopy = - fun(K) -> - {ok, _} = file:copy(K, filename:join(KeyDir, filename:basename(K))), - ok - end, - - PublicZRF = filename:join(CWD, Realm ++ ".zrf"), - RealmFN = Realm ++ ".realm", - ok = zx_lib:write_terms(RealmFN, RealmSettings), - ok = KeyCopy(PackagePub), - ok = KeyCopy(RealmPub), - ok = erl_tar:create(PublicZRF, [RealmFN, "key"], [compressed]), - - PrimeZRF = filename:join(CWD, Realm ++ ".zpf"), - ok = KeyCopy(SysopPub), - ok = zx_lib:write_terms("zomp.conf", ZompSettings), - ok = erl_tar:create(PrimeZRF, [RealmFN, "zomp.conf", "key"], [compressed]), - - KeyBundle = filename:join(CWD, Realm ++ ".zkf"), - ok = lists:foreach(KeyCopy, [PackageKey, RealmKey, SysopKey]), - ok = erl_tar:create(KeyBundle, [KeyDir], [compressed]), - - ok = file:set_cwd(CWD), - ok = rm_rf(TempDir), - - Message = - "===========================================================================~n" - "DONE!~n" - "~n" - "The realm ~ts has been created and is accessible from the current system.~n" - "Three configuration bundles have been created in the current directory:~n" - "~n" - " 1. ~ts ~n" - "This is the PRIVATE realm file you will need to install on the realm's prime~n" - "node. It includes the your (the sysop's) public key.~n" - "~n" - " 2. ~ts ~n" - "This file is the PUBLIC realm file other zomp nodes and zx users will need~n" - "to access the realm. It does not include your (the sysop's) public key.~n" - "~n" - " 3. ~ts ~n" - "This is the bundle of ALL KEYS that are defined in this realm at the moment.~n" - "~n" - "Now you need to make copies of these three files and back them up.~n" - "~n" - "On the PRIME NODE you need to run `zx add realm ~ts` and follow the prompts~n" - "to cause it to begin serving that realm as prime. (Node restart required.)~n" - "~n" - "On all zx CLIENTS that want to access your new realm and on all subordinate~n" - "MIRROR NODES the command `zx add realm ~ts` will need to be run.~n" - "The method of public realm file distribution (~ts) is up to you.~n" - "~n" - "~n" - "Public & Private key installation (if you need to recover them or perform~n" - "sysop functions from another computer) is `zx add keybundle ~ts`.~n" - "===========================================================================~n", - Substitutions = - [Realm, - PrimeZRF, PublicZRF, KeyBundle, - PrimeZRF, - PublicZRF, PublicZRF, - KeyBundle], - ok = io:format(Message, Substitutions), - halt(0). - - --spec create_realmfile(realm()) -> no_return(). - -create_realmfile(Realm) -> - RealmConf = zx_lib:load_realm_conf(Realm), - ok = log(info, "Realm found, creating realm file..."), - {revision, Revision} = lists:keyfind(revision, 1, RealmConf), - {realm_keys, RealmKeys} = lists:keyfind(realm_keys, 1, RealmConf), - {package_keys, PackageKeys} = lists:keyfind(package_keys, 1, RealmConf), - RealmKeyIDs = [element(1, K) || K <- RealmKeys], - PackageKeyIDs = [element(1, K) || K <- PackageKeys], - create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs). - - --spec create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) -> ok - when Realm :: realm(), - Revision :: non_neg_integer(), - RealmKeyIDs :: [key_id()], - PackageKeyIDs :: [key_id()]. - -create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) -> - {ok, CWD} = file:get_cwd(), - ok = file:set_cwd(zx_lib:zomp_dir()), - KeyPath = fun({R, K}) -> filename:join(["key", R, K ++ ".pub.der"]) end, - RealmKeyPaths = lists:map(KeyPath, RealmKeyIDs), - PackageKeyPaths = lists:map(KeyPath, PackageKeyIDs), - Targets = [zx_lib:realm_conf(Realm) | RealmKeyPaths ++ PackageKeyPaths], - OutFile = filename:join(CWD, Realm ++ "." ++ integer_to_list(Revision) ++ ".zrf"), - ok = erl_tar:create(OutFile, Targets, [compressed]), - ok = log(info, "Realm conf file written to ~ts", [OutFile]), - halt(0). - - - %%% Package utilities @@ -1525,78 +527,13 @@ install(PackageID) -> build(PackageID) -> {ok, CWD} = file:get_cwd(), ok = file:set_cwd(zx_lib:package_dir(PackageID)), - ok = build(), + ok = zx_lib:build(), file:set_cwd(CWD). --spec build() -> ok. -%% @private -%% Run any local `zxmake' script needed by the project for non-Erlang code (if present), -%% then add the local `ebin/' directory to the runtime search path, and finally build -%% the Erlang part of the project with make:all/0 according to the local `Emakefile'. - -build() -> - ZxMake = "zxmake", - ok = - case filelib:is_regular(ZxMake) of - true -> - Out = os:cmd(ZxMake), - log(info, Out); - false -> - ok - end, - true = code:add_patha(filename:absname("ebin")), - up_to_date = make:all(), - ok. - - %%% Directory & File Management --spec mktemp_dir(Prefix) -> Result - when Prefix :: string(), - Result :: {ok, TempDir :: file:filename()} - | {error, Reason :: file:posix()}. - -mktemp_dir(Prefix) -> - Rand = integer_to_list(binary:decode_unsigned(crypto:strong_rand_bytes(8)), 36), - TempPath = filename:basedir(user_cache, Prefix), - TempDir = filename:join(TempPath, Rand), - Result1 = filelib:ensure_dir(TempDir), - Result2 = file:make_dir(TempDir), - case {Result1, Result2} of - {ok, ok} -> {ok, TempDir}; - {ok, Error} -> Error; - {Error, _} -> Error - end. - - --spec rm_rf(file:filename()) -> ok | {error, file:posix()}. -%% @private -%% Recursively remove files and directories. Equivalent to `rm -rf'. - -rm_rf(Path) -> - case filelib:is_dir(Path) of - true -> - Pattern = filename:join(Path, "**"), - Contents = lists:reverse(lists:sort(filelib:wildcard(Pattern))), - ok = lists:foreach(fun rm/1, Contents), - file:del_dir(Path); - false -> - file:delete(Path) - end. - - --spec rm(file:filename()) -> ok | {error, file:posix()}. -%% @private -%% An omnibus delete helper. - -rm(Path) -> - case filelib:is_dir(Path) of - true -> file:del_dir(Path); - false -> file:delete(Path) -end. - -spec ensure_package_dirs(package_id()) -> ok. %% @private @@ -1633,74 +570,82 @@ usage_exit(Code) -> %% Display the zx command line usage message. usage() -> - T = "~n" - "zx~n" - "~n" - "Usage: zx [command] [object] [args]~n" - "~n" - "Examples:~n" - " zx help~n" - " zx run PackageID [Args]~n" - " zx init Type PackageID~n" - " zx install PackageID~n" - " zx set dep PackageID~n" - " zx set version Version~n" - " zx list realms~n" - " zx list packages Realm~n" - " zx list versions PackageName~n" - " zx list pending PackageName~n" - " zx list resigns Realm~n" - " zx add realm RealmFile~n" - " zx add package PackageName~n" - " zx add packager PackageName~n" - " zx add maintainer PackageName~n" - " zx add sysop UserID~n" - " zx review PackageID~n" - " zx approve PackageID~n" - " zx reject PackageID~n" - " zx accept PackageID~n" - " zx drop dep PackageID~n" - " zx drop key Realm KeyName~n" - " zx drop realm Realm~n" - " zx verup Level~n" - " zx runlocal [Args]~n" - " zx package [Path]~n" - " zx submit Path~n" - " zx create user Realm Username~n" - " zx create keypair~n" - " zx create plt~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" - " Args :: Arguments to pass to the application~n" - " Type :: The project type: a standalone \"app\" or a \"lib\"~n" - " Version :: Version string X, X.Y, or X.Y.Z: \"1\", \"1.2\", \"1.2.3\"~n" - " RealmFile :: Path to a valid .zrf realm file~n" - " Realm :: The name of a realm as a string [:a-z:]~n" - " KeyName :: The prefix of a keypair to drop~n" - " Level :: The version level, one of \"major\", \"minor\", or \"patch\"~n" - " Path :: Path to a valid project directory or .zsp file~n" - "~n", + T = +"ZX usage: zx [command] [object] [args]~n" +"~n" +"User Actions:~n" +" zx help~n" +" zx run PackageID [Args]~n" +" zx runlocal [Args]~n" +" zx list realms~n" +" zx list packages Realm~n" +" zx list versions PackageID~n" +" zx latest PackageID~n" +" zx add realm RealmFile~n" +" zx drop realm Realm~n" +" zx install PackageID~n" +" zx logpath [Package [1-10]]~n" +" zx status~n" +" zx set timeout Value~n" +" zx set mirror Realm Host:Port~n" +"~n" +"Developer/Packager/Maintainer Actions:~n" +" zx create project [app | lib] PackageID~n" +" zx init Type PackageID~n" +" zx list deps [PackageID]~n" +" zx set dep PackageID~n" +" zx drop dep PackageID~n" +" zx verup Level~n" +" zx set version Version~n" +" zx update .app~n" +" zx create plt~n" +" zx dialyze~n" +" zx package Path~n" +" zx submit ZspFile~n" +" zx list pending PackageName~n" +" zx list resigns Realm~n" +" zx list packagers PackageName~n" +" zx list maintainers PackageName~n" +" zx list sysops Realm~n" +" zx review PackageID~n" +" zx approve PackageID~n" +" 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 Realm~n" +" zx create userfiles Realm UserName~n" +" zx create keypair Realm~n" +" zx export user UserID~n" +" zx import user ZdufFile~n" +"~n" +"Sysop Actions:~n" +" zx add user ZpufFile~n" +" zx add package PackageName~n" +" zx add packager PackageName UserID~n" +" zx add maintainer PackageName UserID~n" +" zx add sysop UserID~n" +" 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" +" Args :: Arguments to pass to the application~n" +" Type :: The project type: a standalone \"app\" or a \"lib\"~n" +" Version :: Version string X, X.Y, or X.Y.Z: \"1\", \"1.2\", \"1.2.3\"~n" +" RealmFile :: Path to a valid .zrf realm file~n" +" Realm :: The name of a realm as a string [:a-z:]~n" +" KeyName :: The prefix of a keypair to drop~n" +" Level :: The version level, one of \"major\", \"minor\", or \"patch\"~n" +" Path :: Path to a valid project directory or .zsp file~n", io:format(T). %%% 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()], diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl index e909189..767de5c 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_auth.erl @@ -37,7 +37,7 @@ list_pending(PackageName) -> {ok, {R, N, {z, z, z}}} -> {R, N}; {error, invalid_package_string} -> - zx:error_exit("~tp is not a valid package name.", [PackageName], ?LINE) + error_exit("~tp is not a valid package name.", [PackageName], ?LINE) end, case zx_daemon:list_pending(Package) of {ok, []} -> @@ -53,12 +53,12 @@ list_pending(PackageName) -> ok = lists:foreach(Print, Versions), halt(0); {error, bad_realm} -> - zx:error_exit("Bad realm name.", ?LINE); + error_exit("Bad realm name.", ?LINE); {error, bad_package} -> - zx:error_exit("Bad package name.", ?LINE); + error_exit("Bad package name.", ?LINE); {error, network} -> Message = "Network issues are preventing connection to the realm.", - zx:error_exit(Message, ?LINE) + error_exit(Message, ?LINE) end. @@ -82,12 +82,12 @@ list_resigns(Realm) -> ok = lists:foreach(Print, PackageIDs), halt(0); {error, bad_realm} -> - zx:error_exit("Bad realm name.", ?LINE); + error_exit("Bad realm name.", ?LINE); {error, no_realm} -> - zx:error_exit("Realm \"~ts\" is not configured.", ?LINE); + error_exit("Realm \"~ts\" is not configured.", ?LINE); {error, network} -> Message = "Network issues are preventing connection to the realm.", - zx:error_exit(Message, ?LINE) + error_exit(Message, ?LINE) end. @@ -220,7 +220,7 @@ add_package(PackageName) -> {ok, {Realm, Name, {z, z, z}}} -> add_package(Realm, Name); _ -> - zx:error_exit("~tp is not a valid package name.", [PackageName], ?LINE) + error_exit("~tp is not a valid package name.", [PackageName], ?LINE) end. @@ -263,14 +263,14 @@ recv_or_die(Socket) -> {ok, Response} -> {ok, Response}; {error, Reason} -> - zx:error_exit("Action failed with: ~tp", [Reason], ?LINE); + error_exit("Action failed with: ~tp", [Reason], ?LINE); Unexpected -> - zx:error_exit("Unexpected message: ~tp", [Unexpected], ?LINE) + error_exit("Unexpected message: ~tp", [Unexpected], ?LINE) end; {tcp_closed, Socket} -> - zx:error_exit("Lost connection to node unexpectedly.", ?LINE) + error_exit("Lost connection to node unexpectedly.", ?LINE) after 5000 -> - zx:error_exit("Connection timed out.", ?LINE) + error_exit("Connection timed out.", ?LINE) end. @@ -450,3 +450,29 @@ disconnect(Socket) -> Message = "Shutdown connection ~p failed with: ~p", log(warning, Message, [Socket, Error]) 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). diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl index a302a85..30f5ef6 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_conn.erl @@ -11,6 +11,9 @@ -copyright("Craig Everett "). -license("GPL-3.0"). +% FIXME +-compile(export_all). + -export([subscribe/2, unsubscribe/2, fetch/3, query/3]). -export([start/1, stop/1]). -export([start_link/1, init/2]). @@ -20,9 +23,9 @@ %%% Types --type incoming() :: ping - | {sub, Channel :: term(), Message :: term()} - | term(). +%-type incoming() :: ping +% | {sub, Channel :: term(), Message :: term()} +% | term(). %%% Interface @@ -250,7 +253,8 @@ handle_message(Socket, Bin) -> Invalid -> {ok, {Addr, Port}} = zomp:peername(Socket), Host = inet:ntoa(Addr), - ok = log(warning, "Invalid message from ~tp:~p: ", [Invalid]), + Warning = "Invalid message from ~s:~p: ~tp", + ok = log(warning, Warning, [Host, Port, Invalid]), ok = zx_net:disconnect(Socket), terminate() end. diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl index 2fc785d..e9e9ed7 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_key.erl @@ -13,7 +13,7 @@ -license("GPL-3.0"). -export([ensure_keypair/1, have_public_key/1, have_private_key/1, - prompt_keygen/0, create_keypair/0, generate_rsa/1, + prompt_keygen/0, grow_a_pair/0, generate_rsa/1, load/2, verify/3]). -include("zx_logger.hrl"). @@ -102,16 +102,16 @@ prompt_keygen() -> end. --spec create_keypair() -> no_return(). +-spec grow_a_pair() -> no_return(). %% @private %% Execute the key generation procedure for 16k RSA keys once and then terminate. -create_keypair() -> +grow_a_pair() -> ok = file:set_cwd(zx_lib:zomp_dir()), KeyID = prompt_keygen(), case generate_rsa(KeyID) of {ok, _, _} -> halt(0); - Error -> zx:error_exit("create_keypair/0 error: ~tp", [Error], ?LINE) + Error -> error_exit("grow_a_pair/0 error: ~tp", [Error], ?LINE) end. @@ -230,7 +230,7 @@ openssl() -> false -> ok = log(error, "OpenSSL could not be found in this system's PATH."), ok = log(error, "Install OpenSSL and then retry."), - zx:error_exit("Missing system dependenct: OpenSSL", ?LINE); + error_exit("Missing system dependenct: OpenSSL", ?LINE); Path -> log(info, "OpenSSL executable found at: ~ts", [Path]) end, @@ -276,5 +276,31 @@ load(Type, {Realm, KeyName}) -> verify(Data, Signature, PubKey) -> case public_key:verify(Data, sha512, Signature, PubKey) of true -> ok; - false -> zx:error_exit("Bad package signature!", ?LINE) + 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). diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl index 64ab93c..a95a618 100644 --- a/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl @@ -29,7 +29,9 @@ namify_zsp/1, namify_tgz/1, find_latest_compatible/2, installed/1, realm_conf/1, load_realm_conf/1, - extract_zsp_or_die/1, halt_if_exists/1]). + extract_zsp_or_die/1, halt_if_exists/1, + build/0, + rm_rf/1, rm/1]). -include("zx_logger.hrl"). @@ -203,7 +205,7 @@ read_project_meta(Dir) -> Path = filename:join(Dir, "zomp.meta"), case file:consult(Path) of {ok, Meta} -> - maps:from_list(Meta); + {ok, maps:from_list(Meta)}; Error -> ok = log(error, "Failed to open \"zomp.meta\" with ~tp", [Error]), ok = log(error, "Wrong directory?"), @@ -670,13 +672,13 @@ extract_zsp_or_die(FileName) -> Files; {error, {FileName, enoent}} -> Message = "Can't find file ~ts.", - zx:error_exit(Message, [FileName], ?LINE); + error_exit(Message, [FileName], ?LINE); {error, invalid_tar_checksum} -> Message = "~ts is not a valid zsp archive.", - zx:error_exit(Message, [FileName], ?LINE); + error_exit(Message, [FileName], ?LINE); {error, Reason} -> Message = "Extracting package file failed with: ~160tp.", - zx:error_exit(Message, [Reason], ?LINE) + error_exit(Message, [Reason], ?LINE) end. @@ -687,6 +689,70 @@ extract_zsp_or_die(FileName) -> halt_if_exists(Path) -> case filelib:is_file(Path) of - true -> zx:error_exit("~ts already exists! Halting.", [Path], ?LINE); + 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), +%% then add the local `ebin/' directory to the runtime search path, and finally build +%% the Erlang part of the project with make:all/0 according to the local `Emakefile'. + +build() -> + ZxMake = "zxmake", + ok = + case filelib:is_regular(ZxMake) of + true -> + Out = os:cmd(ZxMake), + log(info, Out); + false -> + ok + end, + true = code:add_patha(filename:absname("ebin")), + up_to_date = make:all(), + ok. + + +-spec rm_rf(file:filename()) -> ok | {error, file:posix()}. +%% @private +%% Recursively remove files and directories. Equivalent to `rm -rf'. + +rm_rf(Path) -> + case filelib:is_dir(Path) of + true -> + Pattern = filename:join(Path, "**"), + Contents = lists:reverse(lists:sort(filelib:wildcard(Pattern))), + ok = lists:foreach(fun rm/1, Contents), + file:del_dir(Path); + false -> + file:delete(Path) + end. + + +-spec rm(file:filename()) -> ok | {error, file:posix()}. +%% @private +%% An omnibus delete helper. + +rm(Path) -> + case filelib:is_dir(Path) of + true -> file:del_dir(Path); + false -> file:delete(Path) + 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). diff --git a/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl b/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl new file mode 100644 index 0000000..e73c872 --- /dev/null +++ b/zomp/lib/otpr/zx/0.1.0/src/zx_local.erl @@ -0,0 +1,1177 @@ +%%% @doc +%%% ZX Local +%%% +%%% This module defines procedures that affect the local host and terminate on +%%% completion. +%%% @end + +-module(zx_local). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0"). + +-export([initialize/2, assimilate/1, set_dep/1, set_version/1, + list_realms/0, list_packages/1, list_versions/1, add_realm/1, + drop_dep/1, drop_key/1, drop_realm/1, verup/1, package/1, + create_plt/0, dialyze/0, + create_user/2, create_realm/0, create_realmfile/1]). + +-include("zx_logger.hrl"). + + + +%%% Functions + +-spec initialize(Type, PackageString) -> no_return() + when Type :: app | lib, + PackageString :: string(). +%% @private +%% Initialize an application in the local directory based on the PackageID provided. +%% This function does not care about the name of the current directory and leaves +%% providing a complete, proper and accurate PackageID. +%% This function will check the current `lib/' directory for zomp-style dependencies. +%% If this is not the intended function or if there are non-compliant directory names +%% in `lib/' then the project will need to be rearranged to become zomp compliant or +%% the `deps' section of the resulting meta file will need to be manually updated. + +initialize(Type, RawPackageString) -> + ok = + case filelib:is_file("zomp.meta") of + false -> ok; + true -> error_exit("This project is already Zompified.", ?LINE) + end, + PackageID = + case zx_lib:package_id(RawPackageString) of + {ok, {R, N, {z, z, z}}} -> + {R, N, {0, 1, 0}}; + {ok, {R, N, {X, z, z}}} -> + {R, N, {X, 0, 0}}; + {ok, {R, N, {X, Y, z}}} -> + {R, N, {X, Y, 0}}; + {ok, ID} -> + ID; + {error, invalid_package_string} -> + error_exit("Invalid package string: ~tp", [RawPackageString], ?LINE) + end, + {ok, PackageString} = zx_lib:package_string(PackageID), + ok = check_package_conflict(PackageID, PackageString), + ok = log(info, "Initializing ~s...", [PackageString]), + Prefix = solicit_prefix(), + ok = update_source_vsn(element(3, PackageID)), + ok = update_app_file(PackageID), + MetaList = + [{package_id, PackageID}, + {deps, []}, + {type, Type}, + {prefix, Prefix}], + Meta = maps:from_list(MetaList), + ok = zx_lib:write_project_meta(Meta), + ok = log(info, "Project ~tp initialized.", [PackageString]), + Message = + "~nNOTICE:~n" + " This project is currently listed as having no dependencies.~n" + " If this is not true then run `zx set dep DepID` for each current dependency.~n" + " (run `zx help` for more information on usage)~n", + ok = io:format(Message), + halt(0). + + +-spec check_package_conflict(zx:package_id(), string()) -> ok. +%% @private +%% Check the realm's upstream for the existence of a package that already has the same +%% name, or the name of any modules in src/ and report them to the user. Give the user +%% a chance to change their package's name or ignore the conflict, and report all module +%% naming conflicts. + +check_package_conflict(_PackageID, _PackageString) -> + log(info, "TODO: This is where the intended realm is checked for conflicts " + "and the user is given a chance to rename or ignore the conflict. " + "This check will have to wait for the network protocol fix."). + + +-spec solicit_prefix() -> ok. +%% @private +%% Most Erlang projects outside of the core distribution evolve a prefix of some sort +%% to namespace their work from other projects (or deliberately mimic another project's +%% prefix in the case that a new project is meant to augment, but not alter, an already +%% existing one). +%% A prefix is decided upon or disregarded here and stored in the meta file. The user +%% is given the option to update module names now (to include modifying call sites +%% in project code automatically -- while providing ample warnings about breaking +%% external code). +%% Just like with check_package_conflict/2, this procedure gives the user a chance to +%% ignore all warnings and just go. + +solicit_prefix() -> + ok = log(info, "Will solicit a prefix here. Just returning \"zz\" right now. Hah!"), + "zz". + + +-spec update_source_vsn(zx:version()) -> ok. +%% @private +%% Use grep to tell us which files have a `-vsn' attribute and which don't. +%% Use sed to insert a line after `-module' in the files that don't have it, +%% and update the files that do. +%% This procedure is still halfway into "works for me" territory. It will take a bit +%% of survey data to know which platforms can't work with some form of something +%% in here by default (bash may need to be called explicitly, for example, or the +%% old backtick executor terms used, etc). + +update_source_vsn(Version) -> + {ok, VersionString} = zx_lib:version_to_string(Version), + AddF = "sed -i 's/^-module(.*)\\.$/&\\n-vsn(\"~s\")./' $(grep -L '^-vsn(' src/*)", + 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), + log(info, "Source version attributes set"). + + +-spec update_app_file(zx:package_id()) -> ok. +%% @private +%% Update the app file or create it if it is missing. +%% TODO: If the app file is missing, interpret the src/*app.src file correctly. +%% Should really pull a few pages out of the rebar/erland.mk books on this, +%% as they've done all the hard pioneering work already. +%% TODO: Interactively determine whether the main module is the name, or the prefix +%% is the name, or the name is the prefix, or the name is the description, etc. +%% before writing anything out. + +update_app_file({_, Name, Version}) -> + {ok, VersionString} = zx_lib:version_to_string(Version), + AppName = list_to_atom(Name), + AppFile = filename:join("ebin", Name ++ ".app"), + {application, AppName, RawAppData} = + case filelib:is_regular(AppFile) of + true -> + {ok, [D]} = file:consult(AppFile), + D; + false -> + {application, + AppName, + [{registered, []}, + {included_applications, []}, + {applications, [stdlib, kernel]}]} + end, + Grep = "grep -oP '^-module\\(\\K[^)]+' src/* | cut -d: -f2", + Modules = [list_to_atom(M) || M <- string:lexemes(os:cmd(Grep), "\n")], + Properties = + [{vsn, VersionString}, + {modules, Modules}, + {mod, {AppName, []}}], + Store = fun(T, L) -> lists:keystore(element(1, T), 1, L, T) end, + AppData = lists:foldl(Store, RawAppData, Properties), + AppProfile = {application, AppName, AppData}, + ok = log(info, "Writing app file: ~ts~n~tp", [AppFile, AppProfile]), + zx_lib:write_terms(AppFile, [AppProfile]). + + +-spec exec_shell(CMD) -> ok | no_return() + 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) -> PackageID + when PackageFile :: file:filename(), + PackageID :: zx:package_id(). +%% @private +%% Receives a path to a file containing package data, examines it, and copies it to a +%% canonical location under a canonical name, returning the PackageID of the package +%% contents. + +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), + ok = + case public_key:verify(TgzData, sha512, Signature, PubKey) of + true -> + ZrpPath = filename:join("zsp", zx_lib:namify_zsp(PackageID)), + file:copy(PackageFile, ZrpPath); + false -> + error_exit("Bad package signature: ~ts", [PackageFile], ?LINE) + end, + ok = file:set_cwd(CWD), + Message = "~ts is now locally available.", + {ok, PackageString} = zx_lib:package_string(PackageID), + ok = log(info, Message, [PackageString]), + halt(0). + + +-spec set_dep(Identifier :: string()) -> no_return(). +%% @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 +%% reflect that in the PackageString argument. The AppString is permitted to be +%% incomplete. Incomplete elements of the VersionString (if included) will default to +%% the latest version available at the indicated level. + +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}; + _ -> + error_exit("Incomplete version tuple: ~tp", [FuzzyVersion]) + end, + set_dep({Realm, Name}, Version). + + +set_dep({Realm, Name}, Version) -> + PackageID = {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) + end. + + +-spec set_dep(PackageID, Deps, Meta) -> no_return() + when PackageID :: zx:package_id(), + Deps :: [zx:package_id()], + Meta :: [term()]. +%% @private +%% Given the PackageID, list of Deps and the current contents of the project Meta, add +%% or update Deps to include (or update) Deps to reflect a dependency on PackageID, if +%% 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) -> + ExistingPackageIDs = fun({R, N, _}) -> {R, N} == {Realm, Name} end, + NewDeps = + case lists:partition(ExistingPackageIDs, Deps) of + {[{Realm, Name, OldVersion}], Rest} -> + Message = "Updating dep ~ts to ~ts", + {ok, OldPS} = zx_lib:package_string({Realm, Name, OldVersion}), + {ok, NewPS} = zx_lib:package_string({Realm, Name, NewVersion}), + ok = log(info, Message, [OldPS, NewPS]), + [PackageID | Rest]; + {[], Deps} -> + {ok, PackageString} = zx_lib:package_string(PackageID), + ok = log(info, "Adding dep ~ts", [PackageString]), + [PackageID | Deps] + end, + NewMeta = maps:put(deps, NewDeps, Meta), + ok = zx_lib:write_project_meta(NewMeta), + halt(0). + + +-spec set_version(VersionString) -> no_return() + 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, + {ok, Meta} = zx_lib:read_project_meta(), + {Realm, Name, OldVersion} = maps:get(package_id, Meta), + update_version(Realm, Name, OldVersion, NewVersion, Meta). + + +-spec update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> no_return() + when Realm :: zx:realm(), + Name :: zx:name(), + OldVersion :: zx:version(), + NewVersion :: zx:version(), + OldMeta :: zx:package_meta(). +%% @private +%% Update a project's `zomp.meta' file by either incrementing the indicated component, +%% or setting the version number to the one specified in VersionString. +%% This part of the procedure updates the meta and does the final write, if the write +%% turns out to be possible. If successful it will indicate to the user what was +%% changed. + +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), + {ok, OldVS} = zx_lib:version_to_string(OldVersion), + {ok, NewVS} = zx_lib:version_to_string(NewVersion), + ok = log(info, "Version changed from ~s to ~s.", [OldVS, NewVS]), + halt(0). + + +-spec list_realms() -> no_return(). +%% @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_realms() -> + ok = lists:foreach(fun(R) -> io:format("~ts~n", [R]) end, zx_lib:list_realms()), + halt(0). + + +-spec list_packages(zx:realm()) -> no_return(). +%% @private +%% Contact the indicated realm and query it for a list of registered packages and print +%% them to stdout. + +list_packages(Realm) -> + ok = zx:start(), + case zx_daemon:list_packages(Realm) of + {ok, []} -> + ok = log(info, "Realm ~tp has no packages available.", [Realm]), + halt(0); + {ok, Packages} -> + Print = fun({R, N}) -> io:format("~ts-~ts~n", [R, N]) end, + ok = lists:foreach(Print, Packages), + halt(0); + {error, bad_realm} -> + error_exit("Bad realm name.", ?LINE); + {error, no_realm} -> + error_exit("Realm \"~ts\" is not configured.", ?LINE); + {error, network} -> + Message = "Network issues are preventing connection to the realm.", + error_exit(Message, ?LINE) + end. + + +-spec list_versions(PackageName :: string()) -> no_return(). +%% @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}; + {error, invalid_package_string} -> + error_exit("~tp is not a valid package name.", [PackageName], ?LINE) + end, + ok = zx:start(), + case zx_daemon:list_versions(Package) of + {ok, []} -> + Message = "Package ~ts has no versions available.", + ok = log(info, Message, [PackageName]), + halt(0); + {ok, Versions} -> + Print = + fun(Version) -> + {ok, PackageString} = zx_lib:package_string({Realm, Name, Version}), + io:format("~ts~n", [PackageString]) + end, + ok = lists:foreach(Print, Versions), + halt(0); + {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) + end. + + +-spec add_realm(Path) -> no_return() + 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. + +add_realm(Path) -> + case file:read_file(Path) of + {ok, Data} -> + 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); + {error, enoent} -> + ok = log(warning, "FAILED: ~ts does not exist.", [Path]), + halt(1); + {error, eisdir} -> + ok = log(warning, "FAILED: ~ts is a directory, not a realm file.", [Path]), + halt(1) + end. + + +-spec add_realm(Path, Data) -> no_return() + 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), + ok = log(info, "Realm ~ts is now visible to this system.", [Realm]), + halt(0); + {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_dep(zx:package_id()) -> no_return(). +%% @private +%% Remove the indicate dependency from the local project's zomp.meta record. + +drop_dep(PackageID) -> + {ok, PackageString} = zx_lib:package_string(PackageID), + {ok, Meta} = zx_lib:read_project_meta(), + Deps = maps:get(deps, Meta), + case lists:member(PackageID, Deps) of + true -> + NewDeps = lists:delete(PackageID, Deps), + NewMeta = maps:put(deps, NewDeps, Meta), + ok = zx_lib:write_project_meta(NewMeta), + Message = "~ts removed from dependencies.", + ok = log(info, Message, [PackageString]), + halt(0); + false -> + ok = log(info, "~ts not found in dependencies.", [PackageString]), + halt(0) + end. + + +-spec drop_key(zx:key_id()) -> no_return(). +%% @private +%% Given a KeyID, remove the related public and private keys from the keystore, if they +%% exist. If not, exit with a message that no keys were found, but do not return an +%% 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]), + case filelib:wildcard(Pattern) of + [] -> + ok = log(warning, "Key ~ts/~ts not found", [Realm, KeyName]), + halt(0); + Files -> + ok = lists:foreach(fun file:delete/1, Files), + ok = log(info, "Keyset ~ts/~ts removed", [Realm, KeyName]), + halt(0) + end. + + +-spec drop_realm(zx:realm()) -> no_return(). + +drop_realm(Realm) -> + ok = file:set_cwd(zx_lib:zomp_dir()), + RealmConf = zx_lib:realm_conf(Realm), + case filelib:is_regular(RealmConf) of + true -> + Message = + "~n" + " WARNING: Are you SURE you want to remove realm ~ts?~n" + " (Only \"Y\" will confirm this action.)~n", + ok = io:format(Message, [Realm]), + case zx_tty:get_input() of + "Y" -> + ok = file:delete(RealmConf), + ok = drop_prime(Realm), + ok = clear_keys(Realm), + ok = log(info, "All traces of realm ~ts have been removed."), + halt(0); + _ -> + ok = log(info, "Aborting."), + halt(0) + end; + false -> + ok = log(warning, "Realm conf ~ts not found.", [RealmConf]), + clear_keys(Realm) + end. + + +-spec drop_prime(zx:realm()) -> ok. + +drop_prime(Realm) -> + Path = "zomp.conf", + case file:consult(Path) of + {ok, Conf} -> + {managed, Primes} = lists:keyfind(managed, 1, Conf), + NewPrimes = lists:delete(Realm, Primes), + NewConf = lists:keystore(managed, 1, Primes, {managed, NewPrimes}), + ok = zx_lib:write_terms(Path, NewConf), + log(info, "Ensuring ~ts is not a prime in ~ts", [Realm, Path]); + {error, enoent} -> + ok + end. + + +-spec clear_keys(zx:realm()) -> ok. + +clear_keys(Realm) -> + KeyDir = filename:join([zx_lib:zomp_dir(), "key", Realm]), + case filelib:is_dir(KeyDir) of + true -> zx_lib:rm_rf(KeyDir); + false -> log(warning, "Keydir ~ts not found", [KeyDir]) + end. + + +-spec verup(Level) -> no_return() + when Level :: string(). +%% @private +%% Convert input string arguments to acceptable atoms for use in update_version/1. + +verup("major") -> version_up(major); +verup("minor") -> version_up(minor); +verup("patch") -> version_up(patch); +verup(_) -> zx:usage_exit(22). + + +-spec version_up(Level) -> no_return() + when Level :: major + | minor + | patch. +%% @private +%% Update a project's `zomp.meta' file by either incrementing the indicated component, +%% or setting the version number to the one specified in VersionString. +%% This part of the procedure guards for the case when the zomp.meta file cannot be +%% 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, + PackageID = maps:get(package_id, Meta), + version_up(Arg, PackageID, Meta). + + +-spec version_up(Level, PackageID, Meta) -> no_return() + when Level :: major + | minor + | patch + | zx:version(), + PackageID :: zx:package_id(), + Meta :: [{atom(), term()}]. +%% @private +%% Update a project's `zomp.meta' file by either incrementing the indicated component, +%% or setting the version number to the one specified in VersionString. +%% This part of the procedure does the actual update calculation, to include calling to +%% convert the VersionString (if it is passed) to a `version()' type and check its +%% validity (or halt if it is a bad string). + +version_up(major, {Realm, Name, OldVersion = {Major, _, _}}, OldMeta) -> + NewVersion = {Major + 1, 0, 0}, + update_version(Realm, Name, OldVersion, NewVersion, OldMeta); +version_up(minor, {Realm, Name, OldVersion = {Major, Minor, _}}, OldMeta) -> + NewVersion = {Major, Minor + 1, 0}, + update_version(Realm, Name, OldVersion, NewVersion, OldMeta); +version_up(patch, {Realm, Name, OldVersion = {Major, Minor, Patch}}, OldMeta) -> + NewVersion = {Major, Minor, Patch + 1}, + update_version(Realm, Name, OldVersion, NewVersion, OldMeta). + + +-spec package(TargetDir) -> no_return() + when TargetDir :: file:filename(). +%% @private +%% Turn a target project directory into a package, prompting the user for appropriate +%% key selection or generation actions along the way. + +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]), + ok = zx_lib:force_dir(KeyDir), + Pattern = KeyDir ++ "/*.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, _, _} = zx_key:generate_rsa(KeyID), + package(KeyID, TargetDir); + [KeyName] -> + KeyID = {Realm, KeyName}, + ok = log(info, "Using key: ~ts/~ts", [Realm, KeyName]), + package(KeyID, TargetDir); + KeyNames -> + KeyName = zx_tty:select_string(KeyNames), + package({Realm, KeyName}, TargetDir) + end. + + +-spec package(KeyID, TargetDir) -> no_return() + when KeyID :: zx:key_id(), + TargetDir :: file:filename(). +%% @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. + +package(KeyID, TargetDir) -> + {ok, Meta} = zx_lib:read_project_meta(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), + ok = remove_binaries(TargetDir), + {ok, Everything} = file:list_dir(TargetDir), + DotFiles = filelib:wildcard(".*", TargetDir), + Ignores = ["lib" | DotFiles], + Targets = lists:subtract(Everything, Ignores), + {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), + {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). + + +-spec remove_binaries(TargetDir) -> ok + when TargetDir :: file:filename(). +%% @private +%% Procedure to delete all .beam and .ez files from a given directory starting at +%% TargetDir. Called as part of the pre-packaging sanitization procedure. + +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. + + +-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. +%% @private +%% Build a general plt file for Dialyzer based on the core Erland distro. +%% TODO: Make a per-package + dependencies version of this. + +build_plt() -> + PLT = default_plt(), + Template = + "dialyzer --build_plt" + " --output_plt ~ts" + " --apps asn1 reltool wx common_test crypto erts eunit inets" + " kernel mnesia public_key sasl ssh ssl stdlib", + Command = io_lib:format(Template, [PLT]), + Message = + "Generating PLT file and writing to: ~tp~n" + " There will be a list of \"unknown functions\" in the final output.~n" + " Don't panic. This is normal. Turtles all the way down, after all...", + ok = log(info, Message, [PLT]), + ok = log(info, "This may take a while. Patience is a virtue."), + Out = os:cmd(Command), + log(info, Out). + + +-spec default_plt() -> file:filename(). + +default_plt() -> + filename:join(zx_lib:zomp_dir(), "basic.plt"). + + +-spec dialyze() -> no_return(). +%% @private +%% Preps a copy of this script for typechecking with Dialyzer. +%% TODO: Create a package_id() based version of this to handle dialyzation of complex +%% projects. + +dialyze() -> + PLT = default_plt(), + ok = + case filelib:is_regular(PLT) of + 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")), + ok = log(info, "Temporarily reconstructing ~tp as ~tp", [Me, EvilTwin]), + Sed = io_lib:format("sed 's/^#!.*$//' ~s > ~s", [Me, EvilTwin]), + "" = os:cmd(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"); + Warnings -> + Mine = [dialyzer:format_warning({Tag, {Me, Line}, Msg}) + || {Tag, {_, Line}, Msg} <- Warnings], + lists:foreach(fun io:format/1, Mine) + end, + ok = file:delete(EvilTwin), + halt(0). + + +-spec create_user(zx:realm(), zx:user_name()) -> no_return(). +%% @private +%% Validate the realm and username provided, prompt the user to either select a keypair +%% to use or generate a new one, and bundle a .zuser file for conveyance of the user +%% data and his relevant keys (for import into an existing zomp server via `add' +%% command like "add packager", "add maintainer" and "add sysop". + +create_user(Realm, Username) -> + Message = "Would be generating a user file for {~160tp, ~160to}.", + ok = log(info, Message, [Realm, Username]), + halt(0). + + +-spec create_realm() -> no_return(). +%% @private +%% Prompt the user to input the information necessary to create a new zomp realm, +%% package the data appropriately for the server and deliver the final keys and +%% realm file to the user. + +create_realm() -> + Instructions = + "~n" + " Enter a name for your new realm.~n" + " Names can contain only lower-case letters, numbers and the underscore.~n" + " Names must begin with a lower-case letter.~n", + ok = io:format(Instructions), + Realm = zx_tty:get_input(), + case zx_lib:valid_lower0_9(Realm) of + true -> + RealmFile = filename:join(zx_lib:zomp_dir(), Realm ++ ".realm"), + case filelib:is_regular(RealmFile) of + false -> + create_realm(Realm); + true -> + ok = io:format("That realm already exists. Be more original.~n"), + create_realm() + end; + false -> + ok = io:format("Bad realm name \"~ts\". Try again.~n", [Realm]), + create_realm() + end. + + +-spec create_realm(Realm) -> no_return() + when Realm :: zx:realm(). + +create_realm(Realm) -> + ExAddress = prompt_external_address(), + create_realm(Realm, ExAddress). + + +-spec prompt_external_address() -> Result + when Result :: inet:hostname() | inet:ip_address(). + +prompt_external_address() -> + Message = external_address_prompt(), + ok = io:format(Message), + case zx_tty:get_input() of + "" -> + ok = io:format("You need to enter an address.~n"), + prompt_external_address(); + String -> + parse_address(String) + end. + + +-spec external_address_prompt() -> string(). + +external_address_prompt() -> + "~n" + " Enter a static, valid hostname or IPv4 or IPv6 address at which this host " + "can be reached from the public internet (or internal network if it will never " + "need to be reached from the internet).~n" + " DO NOT INCLUDE A PORT NUMBER IN THIS STEP~n". + + +-spec parse_address(string()) -> inet:hostname() | inet:ip_address(). + +parse_address(String) -> + case inet:parse_address(String) of + {ok, Address} -> Address; + {error, einval} -> String + end. + + +-spec create_realm(Realm, ExAddress) -> no_return() + when Realm :: zx:realm(), + ExAddress :: inet:hostname() | inet:ip_address(). + +create_realm(Realm, ExAddress) -> + Message = + "~n" + " Enter the public (external) port number at which this service should be " + "available. (This might be different from the local port number if you are " + "forwarding ports or have a complex network layout.)~n", + ok = io:format(Message), + ExPort = prompt_port_number(11311), + create_realm(Realm, ExAddress, ExPort). + + +-spec create_realm(Realm, ExAddress, ExPort) -> no_return() + when Realm :: zx:realm(), + ExAddress :: inet:hostname() | inet:ip_address(), + ExPort :: inet:port_number(). + +create_realm(Realm, ExAddress, ExPort) -> + Message = + "~n" + " Enter the local (internal/LAN) port number at which this service should be " + "available. (This might be different from the public port visible from the " + "internet if you are port forwarding or have a complex network layout.)~n", + ok = io:format(Message), + InPort = prompt_port_number(11311), + create_realm(Realm, ExAddress, ExPort, InPort). + + +-spec prompt_port_number(Current) -> Result + when Current :: inet:port_number(), + Result :: inet:port_number(). + +prompt_port_number(Current) -> + Instructions = + " A valid port is any number from 1 to 65535." + " [Press enter to accept the current setting: ~tw]~n", + ok = io:format(Instructions, [Current]), + case zx_tty:get_input() of + "" -> + Current; + S -> + try + case list_to_integer(S) of + Port when 16#ffff >= Port, Port > 0 -> + Port; + Illegal -> + Whoops = "Whoops! ~tw is out of bounds (1~65535). Try again.~n", + ok = io:format(Whoops, [Illegal]), + prompt_port_number(Current) + end + catch error:badarg -> + ok = io:format("~tp is not a port number. Try again...", [S]), + prompt_port_number(Current) + end + end. + + +-spec create_realm(Realm, ExAddress, ExPort, InPort) -> no_return() + when Realm :: zx:realm(), + ExAddress :: inet:hostname() | inet:ip_address(), + ExPort :: inet:port_number(), + InPort :: inet:port_number(). + +create_realm(Realm, ExAddress, ExPort, InPort) -> + Instructions = + "~n" + " Enter a username for the realm sysop.~n" + " Names can contain only lower-case letters, numbers and the underscore.~n" + " Names must begin with a lower-case letter.~n", + ok = io:format(Instructions), + UserName = zx_tty:get_input(), + case zx_lib:valid_lower0_9(UserName) of + true -> + create_realm(Realm, ExAddress, ExPort, InPort, UserName); + false -> + ok = io:format("Bad username ~tp. Try again.~n", [UserName]), + create_realm(Realm, ExAddress, ExPort, InPort) + end. + + +-spec create_realm(Realm, ExAddress, ExPort, InPort, UserName) -> no_return() + when Realm :: zx:realm(), + ExAddress :: inet:hostname() | inet:ip_address(), + ExPort :: inet:port_number(), + InPort :: inet:port_number(), + UserName :: string(). + +create_realm(Realm, ExAddress, ExPort, InPort, UserName) -> + Instructions = + "~n" + " Enter an email address for the realm sysop.~n" + " Valid email address rules apply though the checking done here is quite " + "minimal. Check the address you enter carefully. The only people who will " + "suffer from an invalid address are your users.~n", + ok = io:format(Instructions), + Email = zx_tty:get_input(), + [User, Host] = string:lexemes(Email, "@"), + case {zx_lib:valid_lower0_9(User), zx_lib:valid_label(Host)} of + {true, true} -> + create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email); + {false, true} -> + Message = "The user part of the email address seems invalid. Try again.~n", + ok = io:format(Message), + create_realm(Realm, ExAddress, ExPort, InPort, UserName); + {true, false} -> + Message = "The host part of the email address seems invalid. Try again.~n", + ok = io:format(Message), + create_realm(Realm, ExAddress, ExPort, InPort, UserName); + {false, false} -> + Message = "This email address seems like its totally bonkers. Try again.~n", + ok = io:format(Message), + create_realm(Realm, ExAddress, ExPort, InPort, UserName) + end. + + +-spec create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email) -> + no_return() + when Realm :: zx:realm(), + ExAddress :: inet:hostname() | inet:ip_address(), + ExPort :: inet:port_number(), + InPort :: inet:port_number(), + UserName :: string(), + Email :: string(). + +create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email) -> + Instructions = + "~n" + " Enter the real name (or whatever name people recognize) for the sysop.~n" + " There are no rules for this one. Any valid UTF-8 printables are legal.~n", + ok = io:format(Instructions), + RealName = zx_tty:get_input(), + create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email, RealName). + + +-spec create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email, RealName) -> no_return() + when Realm :: zx:realm(), + ExAddress :: inet:hostname() | inet:ip_address(), + ExPort :: inet:port_number(), + InPort :: inet:port_number(), + UserName :: string(), + Email :: string(), + RealName :: string(). + +create_realm(Realm, ExAddress, ExPort, InPort, UserName, Email, RealName) -> + ok = io:format("~nGenerating keys. This might take a while, so settle in...~n"), + {ok, RealmKey, RealmPub} = zx_key:generate_rsa({Realm, Realm ++ ".1.realm"}), + {ok, PackageKey, PackagePub} = zx_key:generate_rsa({Realm, Realm ++ ".1.package"}), + {ok, SysopKey, SysopPub} = zx_key:generate_rsa({Realm, UserName ++ ".1"}), + ok = log(info, "Generated 16k RSA pair ~ts ~ts", [RealmKey, RealmPub]), + ok = log(info, "Generated 16k RSA pair ~ts ~ts", [PackageKey, PackagePub]), + ok = log(info, "Generated 16k RSA pair ~ts ~ts", [SysopKey, SysopPub]), + + Timestamp = calendar:now_to_universal_time(erlang:timestamp()), + + {ok, RealmPubData} = file:read_file(RealmPub), + RealmPubRecord = + {{Realm, filename:basename(RealmPub, ".pub.der")}, + realm, + {realm, Realm}, + crypto:hash(sha512, RealmPubData), + Timestamp}, + {ok, PackagePubData} = file:read_file(PackagePub), + PackagePubRecord = + {{Realm, filename:basename(PackagePub, ".pub.der")}, + package, + {realm, Realm}, + crypto:hash(sha512, PackagePubData), + Timestamp}, + UserRecord = + {{Realm, UserName}, + [filename:basename(SysopPub, ".pub.der")], + Email, + RealName}, + RealmSettings = + [{realm, Realm}, + {revision, 0}, + {prime, {ExAddress, ExPort}}, + {private, []}, + {mirrors, []}, + {sysops, [UserRecord]}, + {realm_keys, [RealmPubRecord]}, + {package_keys, [PackagePubRecord]}], + ZompSettings = + [{managed, [Realm]}, + {external_address, ExAddress}, + {external_port, ExPort}, + {internal_port, InPort}], + + {ok, CWD} = file:get_cwd(), + {ok, TempDir} = mktemp_dir("zomp"), + ok = file:set_cwd(TempDir), + KeyDir = filename:join("key", Realm), + ok = filelib:ensure_dir(KeyDir), + ok = file:make_dir(KeyDir), + KeyCopy = + fun(K) -> + {ok, _} = file:copy(K, filename:join(KeyDir, filename:basename(K))), + ok + end, + + PublicZRF = filename:join(CWD, Realm ++ ".zrf"), + RealmFN = Realm ++ ".realm", + ok = zx_lib:write_terms(RealmFN, RealmSettings), + ok = KeyCopy(PackagePub), + ok = KeyCopy(RealmPub), + ok = erl_tar:create(PublicZRF, [RealmFN, "key"], [compressed]), + + PrimeZRF = filename:join(CWD, Realm ++ ".zpf"), + ok = KeyCopy(SysopPub), + ok = zx_lib:write_terms("zomp.conf", ZompSettings), + ok = erl_tar:create(PrimeZRF, [RealmFN, "zomp.conf", "key"], [compressed]), + + KeyBundle = filename:join(CWD, Realm ++ ".zkf"), + ok = lists:foreach(KeyCopy, [PackageKey, RealmKey, SysopKey]), + ok = erl_tar:create(KeyBundle, [KeyDir], [compressed]), + + ok = file:set_cwd(CWD), + ok = zx_lib:rm_rf(TempDir), + + Message = + "===========================================================================~n" + "DONE!~n" + "~n" + "The realm ~ts has been created and is accessible from the current system.~n" + "Three configuration bundles have been created in the current directory:~n" + "~n" + " 1. ~ts ~n" + "This is the PRIVATE realm file you will need to install on the realm's prime~n" + "node. It includes the your (the sysop's) public key.~n" + "~n" + " 2. ~ts ~n" + "This file is the PUBLIC realm file other zomp nodes and zx users will need~n" + "to access the realm. It does not include your (the sysop's) public key.~n" + "~n" + " 3. ~ts ~n" + "This is the bundle of ALL KEYS that are defined in this realm at the moment.~n" + "~n" + "Now you need to make copies of these three files and back them up.~n" + "~n" + "On the PRIME NODE you need to run `zx add realm ~ts` and follow the prompts~n" + "to cause it to begin serving that realm as prime. (Node restart required.)~n" + "~n" + "On all zx CLIENTS that want to access your new realm and on all subordinate~n" + "MIRROR NODES the command `zx add realm ~ts` will need to be run.~n" + "The method of public realm file distribution (~ts) is up to you.~n" + "~n" + "~n" + "Public & Private key installation (if you need to recover them or perform~n" + "sysop functions from another computer) is `zx add keybundle ~ts`.~n" + "===========================================================================~n", + Substitutions = + [Realm, + PrimeZRF, PublicZRF, KeyBundle, + PrimeZRF, + PublicZRF, PublicZRF, + KeyBundle], + ok = io:format(Message, Substitutions), + halt(0). + + +-spec mktemp_dir(Prefix) -> Result + when Prefix :: string(), + Result :: {ok, TempDir :: file:filename()} + | {error, Reason :: file:posix()}. + +mktemp_dir(Prefix) -> + Rand = integer_to_list(binary:decode_unsigned(crypto:strong_rand_bytes(8)), 36), + TempPath = filename:basedir(user_cache, Prefix), + TempDir = filename:join(TempPath, Rand), + Result1 = filelib:ensure_dir(TempDir), + Result2 = file:make_dir(TempDir), + case {Result1, Result2} of + {ok, ok} -> {ok, TempDir}; + {ok, Error} -> Error; + {Error, _} -> Error + end. + + +-spec create_realmfile(zx:realm()) -> no_return(). + +create_realmfile(Realm) -> + RealmConf = zx_lib:load_realm_conf(Realm), + ok = log(info, "Realm found, creating realm file..."), + {revision, Revision} = lists:keyfind(revision, 1, RealmConf), + {realm_keys, RealmKeys} = lists:keyfind(realm_keys, 1, RealmConf), + {package_keys, PackageKeys} = lists:keyfind(package_keys, 1, RealmConf), + RealmKeyIDs = [element(1, K) || K <- RealmKeys], + PackageKeyIDs = [element(1, K) || K <- PackageKeys], + create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs). + + +-spec create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) -> ok + when Realm :: zx:realm(), + Revision :: non_neg_integer(), + RealmKeyIDs :: [zx:key_id()], + PackageKeyIDs :: [zx:key_id()]. + +create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) -> + {ok, CWD} = file:get_cwd(), + ok = file:set_cwd(zx_lib:zomp_dir()), + KeyPath = fun({R, K}) -> filename:join(["key", R, K ++ ".pub.der"]) end, + RealmKeyPaths = lists:map(KeyPath, RealmKeyIDs), + PackageKeyPaths = lists:map(KeyPath, PackageKeyIDs), + Targets = [zx_lib:realm_conf(Realm) | RealmKeyPaths ++ PackageKeyPaths], + OutFile = filename:join(CWD, Realm ++ "." ++ integer_to_list(Revision) ++ ".zrf"), + ok = erl_tar:create(OutFile, Targets, [compressed]), + ok = log(info, "Realm conf file written to ~ts", [OutFile]), + halt(0). + + + +%%% 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). diff --git a/zomp/zx b/zomp/zx index 96f8928..c2ebddd 100755 --- a/zomp/zx +++ b/zomp/zx @@ -7,4 +7,4 @@ ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION" pushd "$ZX_DIR" > /dev/null ./make_zx popd > /dev/null -erl -pa "$ZX_DIR/ebin" -run zx start $@ +erl -pa "$ZX_DIR/ebin" -run zx run $@ diff --git a/zomp/zx.cmd b/zomp/zx.cmd index a7ed5a3..12e5aec 100644 --- a/zomp/zx.cmd +++ b/zomp/zx.cmd @@ -6,4 +6,4 @@ set ZX_DIR="%ZOMP_DIR%\lib\otpr\zx\%VERSION%" pushd "%ZX_DIR%" escript.exe make_zx popd -erl.exe -pa "%ZX_DIR%/ebin" -run zx start "%*" +erl.exe -pa "%ZX_DIR%/ebin" -run zx run "%*" diff --git a/zx_dev b/zx_dev index 2f4b9c2..a370431 100755 --- a/zx_dev +++ b/zx_dev @@ -1,13 +1,15 @@ #!/bin/bash pushd $(dirname $BASH_SOURCE) > /dev/null -export ZX_DEV_ROOT=$PWD +ZX_DEV_ROOT=$PWD popd > /dev/null -export ZOMP_DIR="$ZX_DEV_ROOT/zomp" -export VERSION=$(cat "$ZOMP_DIR/etc/version.txt") +export ZOMP_DIR="$ZX_DEV_ROOT/tester" +rm -rf "$ZOMP_DIR" +cp -r "$ZX_DEV_ROOT/zomp" "$ZOMP_DIR" +VERSION=$(cat "$ZOMP_DIR/etc/version.txt") export ZX_DIR="$ZOMP_DIR/lib/otpr/zx/$VERSION" pushd "$ZX_DIR" > /dev/null ./make_zx popd > /dev/null -erl -pa "$ZX_DIR/ebin" -run zx start $@ +erl -pa "$ZX_DIR/ebin" -run zx run $@