Finally some time to play with this again!

This commit is contained in:
Craig Everett 2018-01-25 22:35:58 +09:00
parent ae7b117828
commit 2fc295fe07
10 changed files with 1627 additions and 805 deletions

View File

@ -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: ZX is delivered as a zip file containing:
- `zomp.tar.gz`: An archive of a current working zx/zomp installation - `zomp.tar.gz`: An archive of a current working zx/zomp installation
- `install.escript`: The main installation script - `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 - `README.*` files such as this one
- `LICENSE` - `LICENSE`
- `notify.vbs`: A hacky VBS script to communicate with the Windows GUI during setup - `notify.vbs`: A hacky VBS script to communicate with the Windows GUI during setup

View File

@ -2,5 +2,5 @@
[{description, "Zomp client program"}, [{description, "Zomp client program"},
{vsn, "0.1.0"}, {vsn, "0.1.0"},
{applications, [stdlib, kernel]}, {applications, [stdlib, kernel]},
{modules, [zx, zx_daemon]}, {modules, [zx, zxd_sup, zxd]},
{mod, {zx, []}}]}. {mod, {zx, []}}]}.

File diff suppressed because it is too large Load Diff

View File

@ -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 <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
-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) ->

View File

@ -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 <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
-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}}.

View File

@ -2,71 +2,456 @@
%%% ZX Daemon %%% ZX Daemon
%%% %%%
%%% Resident execution daemon and runtime interface to Zomp. %%% 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 %%% @end
-module(zx_daemon). -module(zx_daemon).
-export([]). -behavior(gen_server).
-author("Craig Everett <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
-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() -include("zx_logger.hrl").
when State :: state().
%%% 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 %% @private
%% Execution maintenance loop. %% Load the daemon with the primary running application's meta data and location within
%% Once an application is started by zompc this process will wait for a message from %% the filesystem. This step allows running development code from any location in
%% the application if that application was written in a way to take advantage of zompc %% the filesystem against installed dependencies without requiring any magical
%% facilities such as post-start upgrade checking. %% references.
%% %%
%% NOTE: %% This call blocks specifically so that we can be certain that the target application
%% Adding clauses to this `receive' is where new functionality belongs. %% cannot be started before the impact of this call has taken full effect. It cannot
%% It may make sense to add a `zompc_lib' as an available dependency authors could %% be known whether the very first thing the target application will do is send this
%% use to interact with zompc without burying themselves under the complexity that %% process an async message. That implies that this should only ever be called once,
%% can come with naked send operations. (Would it make sense, for example, to have %% by the launching process (which normally terminates shortly thereafter).
%% the registered zompc process convert itself to a gen_server via zompc_lib to
%% provide more advanced functionality?)
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 receive
{monitor, Pid} -> {tcp, Socket, Bin} -> binary_to_term(Bin)
Mon = monitor(process, Pid), after 5000 -> {error, timeout}
exec_wait(State#s{pid = Pid, mon = Mon});
Unexpected ->
ok = log(warning, "Unexpected message: ~tp", [Unexpected]),
exec_wait(State)
end; end;
exec_wait(State = #s{pid = Pid, mon = Mon}) -> query_latest(Socket, {Realm, Name, Version}) ->
ok = send(Socket, {latest, Realm, Name, Version}),
receive receive
{check_update, Requester, Ref} -> {tcp, Socket, Bin} -> binary_to_term(Bin)
{Response, NewState} = check_update(State), after 5000 -> {error, timeout}
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)
end. end.
-spec check_update(State) -> {Response, NewState} -spec do_unsubscribe(State) -> {ok, NewState}
when State :: state(), when State :: state(),
Response :: term(),
NewState :: state(). 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) -> do_unsubscribe(State = #s{connp = none}) ->
ok = log(info, "Would be checking for an update of the current application now..."), {ok, State};
Response = "Nothing was checked, but you can imagine it to have been.", do_unsubscribe(State = #s{connp = ConnP, connm = ConnM}) ->
{Response, State}. 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).

View File

@ -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 <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
-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}}.

View File

@ -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 <zxq9@zxq9.com>").
-copyright("Craig Everett <zxq9@zxq9.com>").
-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)).

View File

@ -1,6 +1,6 @@
#! /bin/sh #! /bin/sh
# set -x set -x
ZOMP_DIR="$HOME/zomp" ZOMP_DIR="$HOME/zomp"
ORIGIN="$(pwd)" ORIGIN="$(pwd)"

13
zx_dev.sh Executable file
View File

@ -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 $@