657 lines
22 KiB
Erlang
657 lines
22 KiB
Erlang
%%% @doc
|
|
%%% ZX
|
|
%%%
|
|
%%% A general dependency and packaging tool that works together with the zomp
|
|
%%% package manager. Given a project directory with a standard layout, zx can:
|
|
%%% - Initialize your project for packaging and semver tracking under zomp.
|
|
%%% - Add dependencies (recursively) defined in any zomp repository realm.
|
|
%%% - Update dependencies (recursively) defined in any zomp repository realm.
|
|
%%% - Remove dependencies.
|
|
%%% - Update, upgrade or 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.
|
|
%%% @end
|
|
|
|
-module(zx).
|
|
-behavior(application).
|
|
-author("Craig Everett <zxq9@zxq9.com>").
|
|
-copyright("Craig Everett <zxq9@zxq9.com>").
|
|
-license("GPL-3.0").
|
|
|
|
|
|
-export([do/0, do/1]).
|
|
-export([subscribe/1, unsubscribe/0]).
|
|
-export([start/2, stop/1, stop/0]).
|
|
-export([usage_exit/1]).
|
|
|
|
-export_type([serial/0, package_id/0, package/0, realm/0, name/0, version/0,
|
|
identifier/0,
|
|
host/0,
|
|
key_id/0, key_name/0,
|
|
user_id/0, user_name/0, contact_info/0, user_data/0,
|
|
lower0_9/0, label/0,
|
|
package_meta/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_id() :: {realm(), key_name()}.
|
|
-type key_name() :: lower0_9().
|
|
-type user_id() :: {realm(), user_name()}.
|
|
-type user_name() :: label().
|
|
-type contact_info() :: {Type :: string(), Data :: string()}.
|
|
-type user_data() :: {ID :: user_id(),
|
|
RealName :: string(),
|
|
Contact :: [contact_info()],
|
|
Keys :: [key_name()]}.
|
|
-type lower0_9() :: [$a..$z | $0..$9 | $_].
|
|
-type label() :: [$a..$z | $0..$9 | $_ | $- | $.].
|
|
-type package_meta() :: #{package_id := package_id(),
|
|
deps := [package_id()],
|
|
type := app | lib}.
|
|
|
|
-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() ->
|
|
do([]).
|
|
|
|
|
|
-spec do(Args) -> no_return()
|
|
when Args :: [string()].
|
|
%% Dispatch work functions based on the nature of the input arguments.
|
|
|
|
do(["help"]) ->
|
|
usage_exit(0);
|
|
do(["run", PackageString | Args]) ->
|
|
ok = start(),
|
|
run(PackageString, Args);
|
|
do(["runlocal" | ArgV]) ->
|
|
ok = start(),
|
|
run_local(ArgV);
|
|
do(["init", "app", PackageString]) ->
|
|
ok = compatibility_check([unix]),
|
|
done(zx_local:initialize(app, PackageString));
|
|
do(["init", "lib", PackageString]) ->
|
|
ok = compatibility_check([unix]),
|
|
done(zx_local:initialize(lib, PackageString));
|
|
do(["list", "deps"]) ->
|
|
done(zx_local:list_deps());
|
|
do(["list", "deps", PackageString]) ->
|
|
done(zx_local:list_deps(PackageString));
|
|
do(["import", "zrp", PackageFile]) ->
|
|
done(zx_daemon:import_zrp(PackageFile));
|
|
do(["install", PackageString]) ->
|
|
done(zx_daemon:install(PackageString));
|
|
do(["set", "dep", PackageString]) ->
|
|
done(zx_local:set_dep(PackageString));
|
|
do(["set", "version", VersionString]) ->
|
|
ok = compatibility_check([unix]),
|
|
done(zx_local:set_version(VersionString));
|
|
do(["verup", Level]) ->
|
|
ok = compatibility_check([unix]),
|
|
done(zx_local:verup(Level));
|
|
do(["list", "realms"]) ->
|
|
done(zx_local:list_realms());
|
|
do(["list", "packages", Realm]) ->
|
|
ok = start(),
|
|
done(zx_local:list_packages(Realm));
|
|
do(["list", "versions", PackageName]) ->
|
|
ok = start(),
|
|
done(zx_local:list_versions(PackageName));
|
|
do(["add", "realm", RealmFile]) ->
|
|
done(zx_local:add_realm(RealmFile));
|
|
do(["drop", "dep", PackageString]) ->
|
|
PackageID = zx_lib:package_id(PackageString),
|
|
done(zx_local:drop_dep(PackageID));
|
|
do(["package"]) ->
|
|
{ok, TargetDir} = file:get_cwd(),
|
|
zx_local:package(TargetDir);
|
|
do(["package", TargetDir]) ->
|
|
case filelib:is_dir(TargetDir) of
|
|
true -> done(zx_local:package(TargetDir));
|
|
false -> done({error, "Target directory does not exist", 22})
|
|
end;
|
|
do(["dialyze"]) ->
|
|
done(zx_local:dialyze());
|
|
do(["create", "user"]) ->
|
|
done(zx_local:create_user());
|
|
do(["create", "keypair"]) ->
|
|
done(zx_local:grow_a_pair());
|
|
do(["drop", "key", Realm, KeyName]) ->
|
|
done(zx_local:drop_key({Realm, KeyName}));
|
|
do(["create", "plt"]) ->
|
|
done(zx_local:create_plt());
|
|
do(["create", "realm"]) ->
|
|
done(zx_local:create_realm());
|
|
do(["create", "realmfile", Realm]) ->
|
|
done(zx_local:create_realmfile(Realm, "."));
|
|
do(["takeover", Realm]) ->
|
|
done(zx_local:takeover(Realm));
|
|
do(["abdicate", Realm]) ->
|
|
done(zx_local:abdicate(Realm));
|
|
do(["drop", "realm", Realm]) ->
|
|
done(zx_local:drop_realm(Realm));
|
|
do(["list", "pending", PackageName]) ->
|
|
done(zx_auth:list_pending(PackageName));
|
|
do(["list", "resigns", Realm]) ->
|
|
done(zx_auth:list_resigns(Realm));
|
|
do(["submit", PackageFile]) ->
|
|
done(zx_auth:submit(PackageFile));
|
|
do(["review", PackageString]) ->
|
|
done(zx_auth:review(PackageString));
|
|
do(["approve", PackageString]) ->
|
|
PackageID = zx_lib:package_id(PackageString),
|
|
done(zx_auth:approve(PackageID));
|
|
do(["reject", PackageString]) ->
|
|
PackageID = zx_lib:package_id(PackageString),
|
|
done(zx_auth:reject(PackageID));
|
|
do(["accept", PackageString]) ->
|
|
done(zx_auth:accept(PackageString));
|
|
do(["add", "packager", Package, UserName]) ->
|
|
done(zx_auth:add_packager(Package, UserName));
|
|
do(["add", "maintainer", Package, UserName]) ->
|
|
done(zx_auth:add_maintainer(Package, UserName));
|
|
do(["add", "sysop", Package, UserName]) ->
|
|
done(zx_auth:add_sysop(Package, UserName));
|
|
do(["add", "package", PackageName]) ->
|
|
done(zx_auth:add_package(PackageName));
|
|
do(_) ->
|
|
usage_exit(22).
|
|
|
|
|
|
-spec done(outcome()) -> no_return().
|
|
|
|
done(ok) ->
|
|
halt(0);
|
|
done({error, Reason}) when is_atom(Reason) ->
|
|
ok = log(error, "Operation failed with: ~tp", [Reason]),
|
|
halt(1);
|
|
done({error, Code}) when is_integer(Code) ->
|
|
halt(Code);
|
|
done({error, Info, Code}) ->
|
|
ok = log(error, Info),
|
|
halt(Code).
|
|
|
|
|
|
-spec compatibility_check(Platforms) -> ok | no_return()
|
|
when Platforms :: unix | win32.
|
|
%% @private
|
|
%% Some commands only work on specific platforms because they leverage some specific
|
|
%% aspect on that platform, but not common to all. ATM this is mostly developer
|
|
%% commands that leverage things universal to *nix/posix shells but not Windows.
|
|
%% If equivalent procedures are written in Erlang then these restrictions can be
|
|
%% avoided -- but it is unclear whether there are any Erlang developers even using
|
|
%% Windows, so for now this is the bad version of the solution.
|
|
|
|
compatibility_check(Platforms) ->
|
|
{Family, Name} = os:type(),
|
|
case lists:member(Family, Platforms) of
|
|
true ->
|
|
ok;
|
|
false ->
|
|
Message = "Unfortunately this command is not available on ~tp ~tp",
|
|
ok = log(error, Message, [Family, Name]),
|
|
halt(0)
|
|
end.
|
|
|
|
|
|
|
|
%%% Application Start/Stop
|
|
|
|
-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.
|
|
|
|
|
|
|
|
%%% 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().
|
|
|
|
|
|
|
|
%%% Execution of application
|
|
|
|
-spec run(Identifier, RunArgs) -> no_return()
|
|
when Identifier :: 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(Identifier, RunArgs) ->
|
|
ok = file:set_cwd(zx_lib:zomp_dir()),
|
|
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:ppath(lib, PackageID),
|
|
{ok, Meta} = zx_lib:read_project_meta(Dir),
|
|
prepare(PackageID, Meta, Dir, RunArgs).
|
|
|
|
|
|
-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) ->
|
|
{ok, Meta} = zx_lib:read_project_meta(),
|
|
PackageID = maps:get(package_id, Meta),
|
|
ok = zx_lib:build(),
|
|
{ok, Dir} = file:get_cwd(),
|
|
ok = file:set_cwd(zx_lib:zomp_dir()),
|
|
prepare(PackageID, Meta, Dir, RunArgs).
|
|
|
|
|
|
-spec prepare(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.
|
|
|
|
prepare(PackageID, Meta, Dir, RunArgs) ->
|
|
{ok, PackageString} = zx_lib:package_string(PackageID),
|
|
ok = log(info, "Preparing ~ts...", [PackageString]),
|
|
Type = maps:get(type, Meta),
|
|
Deps = maps:get(deps, Meta),
|
|
NotInstalled = fun(P) -> not filelib:is_dir(zx_lib:ppath(lib, P)) end,
|
|
Needed = lists:filter(NotInstalled, Deps),
|
|
Pending = lists:map(fun zx_daemon:fetch/1, Needed),
|
|
case await_fetches(Pending) of
|
|
ok ->
|
|
ok = lists:foreach(fun install/1, Needed),
|
|
ok = lists:foreach(fun build/1, Needed),
|
|
execute(Type, PackageID, Meta, Dir, RunArgs);
|
|
{error, Errors} ->
|
|
error_exit("Failed package fetches: ~tp", [Errors], ?LINE)
|
|
end.
|
|
|
|
|
|
await_fetches([]) -> ok;
|
|
await_fetches(Pending) -> await_fetches(Pending, []).
|
|
|
|
|
|
await_fetches([], []) ->
|
|
ok;
|
|
await_fetches([], Errors) ->
|
|
{error, Errors};
|
|
await_fetches(Pending, Errors) ->
|
|
{NewPending, NewErrors} =
|
|
receive
|
|
{z_reply, ID, ok} ->
|
|
{lists:delete(ID, Pending), Errors};
|
|
{z_reply, ID, {error, Package, Reason}} ->
|
|
{lists:delete(ID, Pending), [{Package, Reason} | Errors]}
|
|
end,
|
|
await_fetches(NewPending, NewErrors).
|
|
|
|
|
|
-spec execute(Type, PackageID, Meta, Dir, RunArgs) -> no_return()
|
|
when Type :: app | lib,
|
|
PackageID :: package_id(),
|
|
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) ->
|
|
{ok, PackageString} = zx_lib:package_string(PackageID),
|
|
ok = log(info, "Starting ~ts.", [PackageString]),
|
|
Name = element(2, PackageID),
|
|
AppTag = list_to_atom(Name),
|
|
{AppMod, _} = maps:get(appmod, Meta),
|
|
ok = zx_daemon:pass_meta(Meta, Dir, RunArgs),
|
|
ok = ensure_all_started(AppTag),
|
|
ok = pass_argv(AppMod, RunArgs),
|
|
log(info, "Launcher complete.");
|
|
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 = 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) ->
|
|
case lists:member({accept_argv, 1}, AppMod:module_info(exports)) of
|
|
true -> AppMod:accept_argv(Args);
|
|
false -> ok
|
|
end.
|
|
|
|
|
|
|
|
|
|
-spec ensure_installed(PackageID) -> Result | no_return()
|
|
when PackageID :: package_id(),
|
|
Result :: {ok, ActualID :: package_id()}.
|
|
%% @private
|
|
%% Given a PackageID, check whether it is installed on the system, and if not, ensure
|
|
%% that the package is either in the cache or can be downloaded. If all attempts at
|
|
%% locating or acquiring the package fail, then exit with an error.
|
|
|
|
ensure_installed(PackageID = {Realm, Name, Version}) ->
|
|
case resolve_installed_version(PackageID) of
|
|
exact -> {ok, PackageID};
|
|
{ok, Installed} -> {ok, {Realm, Name, Installed}};
|
|
not_found -> fetch_one({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.
|
|
|
|
|
|
-spec resolve_installed_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_installed_version({Realm, Name, Version}) ->
|
|
PackageDir = zx_lib:path(lib, Realm, Name),
|
|
case filelib:is_dir(PackageDir) of
|
|
true -> resolve_installed_version(PackageDir, Version);
|
|
false -> not_found
|
|
end.
|
|
|
|
|
|
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.
|
|
|
|
|
|
-spec build(package_id()) -> ok.
|
|
%% @private
|
|
%% Given an AppID, build the project from source and add it to the current lib path.
|
|
|
|
build(PackageID) ->
|
|
{ok, CWD} = file:get_cwd(),
|
|
ok = file:set_cwd(zx_lib:ppath(lib, PackageID)),
|
|
ok = zx_lib:build(),
|
|
file:set_cwd(CWD).
|
|
|
|
|
|
-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) ->
|
|
Dirs = [zx_lib:ppath(D, PackageID) || D <- [etc, var, tmp, log, lib]],
|
|
lists:foreach(fun zx_lib:force_dir/1, Dirs).
|
|
|
|
|
|
|
|
%%% Usage
|
|
|
|
-spec usage_exit(Code) -> no_return()
|
|
when Code :: integer().
|
|
%% @private
|
|
%% A convenience function that will display the zx usage message before halting
|
|
%% with the provided exit code.
|
|
|
|
usage_exit(Code) ->
|
|
ok = usage(),
|
|
halt(Code).
|
|
|
|
|
|
-spec usage() -> ok.
|
|
%% @private
|
|
%% Display the zx command line usage message.
|
|
|
|
usage() ->
|
|
T =
|
|
"ZX usage: zx [command] [object] [args]~n"
|
|
"~n"
|
|
"User Actions:~n"
|
|
" zx help~n"
|
|
" zx run PackageID [Args]~n"
|
|
" zx runlocal [Args]~n"
|
|
" zx list realms~n"
|
|
" zx list packages Realm~n"
|
|
" zx list versions PackageID~n"
|
|
" zx latest PackageID~n"
|
|
" zx add realm RealmFile~n"
|
|
" zx drop realm Realm~n"
|
|
" zx install PackageID~n"
|
|
" zx logpath [Package [1-10]]~n"
|
|
" zx status~n"
|
|
" zx set timeout Value~n"
|
|
" zx set mirror Realm Host:Port~n"
|
|
"~n"
|
|
"Developer/Packager/Maintainer Actions:~n"
|
|
" zx create project [app | lib] PackageID~n"
|
|
" zx init Type PackageID~n"
|
|
" zx list deps [PackageID]~n"
|
|
" zx set dep PackageID~n"
|
|
" zx drop dep PackageID~n"
|
|
" zx verup Level~n"
|
|
" zx set version Version~n"
|
|
" zx update .app~n"
|
|
" zx create plt~n"
|
|
" zx dialyze~n"
|
|
" zx package Path~n"
|
|
" zx submit ZspFile~n"
|
|
" zx list pending PackageName~n"
|
|
" zx list resigns Realm~n"
|
|
" zx list packagers PackageName~n"
|
|
" zx list maintainers PackageName~n"
|
|
" zx list sysops Realm~n"
|
|
" zx review PackageID~n"
|
|
" zx approve PackageID~n"
|
|
" zx reject PackageID~n"
|
|
" zx add key Realm KeyName~n"
|
|
" zx get key Realm KeyName~n"
|
|
" zx create user~n"
|
|
" zx create userfiles Realm UserName~n"
|
|
" zx create keypair~n"
|
|
" zx export user UserID~n"
|
|
" zx import user ZdufFile~n"
|
|
"~n"
|
|
"Sysop Actions:~n"
|
|
" zx add user ZpufFile~n"
|
|
" zx add package PackageName~n"
|
|
" zx add packager PackageName UserID~n"
|
|
" zx add maintainer PackageName UserID~n"
|
|
" zx add sysop UserID~n"
|
|
" zx accept PackageID~n"
|
|
" zx create realm~n"
|
|
" zx create realmfile Realm~n"
|
|
"~n"
|
|
"Where~n"
|
|
" PackageID :: A string of the form Realm-Name[-Version]~n"
|
|
" Args :: Arguments to pass to the application~n"
|
|
" Type :: The project type: a standalone \"app\" or a \"lib\"~n"
|
|
" Version :: Version string X, X.Y, or X.Y.Z: \"1\", \"1.2\", \"1.2.3\"~n"
|
|
" RealmFile :: Path to a valid .zrf realm file~n"
|
|
" Realm :: The name of a realm as a string [:a-z:]~n"
|
|
" KeyName :: The prefix of a keypair to drop~n"
|
|
" Level :: The version level, one of \"major\", \"minor\", or \"patch\"~n"
|
|
" Path :: Path to a valid project directory or .zsp file~n",
|
|
io:format(T).
|
|
|
|
|
|
|
|
%%% Error exits
|
|
|
|
-spec error_exit(Format, Args, Line) -> no_return()
|
|
when Format :: string(),
|
|
Args :: [term()],
|
|
Line :: non_neg_integer().
|
|
%% @private
|
|
%% Format an error message in a way that makes it easy to locate.
|
|
|
|
error_exit(Format, Args, Line) ->
|
|
File = filename:basename(?FILE),
|
|
ok = log(error, "~ts:~tp: " ++ Format, [File, Line | Args]),
|
|
halt(1).
|