573 lines
18 KiB
Erlang
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).
|