diff --git a/README.md b/README.md index 7ab57e7..bb339c7 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Project information can be found at https://zxq9com/zx/ and https://github.com/z ZX is delivered as a zip file containing: - `zomp.tar.gz`: An archive of a current working zx/zomp installation - `install.escript`: The main installation script -- `install_unix` and `install_windows.cms`: System-specific installation starters +- `install_unix` and `install_windows.cmd`: System-specific installation starters - `README.*` files such as this one - `LICENSE` - `notify.vbs`: A hacky VBS script to communicate with the Windows GUI during setup diff --git a/zomp/lib/otpr-zx/0.1.0/ebin/zx.app b/zomp/lib/otpr-zx/0.1.0/ebin/zx.app index 4e623c4..646143b 100644 --- a/zomp/lib/otpr-zx/0.1.0/ebin/zx.app +++ b/zomp/lib/otpr-zx/0.1.0/ebin/zx.app @@ -2,5 +2,5 @@ [{description, "Zomp client program"}, {vsn, "0.1.0"}, {applications, [stdlib, kernel]}, - {modules, [zx, zx_daemon]}, + {modules, [zx, zxd_sup, zxd]}, {mod, {zx, []}}]}. 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 8741ca7..9d1fc3f 100644 --- a/zomp/lib/otpr-zx/0.1.0/src/zx.erl +++ b/zomp/lib/otpr-zx/0.1.0/src/zx.erl @@ -19,33 +19,24 @@ -license("GPL-3.0"). --export([start/1, stop/0]). +-export([do/1]). +-export([subscribe/1, unsubscribe/0]). +-export([start/0, stop/0]). -export([start/2, stop/1]). --export_type([serial/0, package_id/0, package/0, realm/0, name/0, version/0, option/0, - host/0, key_id/0, key_name/0, user/0, username/0, lower0_9/0, label/0, +-export_type([serial/0, package_id/0, package/0, realm/0, name/0, version/0, + option/0, + host/0, + key_id/0, key_name/0, + user/0, username/0, lower0_9/0, label/0, package_meta/0]). -include("zx_logger.hrl"). --record(s, - {realm = "otpr" :: realm(), - name = none :: none | name(), - version = {z, z, z} :: version(), - type = app :: app | lib, - deps = [] :: [package_id()], - serial = 0 :: serial(), - dir = none :: none | file:filename(), - socket = none :: none | gen_tcp:socket(), - pid = none :: none | pid(), - mon = none :: none | reference()}). - - %%% Type Definitions --type state() :: #s{}. -type serial() :: integer(). -type package_id() :: {realm(), name(), version()}. -type package() :: {realm(), name()}. @@ -56,9 +47,6 @@ Patch :: non_neg_integer() | z}. -type option() :: {string(), term()}. -type host() :: {string() | inet:ip_address(), inet:port_number()}. -%-type keybin() :: {ID :: key_id(), -% Type :: public | private, -% DER :: binary()}. -type key_id() :: {realm(), key_name()}. -type key_name() :: label(). -type user() :: {realm(), username()}. @@ -69,74 +57,73 @@ --spec start(Args) -> no_return() +%%% Command Dispatch + +-spec do(Args) -> no_return() when Args :: [string()]. %% Dispatch work functions based on the nature of the input arguments. -start(["help"]) -> +do(["help"]) -> usage_exit(0); -start(["run", PackageString | Args]) -> +do(["run", PackageString | Args]) -> run(PackageString, Args); -start(["init", "app", PackageString]) -> - PackageID = package_id(PackageString), - initialize(app, PackageID); -start(["init", "lib", PackageString]) -> - PackageID = package_id(PackageString), - initialize(lib, PackageID); -start(["install", PackageFile]) -> +do(["runlocal" | ArgV]) -> + run_local(ArgV); +do(["init", "app", PackageString]) -> + initialize(app, PackageString); +do(["init", "lib", PackageString]) -> + initialize(lib, PackageString); +do(["install", PackageFile]) -> assimilate(PackageFile); -start(["set", "dep", PackageString]) -> - PackageID = package_id(PackageString), - set_dep(PackageID); -start(["set", "version", VersionString]) -> +do(["set", "dep", PackageString]) -> + set_dep(PackageString); +do(["set", "version", VersionString]) -> set_version(VersionString); -start(["list", "realms"]) -> +do(["list", "realms"]) -> list_realms(); -start(["list", "packages", Realm]) -> +do(["list", "packages", Realm]) -> ok = valid_realm(Realm), list_packages(Realm); -start(["list", "versions", PackageName]) -> +do(["list", "versions", PackageName]) -> Package = string_to_package(PackageName), list_versions(Package); -start(["list", "pending", PackageName]) -> +do(["list", "pending", PackageName]) -> Package = string_to_package(PackageName), list_pending(Package); -start(["list", "resigns", Realm]) -> +do(["list", "resigns", Realm]) -> ok = valid_realm(Realm), list_resigns(Realm); -start(["add", "realm", RealmFile]) -> +do(["add", "realm", RealmFile]) -> add_realm(RealmFile); -start(["add", "package", PackageName]) -> +do(["add", "package", PackageName]) -> add_package(PackageName); -start(["add", "packager", Package, UserName]) -> +do(["add", "packager", Package, UserName]) -> add_packager(Package, UserName); -start(["add", "maintainer", Package, UserName]) -> +do(["add", "maintainer", Package, UserName]) -> add_maintainer(Package, UserName); -start(["review", PackageString]) -> +do(["review", PackageString]) -> review(PackageString); -start(["approve", PackageString]) -> - PackageID = package_id(PackageString), +do(["approve", PackageString]) -> + PackageID = zx_lib:package_id(PackageString), approve(PackageID); -start(["reject", PackageString]) -> - PackageID = package_id(PackageString), +do(["reject", PackageString]) -> + PackageID = zx_lib:package_id(PackageString), reject(PackageID); -start(["resign", PackageString]) -> +do(["resign", PackageString]) -> resign(PackageString); -start(["drop", "dep", PackageString]) -> - PackageID = package_id(PackageString), +do(["drop", "dep", PackageString]) -> + PackageID = zx_lib:package_id(PackageString), drop_dep(PackageID); -start(["drop", "key", KeyID]) -> +do(["drop", "key", KeyID]) -> drop_key(KeyID); -start(["drop", "realm", Realm]) -> +do(["drop", "realm", Realm]) -> drop_realm(Realm); -start(["verup", Level]) -> +do(["verup", Level]) -> verup(Level); -start(["runlocal" | Args]) -> - run_local(Args); -start(["package"]) -> +do(["package"]) -> {ok, TargetDir} = file:get_cwd(), package(TargetDir); -start(["package", TargetDir]) -> +do(["package", TargetDir]) -> case filelib:is_dir(TargetDir) of true -> package(TargetDir); @@ -144,41 +131,123 @@ start(["package", TargetDir]) -> ok = log(error, "Target directory ~tp does not exist!", [TargetDir]), halt(22) end; -start(["submit", PackageFile]) -> +do(["submit", PackageFile]) -> submit(PackageFile); -start(["dialyze"]) -> +do(["dialyze"]) -> dialyze(); -start(["create", "user", Realm, Name]) -> +do(["create", "user", Realm, Name]) -> create_user(Realm, Name); -start(["create", "keypair"]) -> +do(["create", "keypair"]) -> create_keypair(); -start(["create", "plt"]) -> +do(["create", "plt"]) -> create_plt(); -start(["create", "realm"]) -> +do(["create", "realm"]) -> create_realm(); -start(["create", "realmfile", Realm]) -> +do(["create", "realmfile", Realm]) -> create_realmfile(Realm); -start(["create", "sysop"]) -> +do(["create", "sysop"]) -> create_sysop(); -start(_) -> +do(_) -> usage_exit(22). +%%% Daemon Controls + +-spec subscribe(package()) -> ok | {error, Reason :: term()}. +%% @doc +%% Initiates the zx_daemon and instructs it to subscribe to a package. +%% +%% Any events in the Zomp network that apply to the subscribed package will be +%% forwarded to the process that originally called subscribe/1. How the original +%% caller reacts to these notifications is up to the author -- not reply or "ack" +%% is expected. +%% +%% Package subscriptions can be used as the basis for user notification of updates, +%% automatic upgrade restarts, package catalog tracking, etc. + +subscribe(Package) -> + case application:start(?MODULE, normal) of + ok -> zx_daemon:subscribe(Package); + Error -> Error + end. + + +-spec unsubscribe() -> ok | {error, Reason :: term()}. +%% @doc +%% Unsubscribes from package updates. + +unsubscribe() -> + zx_daemon:unsubscribe(). + + + +%%% Application Start/Stop + +-spec start() -> ok | {error, Reason :: term()}. +%% @doc +%% An alias for `application:ensure_started(zx)', meaning it is safe to call this +%% function more than once, or within a system where you are unsure whether zx is +%% already running (perhaps due to complex dependencies that require zx already). +%% In the typical case this function does not ever need to be called, because the +%% zx_daemon is always started in the background whenever an application is started +%% using the command `zx run [app_id]'. +%% @equiv application:ensure_started(zx). + +start() -> + application:ensure_started(zx). + + +-spec stop() -> ok | {error, Reason :: term()}. +%% @doc +%% A safe wrapper for `application:stop(zx)'. Similar to `ensure_started/1,2', returns +%% `ok' in the case that zx is already stopped. + +stop() -> + case application:stop(zx) of + ok -> ok; + {error, {not_started, zx}} -> ok; + Error -> Error + end. + + +%%% Application Callbacks + +-spec start(StartType, StartArgs) -> Result + when StartType :: normal, + StartArgs :: [], + Result :: {ok, pid()}. +%% @private +%% Application callback. Not to be called directly. + +start(normal, []) -> + ok = application:ensure_started(inets), + zx_daemon:start_link(). + + +-spec stop(term()) -> ok. +%% @private +%% Application callback. Not to be called directly. + +stop(_) -> + ok. + + + %%% Execution of application --spec run(Identifier, Args) -> no_return() +-spec run(Identifier, RunArgs) -> no_return() when Identifier :: string(), - Args :: [string()]. + RunArgs :: [string()]. %% @private %% Given a program Identifier and a list of Args, attempt to locate the program and its %% dependencies and run the program. This implies determining whether the program and %% its dependencies are installed, available, need to be downloaded, or are inaccessible %% given the current system condition (they could also be bogus, of course). The -%% Identifier should be a valid PackageString of the form `realm-appname-version' -%% where the realm and appname should follow standard realm and app package naming -%% conventions and the version should be represented as a semver in string form (where -%% ommitted elements of the version always default to whatever is most current). +%% Identifier must be a valid package string of the form `realm-appname[-version]' +%% where the `realm()' and `name()' must follow Zomp package naming conventions and the +%% version should be represented as a semver in string form (where ommitted elements of +%% the version always default to whatever is most current). %% %% Once the target program is running, this process, (which will run with the registered %% name `zx') will sit in an `exec_wait' state, waiting for either a direct message from @@ -187,28 +256,127 @@ start(_) -> %% If there is a problem anywhere in the locating, discovery, building, and loading %% procedure the runtime will halt with an error message. -run(Identifier, Args) -> - MaybeID = package_id(Identifier), - {ok, PackageID = {Realm, Name, Version}} = ensure_installed(MaybeID), - ok = file:set_cwd(zomp_dir()), - Dir = filename:join("lib", package_string(PackageID)), - Meta = read_meta(Dir), +run(Identifier, RunArgs) -> + ok = file:set_cwd(zx_lib:zomp_home()), + ok = start(), + FuzzyID = + case zx_lib:package_id(Identifier) of + {ok, Fuzzy} -> + Fuzzy; + {error, invalid_package_string} -> + error_exit("Bad package string: ~ts", [Identifier], ?LINE) + end, + {ok, PackageID} = ensure_installed(FuzzyID), + ok = build(PackageID), + Dir = zx_lib:package_dir(PackageID), + {ok, Meta} = zx_lib:read_project_meta(Dir), + execute(PackageID, Meta, Dir, RunArgs). + + + +%%% Execution of local project + +-spec run_local(RunArgs) -> no_return() + when RunArgs :: [term()]. +%% @private +%% Execute a local project from source from the current directory, satisfying dependency +%% requirements via the locally installed zomp lib cache. The project must be +%% initialized as a zomp project (it must have a valid `zomp.meta' file). +%% +%% The most common use case for this function is during development. Using zomp support +%% via the local lib cache allows project authors to worry only about their own code +%% and use zx commands to add or drop dependencies made available via zomp. + +run_local(RunArgs) -> + Meta = zx_lib:read_project_meta(), + PackageID = maps:get(package_id, Meta), + ok = build(), + {ok, Dir} = file:get_cwd(), + ok = file:set_cwd(zx_lib:zomp_home()), + ok = start(), + execute(PackageID, Meta, Dir, RunArgs). + + +-spec execute(PackageID, Meta, Dir, RunArgs) -> no_return() + when PackageID :: package_id(), + Meta :: package_meta(), + Dir :: file:filename(), + RunArgs :: [string()]. +%% @private +%% Execution prep common to all packages. + +execute(PackageID, Meta, Dir, RunArgs) -> + PackageString = zx_lib:package_string(PackageID), + ok = log(info, "Preparing ~ts...", [PackageString]), + Type = maps:get(type, Meta), Deps = maps:get(deps, Meta), - ok = ensure_deps(Deps), - State = #s{realm = Realm, - name = Name, - version = Version, - dir = Dir, - deps = Deps}, - execute(State, Args). + case zx_daemon:fetch(Deps) of + {{ok, _}, {error, []}} -> + ok = lists:foreach(fun install/1, Deps), + ok = lists:foreach(fun build/1, Deps), + execute(Type, PackageID, Dir, Meta, RunArgs); + {{ok, _}, {error, Errors}} -> + error_exit("Failed package fetches: ~tp", [Errors], ?LINE) + end. + + +-spec execute(Type, PackageID, Meta, Dir, RunArgs) -> no_return() + when Type :: app | lib, + Meta :: package_meta(), + Dir :: file:filename(), + RunArgs :: [string()]. +%% @private +%% Gets all the target application's ducks in a row and launches them, then enters +%% the exec_wait/1 loop to wait for any queries from the application. + +execute(app, PackageID, Meta, Dir, RunArgs) -> + PackageString = zx_lib:package_string(PackageID), + ok = log(info, "Starting ~ts.", [PackageString]), + Name = element(2, PackageID), + AppMod = list_to_atom(Name), + ok = zx_daemon:pass_meta(Meta, Dir), + ok = ensure_all_started(AppMod), + ok = pass_argv(AppMod, RunArgs), + exec_wait(State); +execute(lib, PackageID, _, _, _) -> + Message = "Lib ~ts is available on the system, but is not a standalone app.", + PackageString = package_string(PackageID), + ok = log(info, Message, [PackageString]), + halt(0). + + +-spec ensure_all_started(AppMod) -> ok + when AppMod :: module(). +%% @private +%% Wrap a call to application:ensure_all_started/1 to selectively provide output +%% in the case any dependencies are actually started by the call. Might remove this +%% depending on whether SASL winds up becoming a standard part of the system and +%% whether it becomes common for dependencies to all signal their own start states +%% somehow. + +ensure_all_started(AppMod) -> + case application:ensure_all_started(AppMod) of + {ok, []} -> ok; + {ok, Apps} -> log(info, "Started ~tp", [Apps]) + end. + + +-spec pass_argv(AppMod, Args) -> ok + when AppMod :: module(), + Args :: [string()]. +%% @private +%% Check whether the AppMod:accept_argv/1 is implemented. If so, pass in the +%% command line arguments provided. + +pass_argv(AppMod, Args) -> %%% Project initialization --spec initialize(Type, PackageID) -> no_return() - when Type :: app | lib, - PackageID :: package_id(). +-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 @@ -218,14 +386,21 @@ run(Identifier, Args) -> %% 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, PackageID) -> - PackageString = package_string(PackageID), +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}], + MetaList = + [{package_id, PackageID}, + {deps, []}, + {type, Type}], Meta = maps:from_list(MetaList), - ok = write_meta(Meta), + ok = zx_lib:write_project_meta(Meta), ok = log(info, "Project ~tp initialized.", [PackageString]), Message = "NOTICE:~n" @@ -248,23 +423,23 @@ initialize(Type, PackageID) -> %% contents. assimilate(PackageFile) -> - Files = extract_zrp(PackageFile), + Files = extract_zrp_or_die(PackageFile), {ok, CWD} = file:get_cwd(), - ok = file:set_cwd(zomp_dir()), + ok = file:set_cwd(zx_lib:zomp_home()), {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), Meta = binary_to_term(MetaBin), PackageID = maps:get(package_id, Meta), - TgzFile = namify_tgz(PackageID), + TgzFile = zx_lib:namify_tgz(PackageID), {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), {KeyID, Signature} = maps:get(sig, Meta), {ok, PubKey} = loadkey(public, KeyID), ok = case public_key:verify(TgzData, sha512, Signature, PubKey) of true -> - ZrpPath = filename:join("zrp", namify_zrp(PackageID)), - erl_tar:create(ZrpPath, Files); + ZrpPath = filename:join("zrp", zx_lib:namify_zrp(PackageID)), + file:copy(PackageFile, ZrpPath); false -> - error_exit("Bad package signature: ~ts", [PackageFile], ?FILE, ?LINE) + error_exit("Bad package signature: ~ts", [PackageFile], ?LINE) end, ok = file:set_cwd(CWD), Message = "~ts is now locally available.", @@ -275,7 +450,7 @@ assimilate(PackageFile) -> %%% Set dependency --spec set_dep(package_id()) -> no_return(). +-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 @@ -283,9 +458,27 @@ assimilate(PackageFile) -> %% incomplete. Incomplete elements of the VersionString (if included) will default to %% the latest version available at the indicated level. -set_dep(PackageID = {_, _, {X, Y, Z}}) - when is_integer(X), is_integer(Y), is_integer(Z) -> - Meta = read_meta(), +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 -> @@ -293,17 +486,7 @@ set_dep(PackageID = {_, _, {X, Y, Z}}) halt(0); false -> set_dep(PackageID, Deps, Meta) - end; -set_dep({Realm, Name, {z, z, z}}) -> - Socket = connect_user(Realm), - {ok, Version} = query_latest(Socket, {Realm, Name}), - ok = disconnect(Socket), - set_dep({Realm, Name, Version}); -set_dep({Realm, Name, Version}) -> - Socket = connect_user(Realm), - {ok, Latest} = query_latest(Socket, {Realm, Name, Version}), - ok = disconnect(Socket), - set_dep({Realm, Name, Latest}). + end. -spec set_dep(PackageID, Deps, Meta) -> no_return() @@ -317,7 +500,7 @@ set_dep({Realm, Name, Version}) -> %% file and exit. set_dep(PackageID = {Realm, Name, NewVersion}, Deps, Meta) -> - ExistingPackageIDs = fun ({R, N, _}) -> {R, N} == {Realm, Name} end, + ExistingPackageIDs = fun({R, N, _}) -> {R, N} == {Realm, Name} end, NewDeps = case lists:partition(ExistingPackageIDs, Deps) of {[{Realm, Name, OldVersion}], Rest} -> @@ -331,7 +514,7 @@ set_dep(PackageID = {Realm, Name, NewVersion}, Deps, Meta) -> [PackageID | Deps] end, NewMeta = maps:put(deps, NewDeps, Meta), - ok = write_meta(NewMeta), + ok = zx_lib:write_project_meta(NewMeta), halt(0). @@ -347,7 +530,26 @@ ensure_installed(PackageID = {Realm, Name, Version}) -> case resolve_installed_version(PackageID) of exact -> {ok, PackageID}; {ok, Installed} -> {ok, {Realm, Name, Installed}}; - not_found -> ensure_installed(Realm, Name, Version) + not_found -> fetch({Realm, Name, Version}) + end. + + +-spec fetch_one(PackageID) -> {ok, ActualID} | no_return() + when PackageID :: package_id(), + ActualID :: package_id(). +%% @private +%% A helper function to deal with the special case of downloading and installing a +%% single primary application package with a possibly incomplete version designator. +%% All other fetches are for arbitrarily long lists of package IDs with complete +%% version numbers (dependency fetches). + +fetch_one(PackageID) -> + case zx_daemon:fetch([PackageID]) of + {{ok, [ActualID]}, {error, []}} -> + ok = install(ActualID), + {ok, ActualID}; + {{ok, []}, {error, [{PackageID, Reason}]}} -> + error_exit("Package fetch failed with: ~tp", [Reason], ?LINE) end. @@ -363,12 +565,10 @@ ensure_installed(PackageID = {Realm, Name, Version}) -> %% the version indicator is complete, partial or blank. ensure_installed(Realm, Name, Version) -> - Socket = connect_user(Realm), - case query_latest(Socket, {Realm, Name, Version}) of + case zx_daemon:query_latest({Realm, Name, Version}) of {ok, LatestVersion} -> LatestID = {Realm, Name, LatestVersion}, ok = ensure_dep(Socket, LatestID), - ok = disconnect(Socket), {ok, LatestID}; {error, bad_realm} -> PackageString = package_string({Realm, Name, Version}), @@ -385,32 +585,6 @@ ensure_installed(Realm, Name, Version) -> end. --spec query_latest(Socket, Object) -> Result - when Socket :: gen_tcp:socket(), - Object :: package() | package_id(), - Result :: {ok, version()} - | {error, Reason}, - Reason :: bad_realm - | bad_package - | bad_version. -%% @private -%% Queries the connected zomp node for the latest version of a package or package -%% version (complete or incomplete version number). - -query_latest(Socket, {Realm, Name}) -> - ok = send(Socket, {latest, Realm, Name}), - receive - {tcp, Socket, Bin} -> binary_to_term(Bin) - after 5000 -> {error, timeout} - end; -query_latest(Socket, {Realm, Name, Version}) -> - ok = send(Socket, {latest, Realm, Name, Version}), - receive - {tcp, Socket, Bin} -> binary_to_term(Bin) - after 5000 -> {error, timeout} - end. - - -spec resolve_installed_version(PackageID) -> Result when PackageID :: package_id(), Result :: not_found @@ -422,74 +596,27 @@ query_latest(Socket, {Realm, Name, Version}) -> %% found (in the case of a full version input), a version matching a partial version %% input was found, or no match was found at all. -resolve_installed_version(PackageID) -> - PackageString = package_string(PackageID), - Pattern = PackageString ++ "*", - case filelib:wildcard(Pattern, "lib") of - [] -> - not_found; - [PackageString] -> - exact; - [Dir] -> - {_, _, Version} = package_id(Dir), - {ok, Version}; - Dirs -> - Dir = lists:last(lists:sort(Dirs)), - {_, _, Version} = package_id(Dir), - {ok, Version} +resolve_installed_version({Realm, Name, Version}) -> + PackageDir = filename:join(["lib", Realm, Name]), + case filelib:is_dir(PackageDir) of + true -> resolve_installed_version(PackageDir, Version); + false -> not_found end. -ensure_deps(Deps) -> - case scrub(Deps) of - [] -> - ok; - Needed -> - Partitioned = partition_by_realm(Needed), - EnsureDeps = - fun({Realm, Packages}) -> - Socket = connect_user(Realm), - ok = ensure_deps(Socket, Realm, Packages), - ok = disconnect(Socket), - log(info, "Disconnecting from realm: ~ts", [Realm]) - end, - lists:foreach(EnsureDeps, Partitioned) +resolve_installed_version(PackageDir, Version) -> + DirStrings = filelib:wildcard("*", PackageDir), + Versions = lists:fold(fun tuplize/2, [], DirStrings), + zx_lib:find_latest_compatible(Version, Versions). + + +tuplize(String, Acc) -> + case zx_lib:string_to_version(String) of + {ok, Version} -> [Version | Acc]; + _ -> Acc end. -partition_by_realm(PackageIDs) -> - PartitionMap = lists:foldl(fun partition_by_realm/2, #{}, PackageIDs), - maps:to_list(PartitionMap). - - -partition_by_realm({R, P, V}, M) -> - maps:update_with(R, fun(Ps) -> [{P, V} | Ps] end, [{P, V}], M). - - -ensure_deps(_, _, []) -> - ok; -ensure_deps(Socket, Realm, [{Name, Version} | Rest]) -> - ok = ensure_dep(Socket, {Realm, Name, Version}), - ensure_deps(Socket, Realm, Rest). - - --spec ensure_dep(gen_tcp:socket(), package_id()) -> ok | no_return(). -%% @private -%% Given an PackageID as an argument, check whether its package file exists in the -%% system cache, and if not download it. Should return `ok' whenever the file is -%% sourced, but exit with an error if it cannot locate or acquire the package. - -ensure_dep(Socket, PackageID) -> - ZrpFile = filename:join("zrp", namify_zrp(PackageID)), - ok = - case filelib:is_regular(ZrpFile) of - true -> ok; - false -> fetch(Socket, PackageID) - end, - ok = install(PackageID), - build(PackageID). - - %%% Set version @@ -501,7 +628,7 @@ ensure_dep(Socket, PackageID) -> set_version(VersionString) -> NewVersion = - case string_to_version(VersionString) of + case zx_lib:string_to_version(VersionString) of {_, _, z} -> Message = "'set version' arguments must be complete, ex: 1.2.3", ok = log(error, Message), @@ -525,7 +652,7 @@ set_version(VersionString) -> %% read for some reason. update_version(Arg) -> - Meta = read_meta(), + {ok, Meta} = zx_lib:read_project_meta(), PackageID = maps:get(package_id, Meta), update_version(Arg, PackageID, Meta). @@ -573,10 +700,11 @@ update_version(NewVersion, {Realm, Name, OldVersion}, OldMeta) -> update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> PackageID = {Realm, Name, NewVersion}, NewMeta = maps:put(package_id, PackageID, OldMeta), - ok = write_meta(NewMeta), + ok = zx_lib:write_project_meta(NewMeta), ok = log(info, "Version changed from ~s to ~s.", - [version_to_string(OldVersion), version_to_string(NewVersion)]), + [zx_lib:version_to_string(OldVersion), + zx_lib:version_to_string(NewVersion)]), halt(0). @@ -590,7 +718,7 @@ update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> %% stdout and the program will exit. list_realms() -> - Pattern = filename:join(zomp_dir(), "*.realm"), + Pattern = filename:join(zx_lib:zomp_home(), "*.realm"), RealmFiles = filelib:wildcard(Pattern), Realms = [filename:basename(RF, ".realm") || RF <- RealmFiles], ok = lists:foreach(fun(R) -> io:format("~ts~n", [R]) end, Realms), @@ -603,16 +731,23 @@ list_realms() -> %% them to stdout. list_packages(Realm) -> - Socket = connect_user(Realm), - ok = send(Socket, {list, Realm}), - case recv_or_die(Socket) of + 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) + halt(0); + {error, bad_realm} -> + ok = log(error, "Bad realm name."), + halt(1); + {error, no_realm} -> + ok = log(error, "Realm \"~ts\" is not configured.", [Realm]), + halt(1); + {error, network} -> + ok = log(error, "Network issues are preventing connection to the realm."), + halt(1) end. @@ -698,7 +833,7 @@ list_resigns(Realm) -> %% a non-zero error code, if so then return `ok'. valid_package({Realm, Name}) -> - case {valid_lower0_9(Realm), valid_lower0_9(Name)} of + case {zx_lib:valid_lower0_9(Realm), zx_lib:valid_lower0_9(Name)} of {true, true} -> ok; {false, true} -> @@ -735,7 +870,7 @@ string_to_package(String) -> %% halt execution with a non-zero error code, if so then return `ok'. valid_realm(Realm) -> - case valid_lower0_9(Realm) of + case zx_lib:valid_lower0_9(Realm) of true -> ok; false -> @@ -771,7 +906,7 @@ add_realm(Path) -> Data :: binary(). add_realm(Path, Data) -> - case erl_tar:extract({binary, Data}, [compressed, {cwd, zomp_dir()}]) of + case erl_tar:extract({binary, Data}, [compressed, {cwd, zx_lib:zomp_home()}]) of ok -> {Realm, _} = string:take(filename:basename(Path), ".", true), ok = log(info, "Realm ~ts is now visible to this system.", [Realm]), @@ -789,10 +924,10 @@ add_realm(Path, Data) -> when PackageName :: package(). add_package(PackageName) -> - ok = file:set_cwd(zomp_dir()), + ok = file:set_cwd(zx_lib:zomp_home()), case string:lexemes(PackageName, "-") of [Realm, Name] -> - case {valid_lower0_9(Realm), valid_lower0_9(Name)} of + case {zx_lib:valid_lower0_9(Realm), zx_lib:valid_lower0_9(Name)} of {true, true} -> add_package(Realm, Name); {false, true} -> @@ -836,7 +971,7 @@ add_maintainer(Package, UserName) -> review(PackageString) -> - PackageID = {Realm, _, _} = package_id(PackageString), + PackageID = {Realm, _, _} = zx_lib:package_id(PackageString), Socket = connect_auth_or_die(Realm), ok = send(Socket, {review, PackageID}), {ok, ZrpBin} = recv_or_die(Socket), @@ -882,7 +1017,7 @@ reject(PackageID = {Realm, _, _}) -> resign(PackageString) -> - PackageID = {Realm, _, _} = package_id(PackageString), + PackageID = {Realm, _, _} = zx_lib:package_id(PackageString), RealmConf = load_realm_conf(Realm), {package_keys, PackageKeys} = lists:keyfind(package_keys, 1, RealmConf), KeySelection = [{K, {R, K}} || {R, K} <- [element(1, K) || K <- PackageKeys]], @@ -925,13 +1060,13 @@ resign(PackageString) -> drop_dep(PackageID) -> PackageString = package_string(PackageID), - Meta = read_meta(), + {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 = write_meta(NewMeta), + ok = zx_lib:write_project_meta(NewMeta), Message = "~ts removed from dependencies.", ok = log(info, Message, [PackageString]), halt(0); @@ -951,8 +1086,8 @@ drop_dep(PackageID) -> %% error exit value (this instruction is idempotent if used in shell scripts). drop_key({Realm, KeyName}) -> - ok = file:set_cwd(zomp_dir()), - Pattern = filename:join([zomp_dir(), "key", Realm, KeyName ++ ".{key,pub}.der"]), + ok = file:set_cwd(zx_lib:zomp_home()), + Pattern = filename:join([zx_lib:zomp_home(), "key", Realm, KeyName ++ ".{key,pub}.der"]), case filelib:wildcard(Pattern) of [] -> ok = log(warning, "Key ~ts/~ts not found", [Realm, KeyName]), @@ -969,7 +1104,7 @@ drop_key({Realm, KeyName}) -> -spec drop_realm(realm()) -> no_return(). drop_realm(Realm) -> - ok = file:set_cwd(zomp_dir()), + ok = file:set_cwd(zx_lib:zomp_home()), RealmConf = realm_conf(Realm), case filelib:is_regular(RealmConf) of true -> @@ -1003,7 +1138,7 @@ drop_prime(Realm) -> {managed, Primes} = lists:keyfind(managed, 1, Conf), NewPrimes = lists:delete(Realm, Primes), NewConf = lists:keystore(managed, 1, Primes, {managed, NewPrimes}), - ok = write_terms(Path, NewConf), + ok = zx_lib:write_terms(Path, NewConf), log(info, "Ensuring ~ts is not a prime in ~ts", [Realm, Path]); {error, enoent} -> ok @@ -1013,7 +1148,7 @@ drop_prime(Realm) -> -spec clear_keys(realm()) -> ok. clear_keys(Realm) -> - KeyDir = filename:join([zomp_dir(), "key", Realm]), + KeyDir = filename:join([zx_lib:zomp_home(), "key", Realm]), case filelib:is_dir(KeyDir) of true -> ok = log(info, "Wiping key dir ~ts", [KeyDir]), @@ -1042,76 +1177,6 @@ verup(_) -> usage_exit(22). -%%% Run local project - --spec run_local(Args) -> no_return() - when Args :: [term()]. -%% @private -%% Execute a local project from source from the current directory, satisfying dependency -%% requirements via the locally installed zomp lib cache. The project must be -%% initialized as a zomp project (it must have a valid `zomp.meta' file). -%% -%% The most common use case for this function is during development. Using zomp support -%% via the local lib cache allows project authors to worry only about their own code -%% and use zx commands to add or drop dependencies made available via zomp. - -run_local(Args) -> - Meta = read_meta(), - {Realm, Name, Version} = maps:get(package_id, Meta), - Type = maps:get(type, Meta), - Deps = maps:get(deps, Meta), - ok = build(), - {ok, Dir} = file:get_cwd(), - ok = file:set_cwd(zomp_dir()), - State = #s{realm = Realm, - name = Name, - version = Version, - type = Type, - deps = Deps, - dir = Dir}, - ok = ensure_deps(Deps), - ok = file:set_cwd(Dir), - execute(State, Args). - - --spec execute(State, Args) -> no_return() - when State :: state(), - Args :: [string()]. -%% @private -%% Gets all the target application's ducks in a row and launches them, then enters -%% the exec_wait/1 loop to wait for any queries from the application. - -execute(State = #s{type = app, realm = Realm, name = Name, version = Version}, Args) -> - true = register(zx, self()), - ok = inets:start(), - ok = log(info, "Starting ~ts", [package_string({Realm, Name, Version})]), - AppMod = list_to_atom(Name), - {ok, Apps} = application:ensure_all_started(AppMod), - ok = log(info, "Started, ~tp", [Apps]), - ok = pass_argv(AppMod, Args), - exec_wait(State); -execute(#s{type = lib, realm = Realm, name = Name, version = Version}, _) -> - Message = "Lib ~ts is available on the system, but is not a standalone app.", - PackageString = package_string({Realm, Name, Version}), - ok = log(info, Message, [PackageString]), - halt(0). - - --spec pass_argv(AppMod, Args) -> ok - when AppMod :: module(), - Args :: [string()]. -%% @private -%% Check whether the AppMod:accept_argv/1 is implemented. If so, pass in the -%% command line arguments provided. - -pass_argv(AppMod, Args) -> - case lists:member({accept_argv, 1}, AppMod:module_info(exports)) of - true -> AppMod:accept_argv(Args); - false -> ok - end. - - - %%% Package generation -spec package(TargetDir) -> no_return() @@ -1122,9 +1187,9 @@ pass_argv(AppMod, Args) -> package(TargetDir) -> ok = log(info, "Packaging ~ts", [TargetDir]), - Meta = read_meta(TargetDir), + {ok, Meta} = zx_lib:read_project_meta(TargetDir), {Realm, _, _} = maps:get(package_id, Meta), - KeyDir = filename:join([zomp_dir(), "key", Realm]), + KeyDir = filename:join([zx_lib:zomp_home(), "key", Realm]), ok = force_dir(KeyDir), Pattern = KeyDir ++ "/*.key.der", case [filename:basename(F, ".key.der") || F <- filelib:wildcard(Pattern)] of @@ -1151,7 +1216,7 @@ package(TargetDir) -> %% build a zrp package file ready to be submitted to a repository. package(KeyID, TargetDir) -> - Meta = read_meta(TargetDir), + {ok, Meta} = zx_lib:read_project_meta(TargetDir), PackageID = maps:get(package_id, Meta), true = element(1, PackageID) == element(1, KeyID), PackageString = package_string(PackageID), @@ -1209,7 +1274,7 @@ remove_binaries(TargetDir) -> %% Submit a package to the appropriate "prime" server for the given realm. submit(PackageFile) -> - Files = extract_zrp(PackageFile), + Files = extract_zrp_or_die(PackageFile), {ok, PackageData} = file:read_file(PackageFile), {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), Meta = binary_to_term(MetaBin), @@ -1297,7 +1362,7 @@ halt_on_unexpected_close() -> connect_user(Realm) -> ok = log(info, "Connecting to realm ~ts...", [Realm]), Hosts = - case file:consult(hosts_cache_file(Realm)) of + case file:consult(zx_lib:hosts_cache_file(Realm)) of {ok, Cached} -> Cached; {error, enoent} -> [] end, @@ -1309,7 +1374,7 @@ connect_user(Realm) -> %% Try to connect to a subordinate host, if there are none then connect to prime. connect_user(Realm, []) -> - {Host, Port} = get_prime(Realm), + {Host, Port} = zx_lib:get_prime(Realm), HostString = case io_lib:printable_unicode_list(Host) of true -> Host; @@ -1375,7 +1440,7 @@ confirm_user(Realm, Socket, Hosts) -> %% reach, and if not retry on another node. confirm_serial(Realm, Socket, Hosts) -> - SerialFile = filename:join(zomp_dir(), "realm.serials"), + SerialFile = filename:join(zx_lib:zomp_home(), "realm.serials"), Serials = case file:consult(SerialFile) of {ok, Ss} -> Ss; @@ -1397,8 +1462,9 @@ confirm_serial(Realm, Socket, Hosts) -> ok = log(info, "Node's serial newer than ours. Storing."), NewSerials = lists:keystore(Realm, 1, Serials, {Realm, Current}), {ok, Host} = inet:peername(Socket), - ok = write_terms(hosts_cache_file(Realm), [Host | Hosts]), - ok = write_terms(SerialFile, NewSerials), + CacheFile = zx_lib:hosts_cache_file(Realm), + ok = zx_lib:write_terms(CacheFile, [Host | Hosts]), + ok = zx_lib:write_terms(SerialFile, NewSerials), Socket; {ok, Current} when Current < Serial -> log(info, "Our serial: ~tp, node serial: ~tp.", [Serial, Current]), @@ -1542,7 +1608,7 @@ confirm_auth(Socket) -> %% connect_auth/4. prep_auth(Realm, RealmConf) -> - UsersFile = filename:join(zomp_dir(), "zomp.users"), + UsersFile = filename:join(zx_lib:zomp_home(), "zomp.users"), Users = case file:consult(UsersFile) of {ok, U} -> @@ -1586,24 +1652,6 @@ connect_options() -> [{packet, 4}, {mode, binary}, {active, true}]. --spec get_prime(realm()) -> host(). -%% @private -%% Check the given Realm's config file for the current prime node and return it. - -get_prime(Realm) -> - RealmMeta = realm_meta(Realm), - {prime, Prime} = lists:keyfind(prime, 1, RealmMeta), - Prime. - - --spec hosts_cache_file(realm()) -> file:filename(). -%% @private -%% Given a Realm name, construct a realm's .hosts filename and return it. - -hosts_cache_file(Realm) -> - filename:join(zomp_dir(), Realm ++ ".hosts"). - - -spec disconnect(gen_tcp:socket()) -> ok. %% @private %% Gracefully shut down a socket, logging (but sidestepping) the case when the socket @@ -1648,7 +1696,7 @@ ensure_keypair(KeyID = {Realm, KeyName}) -> have_public_key({Realm, KeyName}) -> PublicKeyFile = KeyName ++ ".pub.der", - PublicKeyPath = filename:join([zomp_dir(), "key", Realm, PublicKeyFile]), + PublicKeyPath = filename:join([zx_lib:zomp_home(), "key", Realm, PublicKeyFile]), filelib:is_regular(PublicKeyPath). @@ -1658,32 +1706,10 @@ have_public_key({Realm, KeyName}) -> have_private_key({Realm, KeyName}) -> PrivateKeyFile = KeyName ++ ".key.der", - PrivateKeyPath = filename:join([zomp_dir(), "key", Realm, PrivateKeyFile]), + PrivateKeyPath = filename:join([zx_lib:zomp_home(), "key", Realm, PrivateKeyFile]), filelib:is_regular(PrivateKeyPath). --spec realm_meta(Realm) -> Meta | no_return() - when Realm :: string(), - Meta :: [{atom(), term()}]. -%% @private -%% Given a realm name, try to locate and read the realm's configuration file if it -%% exists, exiting with an appropriate error message if there is a problem reading -%% the file. - -realm_meta(Realm) -> - RealmFile = filename:join(zomp_dir(), Realm ++ ".realm"), - case file:consult(RealmFile) of - {ok, Meta} -> - Meta; - {error, enoent} -> - ok = log(error, "No realm file for ~ts", [Realm]), - halt(1); - Error -> - Message = "Open realm file ~ts failed with ~ts", - error_exit(Message, [RealmFile, Error], ?FILE, ?LINE) - end. - - %%% Key generation @@ -1707,7 +1733,7 @@ prompt_keygen() -> [R, K] -> {R, K}; [K] -> {"otpr", K} end, - case {valid_lower0_9(Realm), valid_label(KeyName)} of + case {zx_lib:valid_lower0_9(Realm), zx_lib:valid_label(KeyName)} of {true, true} -> {Realm, KeyName}; {false, true} -> @@ -1727,11 +1753,11 @@ prompt_keygen() -> %% Execute the key generation procedure for 16k RSA keys once and then terminate. create_keypair() -> - ok = file:set_cwd(zomp_dir()), + ok = file:set_cwd(zx_lib:zomp_home()), KeyID = prompt_keygen(), case generate_rsa(KeyID) of {ok, _, _} -> halt(0); - Error -> error_exit("create_keypair/0 error: ~tp", [Error], ?FILE, ?LINE) + Error -> error_exit("create_keypair/0 error: ~tp", [Error], ?LINE) end. @@ -1747,7 +1773,7 @@ create_keypair() -> %% NOTE: The current version of this command is likely to only work on a unix system. generate_rsa({Realm, KeyName}) -> - KeyDir = filename:join([zomp_dir(), "key", Realm]), + KeyDir = filename:join([zx_lib:zomp_home(), "key", Realm]), ok = force_dir(KeyDir), PemFile = filename:join(KeyDir, KeyName ++ ".pub.pem"), KeyFile = filename:join(KeyDir, KeyName ++ ".key.der"), @@ -1865,7 +1891,7 @@ openssl() -> false -> ok = log(error, "OpenSSL could not be found in this system's PATH."), ok = log(error, "Install OpenSSL and then retry."), - error_exit("Missing system dependenct: OpenSSL", ?FILE, ?LINE); + error_exit("Missing system dependenct: OpenSSL", ?LINE); Path -> log(info, "OpenSSL executable found at: ~ts", [Path]) end, @@ -1884,10 +1910,10 @@ loadkey(Type, {Realm, KeyName}) -> {DerType, Path} = case Type of private -> - P = filename:join([zomp_dir(), "key", Realm, KeyName ++ ".key.der"]), + P = filename:join([zx_lib:zomp_home(), "key", Realm, KeyName ++ ".key.der"]), {'RSAPrivateKey', P}; public -> - P = filename:join([zomp_dir(), "key", Realm, KeyName ++ ".pub.der"]), + P = filename:join([zx_lib:zomp_home(), "key", Realm, KeyName ++ ".pub.der"]), {'RSAPublicKey', P} end, ok = log(info, "Loading key from file ~ts", [Path]), @@ -1930,7 +1956,7 @@ build_plt() -> default_plt() -> - filename:join(zomp_dir(), "basic.plt"). + filename:join(zx_lib:zomp_home(), "basic.plt"). @@ -1947,7 +1973,7 @@ dialyze() -> true -> log(info, "Using PLT: ~tp", [PLT]); false -> build_plt() end, - TmpDir = filename:join(zomp_dir(), "tmp"), + TmpDir = filename:join(zx_lib:zomp_home(), "tmp"), Me = escript:script_name(), EvilTwin = filename:join(TmpDir, filename:basename(Me ++ ".erl")), ok = log(info, "Temporarily reconstructing ~tp as ~tp", [Me, EvilTwin]), @@ -1988,7 +2014,7 @@ create_user(Realm, Username) -> %% realm file to the user. create_realm() -> - ConfFile = filename:join(zomp_dir(), "zomp.conf"), + ConfFile = filename:join(zx_lib:zomp_home(), "zomp.conf"), case file:consult(ConfFile) of {ok, ZompConf} -> create_realm(ZompConf); {error, enoent} -> create_realm([]) @@ -2006,9 +2032,9 @@ create_realm(ZompConf) -> " Names must begin with a lower-case letter.~n", ok = io:format(Instructions), Realm = get_input(), - case valid_lower0_9(Realm) of + case zx_lib:valid_lower0_9(Realm) of true -> - RealmFile = filename:join(zomp_dir(), Realm ++ ".realm"), + RealmFile = filename:join(zx_lib:zomp_home(), Realm ++ ".realm"), case filelib:is_regular(RealmFile) of false -> create_realm(ZompConf, Realm); @@ -2179,7 +2205,7 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort) -> " Names must begin with a lower-case letter.~n", ok = io:format(Instructions), UserName = get_input(), - case valid_lower0_9(UserName) of + case zx_lib:valid_lower0_9(UserName) of true -> create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName); false -> @@ -2206,7 +2232,7 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName) -> ok = io:format(Instructions), Email = get_input(), [User, Host] = string:lexemes(Email, "@"), - case {valid_lower0_9(User), valid_label(Host)} of + case {zx_lib:valid_lower0_9(User), zx_lib:valid_label(Host)} of {true, true} -> create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email); {false, true} -> @@ -2301,8 +2327,8 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email, RealNa {internal_port, InPort}], RealmFN = Realm ++ ".realm", - RealmConf = filename:join(zomp_dir(), RealmFN), - ok = write_terms(RealmConf, RealmSettings), + RealmConf = filename:join(zx_lib:zomp_home(), RealmFN), + ok = zx_lib:write_terms(RealmConf, RealmSettings), {ok, CWD} = file:get_cwd(), {ok, TempDir} = mktemp_dir("zomp"), ok = file:set_cwd(TempDir), @@ -2316,7 +2342,7 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email, RealNa end, TarOpts = [compressed, {cwd, TempDir}], - ok = write_terms(RealmFN, RealmSettings), + ok = zx_lib:write_terms(RealmFN, RealmSettings), ok = KeyCopy(PackagePub), ok = KeyCopy(RealmPub), PublicZRF = filename:join(CWD, Realm ++ ".zrf"), @@ -2324,11 +2350,11 @@ create_realm(ZompConf, Realm, ExAddress, ExPort, InPort, UserName, Email, RealNa ok = erl_tar:create(PublicZRF, [RealmFN, "key"], TarOpts), ok = KeyCopy(SysopPub), - ok = write_terms("zomp.conf", ZompSettings), + ok = zx_lib:write_terms("zomp.conf", ZompSettings), PrimeZRF = filename:join(CWD, Realm ++ ".zpf"), ok = erl_tar:create(PrimeZRF, [RealmFN, "zomp.conf", "key"], TarOpts), - ok = file:set_cwd(zomp_dir()), + ok = file:set_cwd(zx_lib:zomp_home()), KeyBundle = filename:join(CWD, Realm ++ ".zkf"), ok = erl_tar:create(KeyBundle, [KeyDir], [compressed]), @@ -2397,7 +2423,7 @@ create_realmfile(Realm) -> create_realmfile(Realm, Revision, RealmKeyIDs, PackageKeyIDs) -> {ok, CWD} = file:get_cwd(), - ok = file:set_cwd(zomp_dir()), + ok = file:set_cwd(zx_lib:zomp_home()), KeyPath = fun({R, K}) -> filename:join(["key", R, K ++ ".pub.der"]) end, RealmKeyPaths = lists:map(KeyPath, RealmKeyIDs), PackageKeyPaths = lists:map(KeyPath, PackageKeyIDs), @@ -2422,13 +2448,19 @@ create_sysop() -> -spec install(package_id()) -> ok. %% @private %% Install a package from the cache into the local system. +%% Before calling this function it must be known that: +%% - The zrp file is in the cache +%% - The zrp file is valid +%% - This function will only be called on startup by the launch process +%% - The package is not already installed +%% - If this function crashes it will completely halt the system install(PackageID) -> PackageString = package_string(PackageID), ok = log(info, "Installing ~ts", [PackageString]), - ZrpFile = filename:join("zrp", namify_zrp(PackageID)), - Files = extract_zrp(ZrpFile), - TgzFile = namify_tgz(PackageID), + ZrpFile = filename:join("zrp", zx_lib:namify_zrp(PackageID)), + Files = extract_zrp_or_die(ZrpFile), + TgzFile = zx_lib:namify_tgz(PackageID), {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), Meta = binary_to_term(MetaBin), @@ -2442,29 +2474,6 @@ install(PackageID) -> log(info, "~ts installed", [PackageString]). --spec extract_zrp(FileName) -> Files | no_return() - when FileName :: file:filename(), - Files :: [{file:filename(), binary()}]. -%% @private -%% Extract a zrp archive, if possible. If not possible, halt execution with as accurate -%% an error message as can be managed. - -extract_zrp(FileName) -> - case erl_tar:extract(FileName, [memory]) of - {ok, Files} -> - Files; - {error, {FileName, enoent}} -> - Message = "Can't find file ~ts.", - error_exit(Message, [FileName], ?FILE, ?LINE); - {error, invalid_tar_checksum} -> - Message = "~ts is not a valid zrp archive.", - error_exit(Message, [FileName], ?FILE, ?LINE); - {error, Reason} -> - Message = "Extracting package file failed with: ~160tp.", - error_exit(Message, [Reason], ?FILE, ?LINE) - end. - - -spec verify(Data, Signature, PubKey) -> ok | no_return() when Data :: binary(), Signature :: binary(), @@ -2472,11 +2481,12 @@ extract_zrp(FileName) -> %% @private %% Verify the RSA Signature of some Data against the given PubKey or halt execution. %% This function always assumes sha512 is the algorithm being used. +%% Should only ever be called by the initial launch process. verify(Data, Signature, PubKey) -> case public_key:verify(Data, sha512, Signature, PubKey) of true -> ok; - false -> error_exit("Bad package signature!", ?FILE, ?LINE) + false -> error_exit("Bad package signature!", ?LINE) end. @@ -2527,7 +2537,7 @@ request_zrp(Socket, PackageID) -> receive_zrp(Socket, PackageID) -> receive {tcp, Socket, Bin} -> - ZrpPath = filename:join("zrp", namify_zrp(PackageID)), + ZrpPath = filename:join("zrp", zx_lib:namify_zrp(PackageID)), ok = file:write_file(ZrpPath, Bin), ok = send(Socket, ok), log(info, "Wrote ~ts", [ZrpPath]); @@ -2559,63 +2569,6 @@ mktemp_dir(Prefix) -> %%% Utility functions --spec read_meta() -> package_meta() | no_return(). -%% @private -%% @equiv read_meta(".") - -read_meta() -> - read_meta("."). - - --spec read_meta(Dir) -> package_meta() | no_return() - when Dir :: file:filename(). -%% @private -%% Read the `zomp.meta' file from the indicated directory, if possible. If not possible -%% then halt execution with an appropriate error message. - -read_meta(Dir) -> - Path = filename:join(Dir, "zomp.meta"), - case file:consult(Path) of - {ok, Meta} -> - maps:from_list(Meta); - Error -> - ok = log(error, "Failed to open \"zomp.meta\" with ~tp", [Error]), - ok = log(error, "Wrong directory?"), - halt(1) - end. - - --spec write_meta(package_meta()) -> ok. -%% @private -%% @equiv write_meta(".") - -write_meta(Meta) -> - write_meta(".", Meta). - - --spec write_meta(Dir, Meta) -> ok - when Dir :: file:filename(), - Meta :: package_meta(). -%% @private -%% Write the contents of the provided meta structure (a map these days) as a list of -%% Erlang K/V terms. - -write_meta(Dir, Meta) -> - Path = filename:join(Dir, "zomp.meta"), - ok = write_terms(Path, maps:to_list(Meta)). - - --spec write_terms(Filename, Terms) -> ok - when Filename :: file:filename(), - Terms :: [term()]. -%% @private -%% Provides functionality roughly inverse to file:consult/1. - -write_terms(Filename, List) -> - Format = fun(Term) -> io_lib:format("~tp.~n", [Term]) end, - Text = lists:map(Format, List), - file:write_file(Filename, Text). - -spec build(package_id()) -> ok. %% @private @@ -2623,7 +2576,7 @@ write_terms(Filename, List) -> build(PackageID) -> {ok, CWD} = file:get_cwd(), - ok = file:set_cwd(package_home(PackageID)), + ok = file:set_cwd(zx_lib:package_dir(PackageID)), ok = build(), file:set_cwd(CWD). @@ -2649,30 +2602,6 @@ build() -> ok. --spec scrub(Deps) -> Scrubbed - when Deps :: [package_id()], - Scrubbed :: [package_id()]. -%% @private -%% Take a list of dependencies and return a list of dependencies that are not yet -%% installed on the system. - -scrub([]) -> - []; -scrub(Deps) -> - lists:filter(fun(PackageID) -> not installed(PackageID) end, Deps). - - --spec installed(package_id()) -> boolean(). -%% @private -%% True to its name, returns `true' if the package is installed (its directory found), -%% `false' otherwise. - -installed(PackageID) -> - PackageString = package_string(PackageID), - PackageDir = filename:join("lib", PackageString), - filelib:is_dir(PackageDir). - - -spec rm_rf(file:filename()) -> ok | {error, file:posix()}. %% @private %% Recursively remove files and directories, equivalent to `rm -rf' on unix. @@ -2689,215 +2618,9 @@ rm_rf(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. - - - -%%% Input argument mangling - - --spec valid_lower0_9(string()) -> boolean(). -%% @private -%% Check whether a provided string is a valid lower0_9. - -valid_lower0_9([Char | Rest]) - when $a =< Char, Char =< $z -> - valid_lower0_9(Rest, Char); -valid_lower0_9(_) -> - false. - - --spec valid_lower0_9(String, Last) -> boolean() - when String :: string(), - Last :: char(). - -valid_lower0_9([$_ | _], $_) -> - false; -valid_lower0_9([Char | Rest], _) - when $a =< Char, Char =< $z; - $0 =< Char, Char =< $9; - Char == $_ -> - valid_lower0_9(Rest, Char); -valid_lower0_9([], _) -> - true; -valid_lower0_9(_, _) -> - false. - - --spec valid_label(string()) -> boolean(). -%% @private -%% Check whether a provided string is a valid label. - -valid_label([Char | Rest]) - when $a =< Char, Char =< $z -> - valid_label(Rest, Char); -valid_label(_) -> - false. - - --spec valid_label(String, Last) -> boolean() - when String :: string(), - Last :: char(). - -valid_label([$. | _], $.) -> - false; -valid_label([$_ | _], $_) -> - false; -valid_label([$- | _], $-) -> - false; -valid_label([Char | Rest], _) - when $a =< Char, Char =< $z; - $0 =< Char, Char =< $9; - Char == $_; Char == $-; - Char == $. -> - valid_label(Rest, Char); -valid_label([], _) -> - true; -valid_label(_, _) -> - false. - - --spec string_to_version(string()) -> version(). -%% @private -%% @equiv string_to_version(string(), "", {z, z, z}) - -string_to_version(String) -> - string_to_version(String, "", {z, z, z}). - - --spec string_to_version(String, Acc, Version) -> Result - when String :: string(), - Acc :: list(), - Version :: version(), - Result :: version(). -%% @private -%% Accepts a full or partial version string of the form `X.Y.Z', `X.Y' or `X' and -%% returns a zomp-type version tuple or crashes on bad data. - -string_to_version([Char | Rest], Acc, Version) when $0 =< Char andalso Char =< $9 -> - string_to_version(Rest, [Char | Acc], Version); -string_to_version([$. | Rest], Acc, {z, z, z}) -> - X = list_to_integer(lists:reverse(Acc)), - string_to_version(Rest, "", {X, z, z}); -string_to_version([$. | Rest], Acc, {X, z, z}) -> - Y = list_to_integer(lists:reverse(Acc)), - string_to_version(Rest, "", {X, Y, z}); -string_to_version("", "", Version) -> - Version; -string_to_version([], Acc, {z, z, z}) -> - X = list_to_integer(lists:reverse(Acc)), - {X, z, z}; -string_to_version([], Acc, {X, z, z}) -> - Y = list_to_integer(lists:reverse(Acc)), - {X, Y, z}; -string_to_version([], Acc, {X, Y, z}) -> - Z = list_to_integer(lists:reverse(Acc)), - {X, Y, Z}. - - --spec version_to_string(version()) -> string(). -%% @private -%% Inverse of string_to_version/3. - -version_to_string({z, z, z}) -> - ""; -version_to_string({X, z, z}) -> - integer_to_list(X); -version_to_string({X, Y, z}) -> - lists:flatten(lists:join($., [integer_to_list(Element) || Element <- [X, Y]])); -version_to_string({X, Y, Z}) -> - lists:flatten(lists:join($., [integer_to_list(Element) || Element <- [X, Y, Z]])). - - --spec package_id(string()) -> package_id(). -%% @private -%% Converts a proper package_string to a package_id(). -%% This function takes into account missing version elements. -%% Examples: -%% `{"foo", "bar", {1, 2, 3}} = package_id("foo-bar-1.2.3")' -%% `{"foo", "bar", {1, 2, z}} = package_id("foo-bar-1.2")' -%% `{"foo", "bar", {1, z, z}} = package_id("foo-bar-1")' -%% `{"foo", "bar", {z, z, z}} = package_id("foo-bar")' - -package_id(String) -> - case string:lexemes(String, [$-]) of - [Realm, Name, VersionString] -> - true = valid_lower0_9(Realm), - true = valid_lower0_9(Name), - Version = string_to_version(VersionString), - {Realm, Name, Version}; - [A, B] -> - true = valid_lower0_9(A), - case valid_lower0_9(B) of - true -> {A, B, {z, z, z}}; - false -> {"otpr", A, string_to_version(B)} - end; - [Name] -> - true = valid_lower0_9(Name), - {"otpr", Name, {z, z, z}} - end. - - --spec package_string(package_id()) -> string(). -%% @private -%% Map an PackageID to a correct string representation. -%% This function takes into account missing version elements. -%% Examples: -%% `"foo-bar-1.2.3" = package_string({"foo", "bar", {1, 2, 3}})' -%% `"foo-bar-1.2" = package_string({"foo", "bar", {1, 2, z}})' -%% `"foo-bar-1" = package_string({"foo", "bar", {1, z, z}})' -%% `"foo-bar" = package_string({"foo", "bar", {z, z, z}})' - -package_string({Realm, Name, {z, z, z}}) -> - lists:flatten(lists:join($-, [Realm, Name])); -package_string({Realm, Name, Version}) -> - VersionString = version_to_string(Version), - lists:flatten(lists:join($-, [Realm, Name, VersionString])). - - --spec namify_zrp(PackageID) -> ZrpFileName - when PackageID :: package_id(), - ZrpFileName :: file:filename(). -%% @private -%% Map an PackageID to its correct .zrp package file name. - -namify_zrp(PackageID) -> namify(PackageID, "zrp"). - - --spec namify_tgz(PackageID) -> TgzFileName - when PackageID :: package_id(), - TgzFileName :: file:filename(). -%% @private -%% Map an PackageID to its correct gzipped tarball source bundle filename. - -namify_tgz(PackageID) -> namify(PackageID, "tgz"). - - --spec namify(PackageID, Suffix) -> FileName - when PackageID :: package_id(), - Suffix :: string(), - FileName :: file:filename(). -%% @private -%% Converts an PackageID to a canonical string, then appends the provided -%% filename Suffix. - -namify(PackageID, Suffix) -> - PackageString = package_string(PackageID), - PackageString ++ "." ++ Suffix. - - %%% User menu interface (terminal) - -spec get_input() -> string(). %% @private %% Provide a standard input prompt and newline sanitized return value. @@ -2988,7 +2711,7 @@ hurr() -> io:format("That isn't an option.~n"). %% Every entry function should run this initially. ensure_zomp_home() -> - ZompDir = zomp_dir(), + ZompDir = zx_lib:zomp_home(), case filelib:is_dir(ZompDir) of true -> ok; false -> setup(ZompDir) @@ -3014,65 +2737,21 @@ setup_otpr() -> log(info, "Here should pull otpr.0.zrf and install it..."). --spec zomp_dir() -> file:filename(). -%% @private -%% Check the host OS and return the absolute path to the zomp filesystem root. - -zomp_dir() -> - case os:type() of - {unix, _} -> - Home = os:getenv("HOME"), - Dir = "zomp", - filename:join(Home, Dir); - {win32, _} -> - Drive = os:getenv("HOMEDRIVE"), - Path = os:getenv("HOMEPATH"), - Dir = "zomp", - filename:join([Drive, Path, Dir]) - end. - - -spec ensure_package_dirs(package_id()) -> ok. %% @private %% Procedure to guarantee that directory locations necessary for the indicated app to %% run have been created or halt execution. -ensure_package_dirs(PackageID) -> - PackageHome = package_home(PackageID), - PackageData = package_dir("var", PackageID), - PackageConf = package_dir("etc", PackageID), +ensure_package_dirs(PackageID = {Realm, Name, _}) -> + Package = {Realm, Name}, + PackageHome = zx_lib:package_dir(PackageID), + PackageData = zx_lib:package_dir("var", Package), + PackageConf = zx_lib:package_dir("etc", Package), Dirs = [PackageHome, PackageData, PackageConf], ok = lists:foreach(fun force_dir/1, Dirs), log(info, "Created dirs:~n\t~ts~n\t~ts~n\t~ts", Dirs). --spec package_home(PackageID) -> PackageHome - when PackageID :: package_id(), - PackageHome :: file:filename(). -%% @private -%% Accept an PackageID and return the installation directory for the indicated -%% application. -%% NOTE: -%% This system does NOT anticipate symlinks of incomplete versions to their latest -%% installed version (for example, an incomplete `{1, 2, z}' resolving to a symlink -%% `lib/foo-bar-1.2' which is always updated to point to the latest version 1.2.x). - -package_home(PackageID) -> - filename:join([zomp_dir(), "lib", package_string(PackageID)]). - - --spec package_dir(Prefix, PackageID) -> PackageDataDir - when Prefix :: string(), - PackageID :: package_id(), - PackageDataDir :: file:filename(). -%% @private -%% Create an absolute path to an application directory prefixed by the inclued argument. - -package_dir(Prefix, {Realm, Name, _}) -> - PackageName = Realm ++ "-" ++ Name, - filename:join([zomp_dir(), Prefix, PackageName]). - - -spec force_dir(Path) -> Result when Path :: file:filename(), Result :: ok @@ -3105,7 +2784,7 @@ realm_conf(Realm) -> %% Load the config for the given realm or halt with an error. load_realm_conf(Realm) -> - Path = filename:join(zomp_dir(), realm_conf(Realm)), + Path = filename:join(zx_lib:zomp_home(), realm_conf(Realm)), case file:consult(Path) of {ok, C} -> C; @@ -3115,6 +2794,29 @@ load_realm_conf(Realm) -> end. +-spec extract_zrp_or_die(FileName) -> Files | no_return() + when FileName :: file:filename(), + Files :: [{file:filename(), binary()}]. +%% @private +%% Extract a zrp archive, if possible. If not possible, halt execution with as accurate +%% an error message as can be managed. + +extract_zrp_or_die(FileName) -> + case erl_tar:extract(FileName, [memory]) of + {ok, Files} -> + Files; + {error, {FileName, enoent}} -> + Message = "Can't find file ~ts.", + error_exit(Message, [FileName], ?LINE); + {error, invalid_tar_checksum} -> + Message = "~ts is not a valid zrp archive.", + error_exit(Message, [FileName], ?LINE); + {error, Reason} -> + Message = "Extracting package file failed with: ~160tp.", + error_exit(Message, [Reason], ?LINE) + end. + + %%% Usage @@ -3191,28 +2893,24 @@ usage() -> %%% Error exits --spec error_exit(Error, Path, Line) -> no_return() +-spec error_exit(Error, Line) -> no_return() when Error :: term(), - Path :: file:filename(), Line :: non_neg_integer(). %% @private %% Format an error message in a way that makes it easy to locate. -error_exit(Error, Path, Line) -> - File = filename:basename(Path), - ok = log(error, "~ts:~tp: ~tp", [File, Line, Error]), - halt(1). +error_exit(Error, Line) -> + error_exit(Error, [], Line). --spec error_exit(Format, Args, Path, Line) -> no_return() +-spec error_exit(Format, Args, Line) -> no_return() when Format :: string(), Args :: [term()], - Path :: file:filename(), Line :: non_neg_integer(). %% @private %% Format an error message in a way that makes it easy to locate. -error_exit(Format, Args, Path, Line) -> - File = filename:basename(Path), +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 new file mode 100644 index 0000000..f585bc7 --- /dev/null +++ b/zomp/lib/otpr-zx/0.1.0/src/zx_conn.erl @@ -0,0 +1,71 @@ +%%% @doc +%%% ZX Connector +%%% +%%% This module represents a connection to a Zomp server. +%%% Multiple connections can exist at a given time, but each one of these processes +%%% only represents a single connection at a time. +%%% @end + +-module(zx_conn). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0"). + +-export([start/1, stop/0]). +-export([start_link/1]). + +-include("zx_logger.erl"). + + + +%%% Startup + +-spec start(Target) -> Result + when Target :: zx:host(), + Result :: {ok, pid()} + | {error, Reason}, + Reason :: term(). + +start(Target) -> + zx_conn_sup:start_conn(Target). + + +-spec start_link(Target) -> + when Target :: zx:host(), + Result :: {ok, pid()} + | {error, Reason}, + Reason :: term(). +%% @private +%% Starts a connector with a target host in its state. + +start_link(Target) -> + proc_lib:start_link(?MODULE, init, [self(), Target]). + + +-spec init(Parent, Target) -> no_return() + when Parent :: pid(), + Target :: zx:host(). + +init(Parent, Target) -> + ok = log(info, "Connecting to ~tp", [Target]), + Debug = sys:debug_options([]), + ok = proc_lib:init_ack(Parent, {ok, self()}), + connect(Parent, Debug, Target). + + +-spec connect(Parent, Debug, Target) -> no_return(). + +connect(Parent, Debug, {Host, Port}) -> + Options = [{packet, 4}, {mode, binary}, {active, true}], + case gen_tcp:connect(Host, Port, Options, 5000) of + {ok, Socket} -> + confirm(Parent, Debug, Socket); + {error, Error} -> + ok = log(warning, "Connection problem with ~tp: ~tp", [Node, Error]), + ok = zx_daemon:report( + connect_user(Realm, Rest) + end. + + +confirm(Parent, Debug, Socket) -> + diff --git a/zomp/lib/otpr-zx/0.1.0/src/zx_conn_sup.erl b/zomp/lib/otpr-zx/0.1.0/src/zx_conn_sup.erl new file mode 100644 index 0000000..7efd8cd --- /dev/null +++ b/zomp/lib/otpr-zx/0.1.0/src/zx_conn_sup.erl @@ -0,0 +1,72 @@ +%%% @doc +%%% The ZX Connection Supervisor +%%% +%%% This supervisor maintains the lifecycle of all zomp_client worker processes. +%%% @end + +-module(zx_conn_sup). +-behavior(supervisor). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0"). + +-export([start_conn/1]). +-export([start_link/0]). +-export([init/1]). + + + +%%% Interface Functions + +-spec start_conn(Host) -> Result + when Host :: zx:host(), + Result :: {ok, pid()} + | {error, Reason}, + Reason :: term(). +%% @doc +%% Start an upstream connection handler. +%% (Should only be called from zx_conn). + +start_conn(Host) -> + supervisor:start_child(?MODULE, [Host]). + + + +%%% Startup + +-spec start_link() -> Result + when Result :: {ok, pid()} + | {error, Reason}, + Reason :: {already_started, pid()} + | {shutdown, term()} + | term(). +%% @private +%% Called by zx_daemon_sup. +%% +%% Spawns a single, registered supervisor process. +%% +%% Error conditions, supervision strategies, and other important issues are +%% explained in the supervisor module docs: +%% http://erlang.org/doc/man/supervisor.html + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, none). + + +-spec init(none) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +%% @private +%% Do not call this function directly -- it is exported only because it is a +%% necessary part of the OTP supervisor behavior. + +init(none) -> + RestartStrategy = {simple_one_for_one, 1, 60}, + + Client = {zx_conn, + {zx_conn, start_link, []}, + temporary, + brutal_kill, + worker, + [zx_conn]}, + + Children = [Client], + {ok, {RestartStrategy, Children}}. diff --git a/zomp/lib/otpr-zx/0.1.0/src/zx_daemon.erl b/zomp/lib/otpr-zx/0.1.0/src/zx_daemon.erl index e8073a0..d7efa4c 100644 --- a/zomp/lib/otpr-zx/0.1.0/src/zx_daemon.erl +++ b/zomp/lib/otpr-zx/0.1.0/src/zx_daemon.erl @@ -2,71 +2,456 @@ %%% ZX Daemon %%% %%% Resident execution daemon and runtime interface to Zomp. +%%% +%%% The daemon lives in the system and does background tasks and also acts as the +%%% serial interface for any complex functions involving network tasks that may fail +%%% and need to be retried or may span realms. +%%% +%%% In particular, the functions accessible to programs launched by ZX can interact +%%% with Zomp realms via the zx_daemon, and non-administrative tasks that involve +%%% maintaining a connection with a Zomp constellation can be abstracted behind the +%%% zx_daemon. Administrative tasks, however, essentially stateless request/response +%%% pairs. %%% @end -module(zx_daemon). --export([]). +-behavior(gen_server). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0"). -%%% App execution loop +-export([pass_meta/3, + subscribe/1, unsubscribe/0, + list_packages/1, list_versions/1, query_latest/1, + fetch/1]). +-export([report/1]). +-export([start_link/0]). +-export([init/1, handle_call/3, handle_cast/2, handle_info/2, + code_change/3, terminate/2]). --spec exec_wait(State) -> no_return() - when State :: state(). +-include("zx_logger.hrl"). + + + +%%% Type Definitions + +-record(s, + {meta = none :: none | zx:package_meta(), + dir = none :: none | file:filename(), + argv = none :: none | [string()], + reqp = none :: none | pid(), + reqm = none :: none | reference(), + connp = none :: none | pid(), + connm = none :: none | reference(), + prime = none :: none | zx:realm(), + hosts = [] :: #s{zx:realm() := [zx:host()]}}). + +-type state() :: #s{}. + +-type hosts() :: #{zx:realm() := [zx:host()]}. +-type conn_report() :: {connected, Realms :: [zx:realm()]} + | conn_fail + | disconnected. + + +%%% Service Interface + +-spec pass_meta(Meta, Dir, ArgV) -> ok + when Meta :: zx:package_meta(), + Dir :: file:filename(), + ArgV :: [string()]. %% @private -%% Execution maintenance loop. -%% Once an application is started by zompc this process will wait for a message from -%% the application if that application was written in a way to take advantage of zompc -%% facilities such as post-start upgrade checking. +%% Load the daemon with the primary running application's meta data and location within +%% the filesystem. This step allows running development code from any location in +%% the filesystem against installed dependencies without requiring any magical +%% references. %% -%% NOTE: -%% Adding clauses to this `receive' is where new functionality belongs. -%% It may make sense to add a `zompc_lib' as an available dependency authors could -%% use to interact with zompc without burying themselves under the complexity that -%% can come with naked send operations. (Would it make sense, for example, to have -%% the registered zompc process convert itself to a gen_server via zompc_lib to -%% provide more advanced functionality?) +%% This call blocks specifically so that we can be certain that the target application +%% cannot be started before the impact of this call has taken full effect. It cannot +%% be known whether the very first thing the target application will do is send this +%% process an async message. That implies that this should only ever be called once, +%% by the launching process (which normally terminates shortly thereafter). -exec_wait(State = #s{pid = none, mon = none}) -> +pass_meta(Meta, Dir, ArgV) -> + gen_server:call(?MODULE, {pass_meta, Meta, Dir, ArgV}). + + +-spec subscribe(Package) -> Result + when Package :: zx:package(), + Result :: ok + | {error, Reason}, + Reason :: illegal_requestor + | {already_subscribed, zx:package()}. +%% @doc +%% Subscribe to update notification for a for a particular package. +%% The daemon is designed to monitor a single package at a time, so a second call to +%% subscribe/1 will return an `already_subscribed' error, or possibly an +%% `illegal_requestor' error in the case that a second call is made from a different +%% process than the original requestor. +%% Other functions can be used to query the status of a package at an arbitrary time. + +subscribe(Package) -> + gen_server:call(?MODULE, {subscribe, self(), Package}). + + +-spec unsubscribe() -> ok. +%% @doc +%% Instructs the daemon to unsubscribe if subscribed. Has no effect if not subscribed. + +unsubscribe() -> + gen_server:call(?MODULE, unsubscribe). + + +-spec list_packages(Realm) -> Result + when Realm :: zx:realm(), + Result :: {ok, Packages :: [zx:package()]} + | {error, Reason}, + Reason :: bad_realm + | no_realm + | network. + +list_packages(Realm) -> + gen_server:call(?MODULE, {list, Realm}). + + +-spec list_versions(Package) -> Result + when Package :: zx:package(), + Result :: {ok, Versions :: [zx:version()]} + | {error, Reason}, + Reason :: bad_realm + | bad_package + | network. +%% @doc +%% List all versions of a given package. Useful especially for developers wanting to +%% see a full list of maintained packages to include as dependencies. + +list_versions(Package) -> + gen_server:call(?MODULE, {list_versions, Package}). + + +-spec query_latest(Object) -> Result + when Object :: zx:package() | zx:package_id(), + Result :: {ok, version()} + | {error, Reason}, + Reason :: bad_realm + | bad_package + | bad_version + | network. +%% @doc +%% Check for the latest version of a package, with or without a version provided to +%% indicate subversion limit. Useful mostly for developers checking for a latest +%% version of a package. +%% +%% While this function could be used as a primitive operation in a dynamic dependency +%% upgrade scheme, that is not its intent. You will eventually divide by zero trying +%% to implement such a feature, open a portal to Oblivion, and monsters will consume +%% all you love. See? That's a horrible idea. You have been warned. + +query_latest(Object) -> + gen_server:call(?MODULE, {query_latest, Object}). + + +-spec fetch(PackageIDs) -> Result + when PackageIDs :: [zx:package_id()], + Result :: {{ok, [zx:package_id()]}, + {error, [{zx:package_id(), Reason}]}}, + Reason :: bad_realm + | bad_package + | bad_version + | network. +%% @doc +%% Ensure a list of packages is available locally, fetching any missing packages in +%% the process. This is intended to abstract the task of ensuring that a list of +%% dependencies is available locally prior to building/running a dependent app or lib. + +fetch([]) -> + {{ok, []}, {error, []}}; +fetch(PackageIDs) -> + gen_server:call(?MODULE, {fetch, PackageIDs}). + + + +%%% Connection interface + +-spec report(Message) -> ok + when Message :: {connected, Realms :: [zx:realm()]} + | conn_fail + | disconnected. +%% @private +%% Should only be called by a zx_conn. This function is how a zx_conn reports its +%% current connection status. + +report(Message) -> + gen_server:cast(?MODULE, {report, self(), Message}). + + +%%% Startup + +-spec start_link() -> {ok, pid()} | {error, term()}. +%% @private +%% Startup function -- intended to be called by supervisor. + +start_link() -> + gen_server:start_link({local, ?MODULE}, ?MODULE, none, []). + + +-spec init(none) -> {ok, state()}. + +init(none) -> + {ok, #s{}}. + + + +%%% gen_server + +%% @private +%% gen_server callback for OTP calls + +handle_call({pass_meta, Meta, Dir, ArgV}, _, State) -> + {Result, NewState} = do_pass_meta(Requestor, Package, ArgV, State), + {reply, Result, NewState}; +handle_call({subscribe, Requestor, Package}, _, State) -> + {Result, NewState} = do_subscribe(Requestor, Package, State), + {reply, Result, NewState}; +handle_call({query_latest, Object}, _, State) -> + {Result, NewState} = do_query_latest(Object, State), + {reply, Result, NewState}; +handle_call({fetch, Packages}, _, State) -> + {Result, NewState} = do_fetch(Packages, State), + {reply, Result, NewState}; +handle_call(Unexpected, From, State) -> + ok = log(warning, "Unexpected call ~tp: ~tp", [From, Unexpected]), + {noreply, State}. + + +%% @private +%% gen_server callback for OTP casts + +handle_cast(unsubscribe, State) -> + NewState = do_unsubscribe(State), + {noreply, NewState}; +handle_cast({report, From, Message}, State) -> + NewState = do_report(From, Message, State), + {noreply, NewState}; +handle_cast(Unexpected, State) -> + ok = log(warning, "Unexpected cast: ~tp", [Unexpected]), + {noreply, State}. + + +%% @private +%% gen_sever callback for general Erlang message handling + +handle_info(Unexpected, State) -> + ok = log(warning, "Unexpected info: ~tp", [Unexpected]), + {noreply, State}. + + +%% @private +%% gen_server callback to handle state transformations necessary for hot +%% code updates. This template performs no transformation. + +code_change(_, State, _) -> + {ok, State}. + + +%% @private +%% gen_server callback to handle shutdown/cleanup tasks on receipt of a clean +%% termination request. + +terminate(_, _) -> + ok. + + + +%%% Doer Functions + +-spec do_pass_meta(Meta, Dir, ArgV, State) -> {Result, NewState} + when Meta :: zx:package_meta(), + Dir :: file:filename(), + ArgV :: [string()], + State :: state(), + Result :: ok, + Newstate :: state(). + +do_pass_meta(Meta, Dir, ArgV, State) -> + NewState = State#s{meta = Meta, dir = Dir, argv = ArgV}, + {ok, NewState}. + + +-spec do_subscribe(Requestor, Package, State) -> {Result, NewState} + when Requestor :: pid(), + Package :: zx:package(), + State :: state(), + Result :: ok + | {error, Reason}, + Reason :: illegal_requestor + | {already_subscribed, zx:package()}, + NewState :: state(). + +do_subscribe(Requestor, + {Realm, Name}, + State = #s{name = none, connp = none, reqp = none, hosts = Hosts}) -> + Monitor = monitor(process, Requestor), + {Host, NewHosts} = select_host(Realm, Hosts), + {ok, ConnP} = zx_conn:start(Host), + ConnM = monitor(process, ConnP), + NewState = State#s{realm = Realm, name = Name, + connp = ConnP, connm = ConnM, + reqp = Requestor, reqm = Monitor, + hosts = NewHosts}, + {ok, NewState}; +do_subscribe(_, _, State = #s{realm = Realm, name = Name}) -> + {{error, {already_subscribed, {Realm, Name}}}, State}. + + +-spec select_host(Realm, Hosts) -> {Host, NewHosts} + when Realm :: zx:realm(), + Hosts :: none | hosts(), + Host :: zx:host(), + NewHosts :: hosts(). + +select_host(Realm, none) -> + List = + case file:consult(zx_lib:hosts_cache_file(Realm)) of + {ok, Cached} -> Cached; + {error, enoent} -> [zx_lib:get_prime(Realm)] + end, + NewState = State#s{hosts = #{Realm => List}}, + select_host(Realm, NewState); +select_host(Realm, Hosts) -> + {Target, Rest} = + case maps:find(Realm, Hosts) of + {ok, [H | Hs]} -> {H, Hs}; + {ok, []} -> {zx_lib:get_prime(Realm), []}; + error -> {zx_lib:get_prime(Realm), []} + end, + NewHosts = maps:put(Realm, Hosts, Rest), + {Target, NewHosts}. + + +-spec do_query_latest(Object, State) -> {Result, NewState} + when Object :: zx:package() | zx:package_id(), + State :: state(), + Result :: {ok, zx:version()} + | {error, Reason}, + Reason :: bad_realm + | bad_package + | bad_version, + NewState :: state(). +%% @private +%% Queries a zomp realm for the latest version of a package or package +%% version (complete or incomplete version number). + +query_latest(Socket, {Realm, Name}) -> + ok = send(Socket, {latest, Realm, Name}), receive - {monitor, Pid} -> - Mon = monitor(process, Pid), - exec_wait(State#s{pid = Pid, mon = Mon}); - Unexpected -> - ok = log(warning, "Unexpected message: ~tp", [Unexpected]), - exec_wait(State) + {tcp, Socket, Bin} -> binary_to_term(Bin) + after 5000 -> {error, timeout} end; -exec_wait(State = #s{pid = Pid, mon = Mon}) -> +query_latest(Socket, {Realm, Name, Version}) -> + ok = send(Socket, {latest, Realm, Name, Version}), receive - {check_update, Requester, Ref} -> - {Response, NewState} = check_update(State), - Requester ! {Ref, Response}, - exec_wait(NewState); - {exit, Reason} -> - ok = log(info, "Exiting with: ~tp", [Reason]), - halt(0); - {'DOWN', Mon, process, Pid, normal} -> - ok = log(info, "Application exited normally."), - halt(0); - {'DOWN', Mon, process, Pid, Reason} -> - ok = log(warning, "Application exited with: ~tp", [Reason]), - halt(1); - Unexpected -> - ok = log(warning, "Unexpected message: ~tp", [Unexpected]), - exec_wait(State) + {tcp, Socket, Bin} -> binary_to_term(Bin) + after 5000 -> {error, timeout} end. --spec check_update(State) -> {Response, NewState} +-spec do_unsubscribe(State) -> {ok, NewState} when State :: state(), - Response :: term(), NewState :: state(). -%% @private -%% Check for updated version availability of the current application. -%% The return value should probably provide up to three results, a Major, Minor and -%% Patch update, and allow the Requestor to determine what to do with it via some -%% interaction. -check_update(State) -> - ok = log(info, "Would be checking for an update of the current application now..."), - Response = "Nothing was checked, but you can imagine it to have been.", - {Response, State}. +do_unsubscribe(State = #s{connp = none}) -> + {ok, State}; +do_unsubscribe(State = #s{connp = ConnP, connm = ConnM}) -> + true = demonitor(ConnM), + ok = zx_conn:stop(ConnP), + NewState = State#s{realm = none, name = none, version = none, + connp = ConnP, connm = ConnM}, + {ok, NewState}. + + +-spec do_report(From, Message, State) -> NewState + when From :: pid(), + Message :: conn_report(), + State :: state(), + NewState :: state(). + +do_report(From, {connected, Realms}, State = #s{ + + + +-spec do_fetch(PackageIDs) -> Result + when PackageIDs :: [zx:package_id()], + Result :: ok + | {error, Reason}, + Reason :: bad_realm + | bad_package + | bad_version + | network. +%% @private +%% + +do_fetch(PackageIDs, State) -> +% FIXME: Need to create a job queue divided by realm and dispatched to connectors, +% and cleared from the master pending queue kept here by the daemon as the +% workers succeed. Basic task queue management stuff... which never existed +% in ZX before... grrr... + case scrub(PackageIDs) of + [] -> + ok; + Needed -> + Partitioned = partition_by_realm(Needed), + EnsureDeps = + fun({Realm, Packages}) -> + ok = zx_conn:queue_package(Pid, Realm, Packages), + log(info, "Disconnecting from realm: ~ts", [Realm]) + end, + lists:foreach(EnsureDeps, Partitioned) + end. + + +partition_by_realm(PackageIDs) -> + PartitionMap = lists:foldl(fun partition_by_realm/2, #{}, PackageIDs), + maps:to_list(PartitionMap). + + +partition_by_realm({R, P, V}, M) -> + maps:update_with(R, fun(Ps) -> [{P, V} | Ps] end, [{P, V}], M). + + +ensure_deps(_, _, []) -> + ok; +ensure_deps(Socket, Realm, [{Name, Version} | Rest]) -> + ok = ensure_dep(Socket, {Realm, Name, Version}), + ensure_deps(Socket, Realm, Rest). + + +-spec ensure_dep(gen_tcp:socket(), package_id()) -> ok | no_return(). +%% @private +%% Given an PackageID as an argument, check whether its package file exists in the +%% system cache, and if not download it. Should return `ok' whenever the file is +%% sourced, but exit with an error if it cannot locate or acquire the package. + +ensure_dep(Socket, PackageID) -> + ZrpFile = filename:join("zrp", namify_zrp(PackageID)), + ok = + case filelib:is_regular(ZrpFile) of + true -> ok; + false -> fetch(Socket, PackageID) + end, + ok = install(PackageID), + build(PackageID). + + +-spec scrub(Deps) -> Scrubbed + when Deps :: [package_id()], + Scrubbed :: [package_id()]. +%% @private +%% Take a list of dependencies and return a list of dependencies that are not yet +%% installed on the system. + +scrub([]) -> + []; +scrub(Deps) -> + lists:filter(fun(PackageID) -> not zx_lib:installed(PackageID) end, Deps). diff --git a/zomp/lib/otpr-zx/0.1.0/src/zx_daemon_sup.erl b/zomp/lib/otpr-zx/0.1.0/src/zx_daemon_sup.erl new file mode 100644 index 0000000..52f04f0 --- /dev/null +++ b/zomp/lib/otpr-zx/0.1.0/src/zx_daemon_sup.erl @@ -0,0 +1,60 @@ +%%% @doc +%%% ZX Daemon Supervisor +%%% +%%% This supervisor maintains the lifecycle of the zxd worker process. +%%% @end + +-module(zx_daemon_sup). +-behavior(supervisor). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0"). + +-export([start_link/0, init/1]). + + + +%%% Startup + +-spec start_link() -> Result + when Result :: {ok, pid()} + | {error, Reason}, + Reason :: {already_started, pid()} + | {shutdown, term()} + | term(). +%% @private +%% Called by zx:subscribe/1. +%% Starts this single, registered supervisor. +%% +%% Error conditions, supervision strategies, and other important issues are +%% explained in the supervisor module docs: +%% http://erlang.org/doc/man/supervisor.html + +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, none). + + +-spec init(none) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +%% @private +%% Do not call this function directly -- it is exported only because it is a +%% necessary part of the OTP supervisor behavior. + +init(none) -> + RestartStrategy = {rest_for_one, 1, 60}, + + Daemon = {zx_daemon, + {zx_daemon, start_link, []}, + permanent, + 10000, + worker, + [zx_daemon]}, + + ConnSup = {zx_conn_sup, + {zx_conn_sup, start_link, []}, + permanent, + brutal_kill, + supervisor, + [zx_conn_sup]}, + + Children = [Daemon, ConnSup], + {ok, {RestartStrategy, Children}}. 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 new file mode 100644 index 0000000..2f0b39a --- /dev/null +++ b/zomp/lib/otpr-zx/0.1.0/src/zx_lib.erl @@ -0,0 +1,523 @@ +%%% @doc +%%% ZX Library +%%% +%%% This module contains a set of common-use functions internal to the ZX project. +%%% These functions are subject to radical change, are not publicly documented and +%%% should NOT be used by other projects. +%%% +%%% The public interface to the externally useful parts of this library are maintained +%%% in the otpr-zxxl package. +%%% @end + +-module(zx_lib). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("GPL-3.0"). + + +-export([zomp_home/0, find_zomp_home/0, + hosts_cache_file/1, get_prime/1, realm_meta/1, + read_project_meta/0, read_project_meta/1, read_package_meta/1, + write_project_meta/1, write_project_meta/2, + write_terms/2, + valid_lower0_9/1, valid_label/1, + string_to_version/1, version_to_string/1, + package_id/1, package_string/1, + package_dir/1, package_dir/2, + namify_zrp/1, namify_tgz/1, + find_latest_compatible/2, installed/1]). + +-include("zx_logger.hrl"). + + + +%%% Functions + +zomp_home() -> + case os:getenv("ZOMP_HOME") of + false -> + ZompHome = find_zomp_home(), + true = os:putenv("ZOMP_HOME", ZompHome), + ZompHome; + ZompHome -> + ZompHome + end. + + +-spec find_zomp_home() -> file:filename(). +%% @private +%% Check the host OS and return the absolute path to the zomp filesystem root. + +find_zomp_home() -> + case os:type() of + {unix, _} -> + Home = os:getenv("HOME"), + Dir = "zomp", + filename:join(Home, Dir); + {win32, _} -> + Drive = os:getenv("HOMEDRIVE"), + Path = os:getenv("HOMEPATH"), + Dir = "zomp", + filename:join([Drive, Path, Dir]) + end. + + +-spec hosts_cache_file(zx:realm()) -> file:filename(). +%% @private +%% Given a Realm name, construct a realm's .hosts filename and return it. + +hosts_cache_file(Realm) -> + filename:join(zomp_home(), Realm ++ ".hosts"). + + +-spec get_prime(Realm) -> Result + when Realm :: zx:realm(), + Result :: {ok, zx:host()} + | {error, file:posix()}. +%% @private +%% Check the given Realm's config file for the current prime node and return it. + +get_prime(Realm) -> + case realm_meta(Realm) of + {ok, RealmMeta} -> + {prime, Prime} = lists:keyfind(prime, 1, RealmMeta), + {ok, Prime}; + Error -> + Error + end. + + +-spec realm_meta(Realm) -> Result + when Realm :: string(), + Result :: {ok, Meta} + | {error, Reason}, + Meta :: [{atom(), term()}], + Reason :: file:posix(). +%% @private +%% Given a realm name, try to locate and read the realm's configuration file if it +%% exists, exiting with an appropriate error message if there is a problem reading +%% the file. + +realm_meta(Realm) -> + RealmFile = filename:join(zomp_home(), Realm ++ ".realm"), + file:consult(RealmFile). + + +-spec read_project_meta() -> Result + when Result :: {ok, zx:package_meta()} + | {error, file:posix()}. +%% @private +%% @equiv read_meta(".") + +read_project_meta() -> + read_project_meta("."). + + +-spec read_project_meta(Dir) -> Result + when Dir :: file:filename(), + Result :: {ok, zx:package_meta()} + | {error, file:posix()}. +%% @private +%% Read the `zomp.meta' file from the indicated directory, if possible. + +read_project_meta(Dir) -> + Path = filename:join(Dir, "zomp.meta"), + case file:consult(Path) of + {ok, Meta} -> + maps:from_list(Meta); + Error -> + ok = log(error, "Failed to open \"zomp.meta\" with ~tp", [Error]), + ok = log(error, "Wrong directory?"), + Error + end. + + +-spec read_package_meta(PackageID) -> Result + when PackageID :: zx:package_id(), + Result :: {ok, zx:package_meta()} + | {error, file:posix()}. + +read_package_meta({Realm, Name, Version}) -> + VersionString = Version, + Path = filename:join([zomp_home(), "lib", Realm, Name, VersionString]), + read_project_meta(Path). + + +-spec write_project_meta(Meta) -> Result + when Meta :: zx:package_meta(), + Result :: ok + | {error, Reason}, + Reason :: badarg + | terminated + | system_limit + | file:posix(). +%% @private +%% @equiv write_meta(".") + +write_project_meta(Meta) -> + write_project_meta(".", Meta). + + +-spec write_project_meta(Dir, Meta) -> ok + when Dir :: file:filename(), + Meta :: zx:package_meta(). +%% @private +%% Write the contents of the provided meta structure (a map these days) as a list of +%% Erlang K/V terms. + +write_project_meta(Dir, Meta) -> + Path = filename:join(Dir, "zomp.meta"), + write_terms(Path, maps:to_list(Meta)). + + +-spec write_terms(Filename, Terms) -> Result + when Filename :: file:filename(), + Terms :: [term()], + Result :: ok + | {error, Reason}, + Reason :: badarg + | terminated + | system_limit + | file:posix(). +%% @private +%% Provides functionality roughly inverse to file:consult/1. + +write_terms(Filename, List) -> + Format = fun(Term) -> io_lib:format("~tp.~n", [Term]) end, + Text = lists:map(Format, List), + file:write_file(Filename, Text). + + +-spec valid_lower0_9(string()) -> boolean(). +%% @private +%% Check whether a provided string is a valid lower0_9. + +valid_lower0_9([Char | Rest]) + when $a =< Char, Char =< $z -> + valid_lower0_9(Rest, Char); +valid_lower0_9(_) -> + false. + + +-spec valid_lower0_9(String, Last) -> boolean() + when String :: string(), + Last :: char(). + +valid_lower0_9([$_ | _], $_) -> + false; +valid_lower0_9([Char | Rest], _) + when $a =< Char, Char =< $z; + $0 =< Char, Char =< $9; + Char == $_ -> + valid_lower0_9(Rest, Char); +valid_lower0_9([], _) -> + true; +valid_lower0_9(_, _) -> + false. + + +-spec valid_label(string()) -> boolean(). +%% @private +%% Check whether a provided string is a valid label. + +valid_label([Char | Rest]) + when $a =< Char, Char =< $z -> + valid_label(Rest, Char); +valid_label(_) -> + false. + + +-spec valid_label(String, Last) -> boolean() + when String :: string(), + Last :: char(). + +valid_label([$. | _], $.) -> + false; +valid_label([$_ | _], $_) -> + false; +valid_label([$- | _], $-) -> + false; +valid_label([Char | Rest], _) + when $a =< Char, Char =< $z; + $0 =< Char, Char =< $9; + Char == $_; Char == $-; + Char == $. -> + valid_label(Rest, Char); +valid_label([], _) -> + true; +valid_label(_, _) -> + false. + + +-spec string_to_version(VersionString) -> Result + when VersionString :: string(), + Result :: {ok, zx:version()} + | {error, invalid_version_string}. +%% @private +%% @equiv string_to_version(string(), "", {z, z, z}) + +string_to_version(String) -> + string_to_version(String, "", {z, z, z}). + + +-spec string_to_version(String, Acc, Version) -> Result + when String :: string(), + Acc :: list(), + Version :: zx:version(), + Result :: {ok, zx:version()} + | {error, invalid_version_string}. +%% @private +%% Accepts a full or partial version string of the form `X.Y.Z', `X.Y' or `X' and +%% returns a zomp-type version tuple or crashes on bad data. + +string_to_version([Char | Rest], Acc, Version) when $0 =< Char andalso Char =< $9 -> + string_to_version(Rest, [Char | Acc], Version); +string_to_version("", "", Version) -> + {ok, Version}; +string_to_version(_, "", _) -> + {error, invalid_version_string}; +string_to_version([$. | Rest], Acc, {z, z, z}) -> + X = list_to_integer(lists:reverse(Acc)), + string_to_version(Rest, "", {X, z, z}); +string_to_version([$. | Rest], Acc, {X, z, z}) -> + Y = list_to_integer(lists:reverse(Acc)), + string_to_version(Rest, "", {X, Y, z}); +string_to_version([], Acc, {z, z, z}) -> + X = list_to_integer(lists:reverse(Acc)), + {ok, {X, z, z}}; +string_to_version([], Acc, {X, z, z}) -> + Y = list_to_integer(lists:reverse(Acc)), + {ok, {X, Y, z}}; +string_to_version([], Acc, {X, Y, z}) -> + Z = list_to_integer(lists:reverse(Acc)), + {ok, {X, Y, Z}}; +string_to_version(_, _, _) -> + {error, invalid_version_string}. + + +-spec version_to_string(zx:version()) -> {ok, string()} | {error, invalid_version}. +%% @private +%% Inverse of string_to_version/3. + +version_to_string({z, z, z}) -> + {ok, ""}; +version_to_string({X, z, z}) when is_integer(X) -> + {ok, integer_to_list(X)}; +version_to_string({X, Y, z}) when is_integer(X), is_integer(Y) -> + DeepList = lists:join($., [integer_to_list(Element) || Element <- [X, Y]]), + FlatString = lists:flatten(DeepList), + {ok, FlatString}; +version_to_string({X, Y, Z}) when is_integer(X), is_integer(Y), is_integer(Z) -> + DeepList = lists:join($., [integer_to_list(Element) || Element <- [X, Y, Z]]), + FlatString = lists:flatten(DeepList), + {ok, FlatString}; +version_to_string(_) -> + {error, invalid_version}. + + +-spec package_id(string()) -> {ok, zx:package_id()} | {error, invalid_package_string}. +%% @private +%% Converts a proper package_string to a package_id(). +%% This function takes into account missing version elements. +%% Examples: +%% `{ok, {"foo", "bar", {1, 2, 3}}} = package_id("foo-bar-1.2.3")' +%% `{ok, {"foo", "bar", {1, 2, z}}} = package_id("foo-bar-1.2")' +%% `{ok, {"foo", "bar", {1, z, z}}} = package_id("foo-bar-1")' +%% `{ok, {"foo", "bar", {z, z, z}}} = package_id("foo-bar")' +%% `{error, invalid_package_string} = package_id("Bad-Input")' + +package_id(String) -> + case dash_split(String) of + [Realm, Name, VersionString] -> + package_id(Realm, Name, VersionString); + [A, B] -> + case valid_lower0_9(B) of + true -> package_id(A, B, ""); + false -> package_id("otpr", A, B) + end; + [Name] -> + package_id("otpr", Name, ""); + _ -> + {error, invalid_package_string} + end. + + +-spec dash_split(string()) -> [string()] | error. +%% @private +%% An explicit, strict token split that ensures invalid names with leading, trailing or +%% double dashes don't slip through (a problem discovered with using string:tokens/2 +%% and string:lexemes/2. +%% Intended only as a helper function for package_id/1 + +dash_split(String) -> + dash_split(String, "", []). + + +dash_split([$- | Rest], Acc, Elements) -> + Element = lists:reverse(Acc), + dash_split(Rest, "", [Element | Elements]); +dash_split([Char | Rest], Acc, Elements) -> + dash_split(Rest, [Char | Acc], Elements); +dash_split("", Acc, Elements) -> + Element = lists:reverse(Acc), + lists:reverse([Element | Elements]); +dash_split(_, _, _) -> + error. + + +-spec package_id(Realm, Name, VersionString) -> Result + when Realm :: zx:realm(), + Name :: zx:name(), + VersionString :: string(), + Result :: {ok, zx:package_id()} + | {error, invalid_package_string}. + +package_id(Realm, Name, VersionString) -> + ValidRealm = valid_lower0_9(Realm), + ValidName = valid_lower0_9(Name), + MaybeVersion = string_to_version(VersionString), + case {ValidRealm, ValidName, MaybeVersion} of + {true, true, {ok, Version}} -> {ok, {Realm, Name, Version}}; + _ -> {error, invalid_package_string} + end. + + +-spec package_string(zx:package_id()) -> {ok, string()} | {error, invalid_package_id}. +%% @private +%% Map an PackageID to a correct string representation. +%% This function takes into account missing version elements. +%% Examples: +%% `{ok, "foo-bar-1.2.3"} = package_string({"foo", "bar", {1, 2, 3}})' +%% `{ok, "foo-bar-1.2"} = package_string({"foo", "bar", {1, 2, z}})' +%% `{ok, "foo-bar-1"} = package_string({"foo", "bar", {1, z, z}})' +%% `{ok, "foo-bar"} = package_string({"foo", "bar", {z, z, z}})' +%% `{error, invalid_package_id = package_string({"Bad", "Input"})' + +package_string({Realm, Name, {z, z, z}}) -> + ValidRealm = valid_lower0_9(Realm), + ValidName = valid_lower0_9(Name), + case ValidRealm and ValidName of + true -> + PackageString = lists:flatten(lists:join($-, [Realm, Name])), + {ok, PackageString}; + false -> + {error, invalid_package_id} + end; +package_string({Realm, Name, Version}) -> + ValidRealm = valid_lower0_9(Realm), + ValidName = valid_lower0_9(Name), + MaybeVersionString = version_to_string(Version), + case {ValidRealm, ValidName, MaybeVersionString} of + {true, true, {ok, VerString}} -> + PackageString = lists:flatten(lists:join($-, [Realm, Name, VerString])), + {ok, PackageString}; + _ -> + {error, invalid_package_id} + end; +package_string({Realm, Name}) -> + package_string({Realm, Name, {z, z, z}}); +package_string(_) -> + {error, invalid_package_id}. + + +-spec package_dir(zx:package_id()) -> file:filename(). +%% @private +%% Returns the path to a package installation. Crashes if PackageID is not a valid +%% identitifer or if the version is incomplete (it is not possible to create a path +%% to a partial version number). + +package_dir({Realm, Name, Version = {X, Y, Z}}) + when is_integer(X), is_integer(Y), is_integer(Z) -> + {ok, PackageDir} = package_string({Realm, Name}), + {ok, VersionDir} = version_to_string(Version), + filename:join([zomp_home(), "lib", PackageDir, VersionDir]). + + +-spec package_dir(Prefix, Package) -> PackageDataDir + when Prefix :: string(), + Package :: zx:package(), + PackageDataDir :: file:filename(). +%% @private +%% Create an absolute path to an application directory prefixed by the inclued argument. + +package_dir(Prefix, {Realm, Name}) -> + PackageString = package_string({Realm, Name, {z, z, z}}), + filename:join([zomp_home(), Prefix, PackageString]). + + +-spec namify_zrp(PackageID) -> ZrpFileName + when PackageID :: package_id(), + ZrpFileName :: file:filename(). +%% @private +%% Map an PackageID to its correct .zrp package file name. + +namify_zrp(PackageID) -> namify(PackageID, "zrp"). + + +-spec namify_tgz(PackageID) -> TgzFileName + when PackageID :: package_id(), + TgzFileName :: file:filename(). +%% @private +%% Map an PackageID to its correct gzipped tarball source bundle filename. + +namify_tgz(PackageID) -> namify(PackageID, "tgz"). + + +-spec namify(PackageID, Suffix) -> FileName + when PackageID :: package_id(), + Suffix :: string(), + FileName :: file:filename(). +%% @private +%% Converts an PackageID to a canonical string, then appends the provided +%% filename Suffix. + +namify(PackageID, Suffix) -> + {ok, PackageString} = package_string(PackageID), + PackageString ++ "." ++ Suffix. + + +-spec find_latest_compatible(Version, Versions) -> Result + when Version :: zx:version(), + Versions :: [zx:version()], + Result :: exact + | {ok, zx:version()} + | not_found. +%% @private +%% Find the latest compatible version from a list of versions. Returns the atom +%% `exact' in the case a full version is specified and it exists, the tuple +%% `{ok, Version}' in the case a compatible version was found against a partial +%% version tuple, and the atom `not_found' in the case no compatible version exists +%% in the list. Will fail with `not_found' if the input `Version' is not a valid +%% `zx:version()' tuple. + +find_latest_compatible(Version, Versions) -> + Descending = lists:reverse(lists:sort(Versions)), + latest_compatible(Version, Descending). + + +latest_compatible({z, z, z}, Versions) -> + {ok, hd(Versions)}; +latest_compatible({X, z, z}, Versions) -> + case lists:keyfind(X, 1, Versions) of + false -> not_found; + Version -> {ok, Version} + end; +latest_compatible({X, Y, z}, Versions) -> + NotMatch = fun({Q, W, _}) -> not (Q == X andalso W == Y) end, + case lists:dropwhile(NotMatch, Versions) of + [] -> not_found; + Vs -> {ok, hd(Vs)} + end; +latest_compatible(Version, Versions) -> + case lists:member(Version, Versions) of + true -> exact; + false -> not_found + end. + + +-spec installed(zx:package_id()) -> boolean(). +%% @private +%% True to its name, tells whether a package's install directory is found. + +installed(PackageID) -> + filelib:is_dir(package_dir(PackageID)). diff --git a/zomp/zx.sh b/zomp/zx.sh index dc91166..c439460 100755 --- a/zomp/zx.sh +++ b/zomp/zx.sh @@ -1,6 +1,6 @@ #! /bin/sh -# set -x +set -x ZOMP_DIR="$HOME/zomp" ORIGIN="$(pwd)" diff --git a/zx_dev.sh b/zx_dev.sh new file mode 100755 index 0000000..f02735a --- /dev/null +++ b/zx_dev.sh @@ -0,0 +1,13 @@ +#! /bin/sh + +set -x + +ZOMP_DIR="$HOME/vcs/zx/zomp" +ORIGIN="$(pwd)" +VERSION="$(ls $ZOMP_DIR/lib/otpr-zx/ | sort --field-separator=. --reverse | head --lines=1)" +ZX_DIR="$ZOMP_DIR/lib/otpr-zx/$VERSION" + +cd "$ZX_DIR" +./zmake +cd "$ZOMP_DIR" +erl -pa "$ZX_DIR/ebin" -run zx start $@