979 lines
33 KiB
Erlang
979 lines
33 KiB
Erlang
%%% @doc
|
|
%%% ZX: A suite of tools for Erlang development and deployment.
|
|
%%%
|
|
%%% ZX can:
|
|
%%% - Create project templates for applications, libraries and escripts.
|
|
%%% - Initialize existing projects for packaging and management.
|
|
%%% - Create and manage zomp realms, users, keys, etc.
|
|
%%% - Manage dependencies hosted in any zomp realm.
|
|
%%% - Package, submit, pull-for-review, and resign-to-accept packages.
|
|
%%% - Update, upgrade, and run any application from source that zomp tracks.
|
|
%%% - Locally install packages from files and locally stored public keys.
|
|
%%% - Build and run a local project from source using zomp dependencies.
|
|
%%% - Start an anonymous zomp distribution node.
|
|
%%% - Act as a unified code launcher for any projects (Erlang + ZX = deployed).
|
|
%%%
|
|
%%% ZX is currently limited in one specific way:
|
|
%%% - Can only launch pure Erlang code.
|
|
%%%
|
|
%%% In the works:
|
|
%%% - Support for LFE
|
|
%%% - Support for Rust (cross-platform)
|
|
%%% - Support for Elixir (as a peer language)
|
|
%%% - Unified Windows installer to deploy Erlang, Rust, LFE, Elixir and ZX
|
|
%%% @end
|
|
|
|
-module(zx).
|
|
-vsn("0.7.1").
|
|
-behavior(application).
|
|
-author("Craig Everett <zxq9@zxq9.com>").
|
|
-copyright("Craig Everett <zxq9@zxq9.com>").
|
|
-license("GPL-3.0").
|
|
|
|
|
|
-export([do/0, run/2, not_done/1, done/1]).
|
|
-export([subscribe/1, unsubscribe/0]).
|
|
-export([list/0, list/1, list/2, list/3, latest/1]).
|
|
-export([start/2, stop/1, stop/0, silent_stop/0]).
|
|
|
|
-export_type([serial/0, package_id/0, package/0, realm/0, name/0, version/0,
|
|
identifier/0,
|
|
host/0,
|
|
key/0, key_data/0, key_bin/0, key_id/0, key_name/0,
|
|
user_id/0, user_name/0, contact_info/0, user_data/0,
|
|
lower0_9/0, label/0,
|
|
ss_tag/0, search_tag/0, description/0, package_type/0,
|
|
outcome/0]).
|
|
|
|
-include("zx_logger.hrl").
|
|
|
|
|
|
|
|
%%% Type Definitions
|
|
|
|
-type serial() :: integer().
|
|
-type package_id() :: {realm(), name(), version()}.
|
|
-type package() :: {realm(), name()}.
|
|
-type realm() :: lower0_9().
|
|
-type name() :: lower0_9().
|
|
-type version() :: {Major :: non_neg_integer() | z,
|
|
Minor :: non_neg_integer() | z,
|
|
Patch :: non_neg_integer() | z}.
|
|
-type host() :: {string() | inet:ip_address(), inet:port_number()}.
|
|
-type key() :: term(). % Srsly. This is what public_key:der_decode/2 returns.
|
|
-type key_data() :: {Name :: key_name(),
|
|
Public :: none | key_bin(),
|
|
Private :: none | key_bin()}.
|
|
-type key_bin() :: {Sig :: none | {key_name(), binary()},
|
|
DER :: binary()}.
|
|
-type key_id() :: {realm(), key_name()}.
|
|
-type key_name() :: key_hash().
|
|
-type key_hash() :: binary().
|
|
-type user_data() :: {ID :: user_id(),
|
|
RealName :: string(),
|
|
Contact :: [contact_info()],
|
|
Keys :: [key_data()]}.
|
|
-type user_id() :: {realm(), user_name()}.
|
|
-type user_name() :: label().
|
|
-type contact_info() :: {Type :: string(), Data :: string()}.
|
|
-type lower0_9() :: [$a..$z | $0..$9 | $_].
|
|
-type label() :: [$a..$z | $0..$9 | $_ | $- | $.].
|
|
-type ss_tag() :: {serial(), erlang:timestamp()}.
|
|
-type search_tag() :: string().
|
|
|
|
-type description() :: {description,
|
|
PackageID :: package_id(),
|
|
DisplayName :: string(),
|
|
Type :: package_type(),
|
|
Desc :: string(),
|
|
Author :: string(),
|
|
AEmail :: string(),
|
|
WebURL :: string(),
|
|
RepoURL :: string(),
|
|
Tags :: [zx:search_tag()]}.
|
|
-type package_type() :: app | lib | gui | cli.
|
|
|
|
-type outcome() :: ok
|
|
| {error, Reason :: term()}
|
|
| {error, Code :: non_neg_integer()}
|
|
| {error, Info :: string(), Code :: non_neg_integer()}.
|
|
|
|
|
|
|
|
%%% Command Dispatch
|
|
|
|
-spec do() -> no_return().
|
|
|
|
do() ->
|
|
ok = io:setopts([{encoding, unicode}]),
|
|
ok = start(),
|
|
Args = init:get_plain_arguments(),
|
|
do(Args).
|
|
|
|
|
|
-spec do(Args) -> no_return()
|
|
when Args :: [string()].
|
|
%% Dispatch work functions based on the nature of the input arguments.
|
|
|
|
do(["help"]) ->
|
|
done(help(top));
|
|
do(["help", "user"]) ->
|
|
done(help(user));
|
|
do(["help", "dev"]) ->
|
|
done(help(dev));
|
|
do(["help", "sysop"]) ->
|
|
done(help(sysop));
|
|
do(["--version"]) ->
|
|
done(version());
|
|
do(["run", PackageString | ArgV]) ->
|
|
ok = zx_daemon:connect(),
|
|
not_done(run(PackageString, ArgV));
|
|
do(["list", "realms"]) ->
|
|
done(zx_local:list_realms());
|
|
do(["list", "packages", Realm]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:list_packages(Realm));
|
|
do(["list", "versions", PackageName]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:list_versions(PackageName));
|
|
do(["list", "gui"]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:list_type(gui));
|
|
do(["list", "cli"]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:list_type(cli));
|
|
do(["list", "app"]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:list_type(app));
|
|
do(["list", "lib"]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:list_type(lib));
|
|
do(["latest", PackageString]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:latest(PackageString));
|
|
do(["describe", PackageString]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:describe(PackageString));
|
|
do(["upgrade"]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(upgrade());
|
|
do(["import", "realm", RealmFile]) ->
|
|
done(zx_local:import_realm(RealmFile));
|
|
do(["drop", "realm", Realm]) ->
|
|
done(zx_local:drop_realm(Realm));
|
|
do(["logpath", PackageString, AgoString]) ->
|
|
case try list_to_integer(AgoString) catch Error:Reason -> {Error, Reason} end of
|
|
{error, badarg} -> done(help(user));
|
|
Ago -> done(zx_local:logpath(PackageString, Ago))
|
|
end;
|
|
do(["set", "timeout", String]) ->
|
|
done(zx_local:set_timeout(String));
|
|
do(["add", "mirror"]) ->
|
|
done(zx_local:add_mirror());
|
|
do(["add", "mirror", Address]) ->
|
|
done(zx_local:add_mirror(Address));
|
|
do(["add", "mirror", Address, Port]) ->
|
|
done(zx_local:add_mirror(Address, Port));
|
|
do(["drop", "mirror"]) ->
|
|
done(zx_local:drop_mirror());
|
|
do(["drop", "mirror", Address]) ->
|
|
done(zx_local:drop_mirror(Address));
|
|
do(["drop", "mirror", Address, Port]) ->
|
|
done(zx_local:drop_mirror(Address, Port));
|
|
do(["create", "project"]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:create_project());
|
|
do(["runlocal" | ArgV]) ->
|
|
ok = zx_daemon:connect(),
|
|
not_done(run_local(ArgV));
|
|
do(["rundir", Path | ArgV]) ->
|
|
ok = zx_daemon:connect(),
|
|
not_done(run_dir(Path, ArgV));
|
|
do(["init"]) ->
|
|
ok = zx_daemon:connect(),
|
|
ok = compatibility_check([unix]),
|
|
done(zx_local:initialize());
|
|
do(["list", "deps"]) ->
|
|
done(zx_local:list_deps());
|
|
do(["list", "deps", PackageString]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:list_deps(PackageString));
|
|
do(["set", "dep", PackageString]) ->
|
|
done(zx_local:set_dep(PackageString));
|
|
do(["drop", "dep", PackageString]) ->
|
|
done(zx_local:drop_dep(PackageString));
|
|
do(["verup", Level]) ->
|
|
ok = compatibility_check([unix]),
|
|
done(zx_local:verup(Level));
|
|
do(["set", "version", VersionString]) ->
|
|
ok = compatibility_check([unix]),
|
|
done(zx_local:set_version(VersionString));
|
|
do(["provides", Module]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:provides(Module));
|
|
do(["search"]) ->
|
|
done(help(user));
|
|
do(["search" | Terms]) ->
|
|
ok = zx_daemon:connect(),
|
|
Strings = string:join(Terms, " "),
|
|
done(zx_local:search(Strings));
|
|
do(["update", "meta"]) ->
|
|
done(zx_local:update_meta());
|
|
do(["update", ".app"]) ->
|
|
done(zx_local:update_app_file());
|
|
do(["package"]) ->
|
|
{ok, TargetDir} = file:get_cwd(),
|
|
done(zx_local:package(TargetDir));
|
|
do(["package", TargetDir]) ->
|
|
case filelib:is_dir(TargetDir) of
|
|
true -> done(zx_local:package(TargetDir));
|
|
false -> done({error, "Target directory does not exist", 22})
|
|
end;
|
|
do(["submit", PackageFile]) ->
|
|
done(zx_auth:submit(PackageFile));
|
|
do(["list", "pending", PackageName]) ->
|
|
done(zx_auth:list_pending(PackageName));
|
|
do(["review", PackageString]) ->
|
|
done(zx_auth:review(PackageString));
|
|
do(["approve", PackageString]) ->
|
|
done(zx_auth:approve(PackageString));
|
|
do(["reject", PackageString]) ->
|
|
done(zx_auth:reject(PackageString));
|
|
do(["sync", "keys"]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_auth:sync_keys());
|
|
do(["create", "user"]) ->
|
|
done(zx_local:create_user());
|
|
do(["create", "keypair"]) ->
|
|
done(zx_local:grow_a_pair());
|
|
do(["export", "user"]) ->
|
|
done(zx_local:export_user(zpuf));
|
|
do(["export", "user", "dangerous"]) ->
|
|
done(zx_local:export_user(zduf));
|
|
do(["import", "user", ZdufFile]) ->
|
|
done(zx_local:import_user(ZdufFile));
|
|
do(["list", "users", Realm]) ->
|
|
done(zx_auth:list_users(Realm));
|
|
do(["list", "packagers", PackageName]) ->
|
|
done(zx_auth:list_packagers(PackageName));
|
|
do(["list", "maintainers", PackageName]) ->
|
|
done(zx_auth:list_maintainers(PackageName));
|
|
do(["list", "sysops", Realm]) ->
|
|
ok = zx_daemon:connect(),
|
|
done(zx_local:list_sysops(Realm));
|
|
do(["export", "realm"]) ->
|
|
done(zx_local:export_realm());
|
|
do(["export", "realm", Realm]) ->
|
|
done(zx_local:export_realm(Realm));
|
|
do(["install", PackageFile]) ->
|
|
case filelib:is_regular(PackageFile) of
|
|
true -> done(zx_daemon:install(PackageFile));
|
|
false -> done({error, ".zsp file does not exist", 22})
|
|
end;
|
|
do(["list", "approved", Realm]) ->
|
|
done(zx_auth:list_approved(Realm));
|
|
do(["accept", PackageString]) ->
|
|
done(zx_auth:accept(PackageString));
|
|
do(["add", "package", PackageName]) ->
|
|
done(zx_auth:add_package(PackageName));
|
|
do(["add", "user", ZpuFile]) ->
|
|
done(zx_auth:add_user(ZpuFile));
|
|
do(["rem", "user", Realm, UserName]) ->
|
|
done(zx_auth:rem_user(Realm, UserName));
|
|
do(["add", "packager", Package, UserName]) ->
|
|
done(zx_auth:add_packager(Package, UserName));
|
|
do(["rem", "packager", Package, UserName]) ->
|
|
done(zx_auth:rem_packager(Package, UserName));
|
|
do(["add", "maintainer", Package, UserName]) ->
|
|
done(zx_auth:add_maintainer(Package, UserName));
|
|
do(["rem", "maintainer", Package, UserName]) ->
|
|
done(zx_auth:rem_maintainer(Package, UserName));
|
|
do(["add", "sysop", Realm, UserName]) ->
|
|
done(zx_auth:add_sysop(Realm, UserName));
|
|
do(["create", "realm"]) ->
|
|
done(zx_local:create_realm());
|
|
do(["takeover", Realm]) ->
|
|
done(zx_daemon:takeover(Realm));
|
|
do(["abdicate", Realm]) ->
|
|
done(zx_daemon:abdicate(Realm));
|
|
do(_) ->
|
|
done(help(top)).
|
|
|
|
|
|
-spec done(outcome()) -> no_return().
|
|
|
|
done(ok) ->
|
|
ok = zx_daemon:idle(),
|
|
init:stop(0);
|
|
done({error, Code}) when is_integer(Code) ->
|
|
ok = zx_daemon:idle(),
|
|
Message = "Operation failed with code: ~w",
|
|
ok = tell(error, Message, [Code]),
|
|
init:stop(Code);
|
|
done({error, Reason}) ->
|
|
ok = zx_daemon:idle(),
|
|
Message = "Operation failed with: ~160tp",
|
|
ok = tell(error, Message, [Reason]),
|
|
init:stop(1);
|
|
done({error, Info, Code}) ->
|
|
ok = zx_daemon:idle(),
|
|
Message = "Operation failed with: ~160tp",
|
|
ok = tell(error, Message, [Info]),
|
|
init:stop(Code).
|
|
|
|
|
|
-spec not_done(outcome()) -> ok | no_return().
|
|
|
|
not_done(ok) -> ok;
|
|
not_done(Error) -> done(Error).
|
|
|
|
|
|
-spec compatibility_check(Platforms) -> ok | no_return()
|
|
when Platforms :: unix | win32.
|
|
%% @private
|
|
%% Some commands only work on specific platforms because they leverage some specific
|
|
%% aspect on that platform, but not common to all. ATM this is mostly developer
|
|
%% commands that leverage things universal to *nix/posix shells but not Windows.
|
|
%% If equivalent procedures are written in Erlang then these restrictions can be
|
|
%% avoided -- but it is unclear whether there are any Erlang developers even using
|
|
%% Windows, so for now this is the bad version of the solution.
|
|
|
|
compatibility_check(Platforms) ->
|
|
{Family, Name} = os:type(),
|
|
case lists:member(Family, Platforms) of
|
|
true ->
|
|
ok;
|
|
false ->
|
|
Message = "Unfortunately this command is not available on ~tw ~tw",
|
|
ok = tell(error, Message, [Family, Name]),
|
|
init:stop()
|
|
end.
|
|
|
|
|
|
|
|
%%% 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() ->
|
|
LogPath =
|
|
case init:get_plain_arguments() of
|
|
["run", PackageString | _] ->
|
|
case zx_lib:package_id(PackageString) of
|
|
{ok, PackageID} -> zx_lib:new_logpath(PackageID);
|
|
Error -> done(Error)
|
|
end;
|
|
["rundir", Path | _] ->
|
|
case zx_lib:read_project_meta(Path) of
|
|
{ok, #{package_id := PackageID}} -> zx_lib:new_logpath(PackageID);
|
|
Error -> done(Error)
|
|
end;
|
|
["runlocal" | _] ->
|
|
case zx_lib:read_project_meta() of
|
|
{ok, #{package_id := PackageID}} -> zx_lib:new_logpath(PackageID);
|
|
Error -> done(Error)
|
|
end;
|
|
_ ->
|
|
{ok, Version} = zx_lib:string_to_version(os:getenv("ZX_VERSION")),
|
|
zx_lib:new_logpath({"otpr", "zx", Version})
|
|
end,
|
|
ok = logger:remove_handler(default),
|
|
LoggerConf =
|
|
#{config =>
|
|
#{burst_limit_enable => true,
|
|
burst_limit_max_count => 500,
|
|
burst_limit_window_time => 1000,
|
|
drop_mode_qlen => 200,
|
|
filesync_repeat_interval => no_repeat,
|
|
flush_qlen => 1000,
|
|
overload_kill_enable => false,
|
|
overload_kill_mem_size => 3000000,
|
|
overload_kill_qlen => 20000,
|
|
overload_kill_restart_after => 5000,
|
|
sync_mode_qlen => 10,
|
|
type => {file, LogPath}},
|
|
filter_default =>
|
|
stop,
|
|
filters =>
|
|
[{remote_gl, {fun logger_filters:remote_gl/2, stop}},
|
|
{domain, {fun logger_filters:domain/2, {log, super, [otp, sasl]}}},
|
|
{no_domain, {fun logger_filters:domain/2, {log, undefined,[]}}}],
|
|
formatter =>
|
|
{logger_formatter, #{legacy_header => false, single_line => true}},
|
|
id => default,
|
|
level => all,
|
|
module => logger_std_h},
|
|
ok = logger:add_handler(default, logger_std_h, LoggerConf),
|
|
ok = logger:set_primary_config(level, debug),
|
|
% Hacky:
|
|
% Load all necessary atoms for binary_to_term(B, [safe]) to work with zx_zsp:meta()
|
|
MetaKeys = maps:keys(zx_zsp:new_meta()),
|
|
Types = [lib, app, gui, cli],
|
|
ok = log(info, "ZSP meta keys: ~w", [MetaKeys]),
|
|
ok = log(info, "Available package types: ~w", [Types]),
|
|
application:ensure_started(zx, permanent).
|
|
|
|
|
|
-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() ->
|
|
ok = tell("Shutting down runtime."),
|
|
ok = zx_daemon:idle(),
|
|
case application:stop(zx) of
|
|
ok ->
|
|
init:stop();
|
|
{error, {not_started, zx}} ->
|
|
init:stop();
|
|
Error ->
|
|
ok = tell(error, "zx:stop/0 failed with ~tp", [Error]),
|
|
init:stop(1)
|
|
end.
|
|
|
|
|
|
-spec silent_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.
|
|
|
|
silent_stop() ->
|
|
ok = zx_daemon:idle(),
|
|
case application:stop(zx) of
|
|
ok ->
|
|
init:stop();
|
|
{error, {not_started, zx}} ->
|
|
init:stop();
|
|
Error ->
|
|
ok = tell(error, "zx:stop_quiet/0 failed with ~tp", [Error]),
|
|
init:stop(1)
|
|
end.
|
|
|
|
|
|
%%% Application Callbacks
|
|
|
|
-spec start(StartType, StartArgs) -> Result
|
|
when StartType :: normal,
|
|
StartArgs :: none,
|
|
Result :: {ok, pid()}.
|
|
%% @private
|
|
%% Application callback. Not to be called directly.
|
|
|
|
start(normal, none) ->
|
|
ok = application:ensure_started(inets),
|
|
zx_sup:start_link().
|
|
|
|
|
|
-spec stop(term()) -> ok.
|
|
%% @private
|
|
%% Application callback. Not to be called directly.
|
|
|
|
stop(_) ->
|
|
ok.
|
|
|
|
|
|
|
|
%%% 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().
|
|
|
|
|
|
|
|
%%% Query Functions
|
|
|
|
-spec list() -> Result
|
|
when Result :: {ok, [realm()]}
|
|
| {error, no_realms}.
|
|
|
|
list() ->
|
|
case zx_lib:list_realms() of
|
|
[] -> {error, no_realms};
|
|
Realms -> {ok, Realms}
|
|
end.
|
|
|
|
|
|
-spec list(realm()) -> Result
|
|
when Result :: {ok, [realm()]}
|
|
| {error, Reason},
|
|
Reason :: bad_realm
|
|
| timeout
|
|
| network.
|
|
|
|
list(Realm) ->
|
|
{ok, ID} = zx_daemon:list(Realm),
|
|
zx_daemon:wait_result(ID).
|
|
|
|
|
|
-spec list(realm(), name()) -> Result
|
|
when Result :: {ok, [version()]}
|
|
| {error, Reason},
|
|
Reason :: bad_realm
|
|
| bad_package
|
|
| timeout
|
|
| network.
|
|
|
|
list(Realm, Name) ->
|
|
list(Realm, Name, {z, z, z}).
|
|
|
|
|
|
-spec list(realm(), name(), version()) -> Result
|
|
when Result :: {ok, [version()]}
|
|
| {error, Reason},
|
|
Reason :: bad_realm
|
|
| bad_package
|
|
| bad_version
|
|
| timeout
|
|
| network.
|
|
|
|
list(Realm, Name, Version) ->
|
|
{ok, ID} = zx_daemon:list(Realm, Name, Version),
|
|
zx_daemon:wait_result(ID).
|
|
|
|
|
|
-spec latest(package_id()) -> Result
|
|
when Result :: {ok, package_id()}
|
|
| {error, Reason},
|
|
Reason :: bad_realm
|
|
| bad_package
|
|
| bad_version
|
|
| timeout
|
|
| network.
|
|
|
|
latest(PackageID) ->
|
|
{ok, ID} = zx_daemon:latest(PackageID),
|
|
zx_daemon:wait_result(ID).
|
|
|
|
|
|
|
|
%%% Execution of application
|
|
|
|
-spec run(PackageString, RunArgs) -> zx:outcome()
|
|
when PackageString :: 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 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
|
|
%% a child program or for calls made via zx_lib to assist in environment discovery.
|
|
%%
|
|
%% If there is a problem anywhere in the locating, discovery, building, and loading
|
|
%% procedure the runtime will halt with an error message.
|
|
|
|
run(PackageString, RunArgs) ->
|
|
case zx_lib:package_id(PackageString) of
|
|
{ok, {"otpr", "zomp", Version}} -> run2_maybe(Version, RunArgs);
|
|
{ok, FuzzyID} -> run2(FuzzyID, RunArgs);
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
run2_maybe(Version, RunArgs) ->
|
|
{ok, Managed} = zx_daemon:conf(managed),
|
|
case lists:member("otpr", Managed) of
|
|
true -> run_zomp(RunArgs);
|
|
false -> run2({"otpr", "zomp", Version}, RunArgs)
|
|
end.
|
|
|
|
run_zomp(RunArgs) ->
|
|
{ok, Dirs} = file:list_dir(zx_lib:path(lib, "otpr", "zomp")),
|
|
Versions = lists:foldl(fun tuplize/2, [], Dirs),
|
|
case zx_lib:find_latest_compatible({z, z, z}, Versions) of
|
|
not_found -> {error, not_found};
|
|
{ok, Latest} -> run3({"otpr", "zomp", Latest}, RunArgs)
|
|
end.
|
|
|
|
tuplize(String, Acc) ->
|
|
case zx_lib:string_to_version(String) of
|
|
{ok, Version} -> [Version | Acc];
|
|
_ -> Acc
|
|
end.
|
|
|
|
|
|
run2(FuzzyID, RunArgs) ->
|
|
case resolve_version(FuzzyID) of
|
|
{installed, PackageID} -> run3(PackageID, RunArgs);
|
|
{fetch, PackageID} -> run3_maybe(PackageID, RunArgs);
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
run3_maybe(PackageID, RunArgs) ->
|
|
case fetch(PackageID) of
|
|
ok -> run3(PackageID, RunArgs);
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
run3(PackageID, RunArgs) ->
|
|
Dir = zx_lib:ppath(lib, PackageID),
|
|
{ok, Meta} = zx_lib:read_project_meta(Dir),
|
|
Type = maps:get(type, Meta),
|
|
Deps = maps:get(deps, Meta),
|
|
ok = prepare([PackageID | Deps]),
|
|
execute(Type, PackageID, Meta, Dir, RunArgs).
|
|
|
|
|
|
-spec resolve_version(PackageID) -> Result
|
|
when PackageID :: package_id(),
|
|
Result :: not_found
|
|
| exact
|
|
| {ok, Installed :: version()}.
|
|
%% @private
|
|
%% Resolve the provided PackageID to the latest matching installed package directory
|
|
%% version if one exists, returning a value that indicates whether an exact match was
|
|
%% 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_version(PackageID = {_, _, {X, Y, Z}})
|
|
when is_integer(X), is_integer(Y), is_integer(Z) ->
|
|
case zx_lib:installed(PackageID) of
|
|
true -> {installed, PackageID};
|
|
false -> {fetch, PackageID}
|
|
end;
|
|
resolve_version(PackageID = {Realm, Name, _}) ->
|
|
{ok, ID} = zx_daemon:latest(PackageID),
|
|
case zx_daemon:wait_result(ID) of
|
|
{ok, Latest} -> resolve_version({Realm, Name, Latest});
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec run_local(RunArgs) -> zx:outcome() | no_return()
|
|
when RunArgs :: [string()].
|
|
%% @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) ->
|
|
{ok, ProjectDir} = file:get_cwd(),
|
|
run_project(ProjectDir, ProjectDir, RunArgs).
|
|
|
|
|
|
-spec run_dir(TargetDir, RunArgs) -> zx:outcome() | no_return()
|
|
when TargetDir :: file:filename(),
|
|
RunArgs :: [string()].
|
|
|
|
run_dir(TargetDir, RunArgs) ->
|
|
{ok, ExecDir} = file:get_cwd(),
|
|
case file:set_cwd(TargetDir) of
|
|
ok ->
|
|
{ok, ProjectDir} = file:get_cwd(),
|
|
run_project(ProjectDir, ExecDir, RunArgs);
|
|
Error -> Error
|
|
end.
|
|
|
|
|
|
-spec run_project(ProjectDir, ExecDir, RunArgs) -> zx:outcome() | no_return()
|
|
when ProjectDir :: file:filename(),
|
|
ExecDir :: file:filename(),
|
|
RunArgs :: [string()].
|
|
|
|
run_project(ProjectDir, ExecDir, RunArgs) ->
|
|
{ok, Meta} = zx_lib:read_project_meta(),
|
|
PackageID = {_, Name, _} = maps:get(package_id, Meta),
|
|
Type = maps:get(type, Meta),
|
|
Deps = maps:get(deps, Meta),
|
|
{ok, Dir} = file:get_cwd(),
|
|
true = os:putenv(Name ++ "_include", filename:join(Dir, "include")),
|
|
case prepare(Deps) of
|
|
ok ->
|
|
ok = file:set_cwd(ProjectDir),
|
|
ok = zx_lib:build(),
|
|
ok = file:set_cwd(ExecDir),
|
|
execute(Type, PackageID, Meta, Dir, RunArgs);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
-spec prepare([zx:package_id()]) -> ok.
|
|
%% @private
|
|
%% Execution prep common to all packages.
|
|
|
|
prepare(Deps) ->
|
|
true = os:putenv("zx_include", filename:join(os:getenv("ZX_DIR"), "include")),
|
|
ok = lists:foreach(fun include_env/1, Deps),
|
|
NotInstalled = fun(P) -> not filelib:is_dir(zx_lib:ppath(lib, P)) end,
|
|
Needed = lists:filter(NotInstalled, Deps),
|
|
acquire(Needed, Deps).
|
|
|
|
acquire([Dep | Rest], Deps) ->
|
|
case fetch(Dep) of
|
|
ok -> acquire(Rest, Deps);
|
|
Error -> Error
|
|
end;
|
|
acquire([], Deps) ->
|
|
make(Deps).
|
|
|
|
make([Dep | Rest]) ->
|
|
case zx_daemon:build(Dep) of
|
|
ok -> make(Rest);
|
|
Error -> Error
|
|
end;
|
|
make([]) ->
|
|
ok.
|
|
|
|
|
|
include_env(PackageID = {_, Name, _}) ->
|
|
Path = filename:join(zx_lib:ppath(lib, PackageID), "include"),
|
|
os:putenv(Name ++ "_include", Path).
|
|
|
|
|
|
-spec upgrade() -> zx:outcome().
|
|
%% @private
|
|
%% Upgrade ZX itself to the latest version.
|
|
|
|
upgrade() ->
|
|
ZxDir = os:getenv("ZX_DIR"),
|
|
{ok, Meta} = zx_lib:read_project_meta(ZxDir),
|
|
PackageID = {Realm, Name, Current} = maps:get(package_id, Meta),
|
|
{ok, PackageString} = zx_lib:package_string(PackageID),
|
|
ok = tell("Current version: ~s", [PackageString]),
|
|
{ok, ID} = zx_daemon:latest({Realm, Name}),
|
|
case zx_daemon:wait_result(ID) of
|
|
{ok, Current} ->
|
|
tell("Running latest version.");
|
|
{ok, Latest} when Latest > Current ->
|
|
NewID = {Realm, Name, Latest},
|
|
ok = acquire([NewID], [NewID]),
|
|
{ok, LatestString} = zx_lib:version_to_string(Latest),
|
|
ok = tell(info, "Acquiring upgrade: ~s", [LatestString]),
|
|
VersionTxt = filename:join(zx_lib:path(etc), "version.txt"),
|
|
ok = file:write_file(VersionTxt, LatestString),
|
|
{ok, NewString} = zx_lib:package_string(NewID),
|
|
tell("Upgraded to ~s.", [NewString]);
|
|
{ok, Available} when Available < Current ->
|
|
{ok, AvailableString} = zx_lib:version_to_string(Available),
|
|
Message = "Local version is newer than ~s. Nothing to do.",
|
|
ok = tell(Message, [AvailableString]);
|
|
Error ->
|
|
Error
|
|
end.
|
|
|
|
|
|
-spec fetch(zx:package_id()) -> zx:outcome().
|
|
|
|
fetch(PackageID) ->
|
|
{ok, ID} = zx_daemon:fetch(PackageID),
|
|
fetch2(ID).
|
|
|
|
fetch2(ID) ->
|
|
receive
|
|
{result, ID, done} ->
|
|
ok;
|
|
{result, ID, {hops, Count}} ->
|
|
ok = tell("Inbound; ~w hops away.", [Count]),
|
|
fetch2(ID);
|
|
{result, ID, Error} ->
|
|
Error
|
|
after 10000 ->
|
|
{error, timeout}
|
|
end.
|
|
|
|
|
|
-spec execute(Type, PackageID, Meta, Dir, RunArgs) -> no_return()
|
|
when Type :: app | cli | gui | lib,
|
|
PackageID :: package_id(),
|
|
Meta :: zx_zsp: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(lib, PackageID, _, _, _) ->
|
|
Message = "Lib ~ts is available on the system, but is not a standalone app.",
|
|
{ok, PackageString} = zx_lib:package_string(PackageID),
|
|
ok = tell(Message, [PackageString]),
|
|
init:stop();
|
|
execute(cli, PackageID, Meta, Dir, RunArgs) ->
|
|
Name = element(2, PackageID),
|
|
ok = zx_daemon:pass_meta(Meta, Dir, RunArgs),
|
|
AppTag = list_to_atom(Name),
|
|
{ok, _} = application:ensure_all_started(AppTag, permanent),
|
|
case maps:get(mod, Meta, none) of
|
|
none ->
|
|
{error, "No executable module"};
|
|
ModName ->
|
|
Mod = list_to_atom(ModName),
|
|
Mod:start(RunArgs)
|
|
end;
|
|
execute(Type, PackageID, Meta, Dir, RunArgs) ->
|
|
{ok, PackageString} = zx_lib:package_string(PackageID),
|
|
ok = tell("Starting ~p ~ts.", [Type, PackageString]),
|
|
Name = element(2, PackageID),
|
|
ok = zx_daemon:pass_meta(Meta, Dir, RunArgs),
|
|
AppTag = list_to_atom(Name),
|
|
ok = ensure_all_started(AppTag, permanent),
|
|
log(info, "Launcher complete.").
|
|
|
|
|
|
-spec ensure_all_started(AppMod, Type) -> ok
|
|
when AppMod :: module(),
|
|
Type :: application:restart_type().
|
|
%% @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, Type) ->
|
|
case application:ensure_all_started(AppMod, Type) of
|
|
{ok, []} -> ok;
|
|
{ok, Apps} -> tell("Started ~160tp", [Apps])
|
|
end.
|
|
|
|
|
|
|
|
%%% Usage
|
|
|
|
help(top) -> show_help();
|
|
help(user) -> show_help([usage_header(), usage_user(), usage_spec()]);
|
|
help(dev) -> show_help([usage_header(), usage_dev(), usage_spec()]);
|
|
help(sysop) -> show_help([usage_header(), usage_sysop(), usage_spec()]).
|
|
|
|
|
|
show_help() ->
|
|
T =
|
|
"ZX help has three forms, one for each category of commands:~n"
|
|
" zx help [user | dev | sysop]~n"
|
|
"The user manual is also available online at: http://zxq9.com/projects/zomp/~n",
|
|
io:format(T).
|
|
|
|
|
|
show_help(Info) -> lists:foreach(fun io:format/1, Info).
|
|
|
|
|
|
usage_header() ->
|
|
"ZX usage: zx [command] [object] [args]~n~n".
|
|
|
|
usage_user() ->
|
|
"User Actions:~n"
|
|
" zx run PackageID [Args]~n"
|
|
" zx list realms~n"
|
|
" zx list packages Realm~n"
|
|
" zx list versions PackageID~n"
|
|
" zx list [gui | cli | app | lib]~n"
|
|
" zx latest PackageID~n"
|
|
" zx search Tag~n"
|
|
" zx describe Package~n"
|
|
" zx upgrade~n"
|
|
" zx import realm RealmFile~n"
|
|
" zx drop realm Realm~n"
|
|
" zx add mirror [Address [Port]]~n"
|
|
" zx drop mirror [Address [Port]]~n~n".
|
|
|
|
usage_dev() ->
|
|
"Developer/Packager/Maintainer Actions:~n"
|
|
" zx create project~n"
|
|
" zx runlocal [Args]~n"
|
|
" zx rundir Path [Args]~n"
|
|
" zx init~n"
|
|
" zx list deps [PackageID]~n"
|
|
" zx set dep PackageID~n"
|
|
" zx drop dep PackageID~n"
|
|
" zx verup Level~n"
|
|
" zx set version Version~n"
|
|
" zx provides Module~n"
|
|
" zx update meta~n"
|
|
" zx update .app~n"
|
|
" zx package [Path]~n"
|
|
" zx submit ZSP~n"
|
|
" zx list pending PackageName~n"
|
|
" zx review PackageID~n"
|
|
" zx approve PackageID~n"
|
|
" zx reject PackageID~n"
|
|
" zx create user~n"
|
|
" zx create keypair~n"
|
|
" zx export user [dangerous]~n"
|
|
" zx import user ZDUF~n"
|
|
" zx list users Realm~n"
|
|
" zx list packagers PackageName~n"
|
|
" zx list maintainers PackageName~n"
|
|
" zx list sysops Realm~n"
|
|
" zx export realm [Realm]~n"
|
|
" zx install ZSP~n~n".
|
|
|
|
usage_sysop() ->
|
|
"Sysop Actions:~n"
|
|
" zx list approved Realm~n"
|
|
" zx accept ZSP~n"
|
|
" zx add package PackageName~n"
|
|
" zx add user ZPUF~n"
|
|
" zx rem user Realm UserName~n"
|
|
" zx add packager PackageName UserName~n"
|
|
" zx rem packager PackageName UserName~n"
|
|
" zx add maintainer PackageName UserName~n"
|
|
" zx rem maintainer PackageName UserName~n"
|
|
" zx create realm~n"
|
|
" zx takeover Realm~n"
|
|
" zx abdicate Realm~n~n".
|
|
|
|
usage_spec() ->
|
|
"Where~n"
|
|
" PackageID :: A string of the form [Realm-]Name[-Version]~n"
|
|
" PackageName :: A string that matches [^[a-z]a-z0-9_]~n"
|
|
" UserName :: A string that matches [^[a-z]a-z0-9_]~n"
|
|
" Args :: Arguments to pass to the application~n"
|
|
" Type :: The project type: a standalone \"app\" or a \"lib\"~n"
|
|
" Version :: Version string X, X.Y, or X.Y.Z: \"1\", \"1.2\", \"1.2.3\"~n"
|
|
" RealmFile :: Path to a valid .zrf realm file~n"
|
|
" Realm :: The name of a realm as a string [:a-z:]~n"
|
|
" Module :: Name of a code module.~n"
|
|
" KeyName :: The prefix of a keypair~n"
|
|
" Level :: The version level, one of \"major\", \"minor\", or \"patch\"~n"
|
|
" Path :: Path or filename.~n"
|
|
" ZSP :: Path to a .zsp file (Zomp Source Package).~n"
|
|
" ZPUF :: Path to a .zpuf file (Zomp Public User File).~n"
|
|
" ZDUF :: Path to a .zduf file (Zomp DANGEROUS User File).~n".
|
|
|
|
|
|
version() ->
|
|
io:format("zx ~ts~n", [os:getenv("ZX_VERSION")]).
|