zx/zomp/lib/otpr/zx/0.1.0/src/zx_lib.erl
2018-05-24 13:25:13 +09:00

573 lines
18 KiB
Erlang

%%% @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, valid_version/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,
realm_conf/1, load_realm_conf/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}) ->
{ok, VersionString} = version_to_string(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 valid_version(zx:version()) -> boolean().
valid_version({z, z, z}) ->
true;
valid_version({X, z, z})
when is_integer(X), X >= 0 ->
true;
valid_version({X, Y, z})
when is_integer(X), X >= 0,
is_integer(Y), Y >= 0 ->
true;
valid_version({X, Y, Z})
when is_integer(X), X >= 0,
is_integer(Y), Y >= 0,
is_integer(Z), Z >= 0 ->
true;
valid_version(_) ->
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
{ok, [Realm, Name, VersionString]} ->
package_id(Realm, Name, VersionString);
{ok, [A, B]} ->
case valid_lower0_9(B) of
true -> package_id(A, B, "");
false -> package_id("otpr", A, B)
end;
{ok, [Name]} ->
package_id("otpr", Name, "");
error ->
{error, invalid_package_string}
end.
-spec dash_split(string()) -> {ok, [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.
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),
{ok, 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 :: zx: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 :: zx: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 :: zx: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)).
-spec realm_conf(Realm) -> RealmFileName
when Realm :: string(),
RealmFileName :: file:filename().
%% @private
%% Take a realm name, and return the name of the realm filename that would result.
realm_conf(Realm) ->
Realm ++ ".realm".
-spec load_realm_conf(Realm) -> Result
when Realm :: zx:realm(),
Result :: {ok, RealmConf}
| {error, Reason},
RealmConf :: list(),
Reason :: badarg
| terminated
| system_limit
| file:posix()
| {Line :: integer(), Mod :: module(), Cause :: term()}.
%% @private
%% Load the config for the given realm or halt with an error.
load_realm_conf(Realm) ->
Path = filename:join(zx_lib:zomp_home(), realm_conf(Realm)),
file:consult(Path).