diff --git a/zx b/zx index 60bb198..e1a35e3 100755 --- a/zx +++ b/zx @@ -19,22 +19,29 @@ -record(s, {realm = "otpr" :: name(), - app = none :: none | name(), + name = none :: none | name(), version = {z, z, z} :: version(), pid = none :: none | pid(), mon = none :: none | reference()}). --type state() :: #s{}. --type name() :: string(). --type version() :: {Major :: z | non_neg_integer(), - Minor :: z | non_neg_integer(), - Patch :: z | non_neg_integer()}. --type app_id() :: {Realm :: name(), - App :: name(), - Version :: version()}. --type option() :: {string(), term()}. --type peer() :: {inet:hostname() | inet:ip_address(), inet:port_number()}. +-type state() :: #s{}. +%-type serial() :: pos_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 option() :: {string(), term()}. +-type host() :: {string() | inet:ip_address(), inet:port_number()}. +%-type keybin() :: {ID :: key_id(), +% Type :: public | private, +% DER :: binary()}. +-type key_id() :: {realm(), KeyName :: lower0_9()}. +-type lower0_9() :: [$a..$z | $0..$9 | $_]. +%-type label() :: [$a..$z | $0..$9 | $_ | $- | $.]. @@ -56,23 +63,23 @@ main(Args) -> start(["help"]) -> usage_exit(0); -start(["run", AppString | Args]) -> - execute(AppString, Args); -start(["init", "app", AppString]) -> - AppID = appstring_to_appid(AppString), - initialize(app, AppID); -start(["init", "lib", AppString]) -> - AppID = appstring_to_appid(AppString), - initialize(lib, AppID); +start(["run", PackageString | Args]) -> + execute(PackageString, Args); +start(["init", "app", PackageString]) -> + PackageID = package_id(PackageString), + initialize(app, PackageID); +start(["init", "lib", PackageString]) -> + PackageID = package_id(PackageString), + initialize(lib, PackageID); start(["install", PackageFile]) -> assimilate(PackageFile); -start(["set", "dep", AppString]) -> - set_dep(AppString); +start(["set", "dep", PackageString]) -> + set_dep(PackageString); start(["set", "version", VersionString]) -> set_version(VersionString); -start(["drop", "dep", AppString]) -> - AppID = appstring_to_appid(AppString), - drop_dep(AppID); +start(["drop", "dep", PackageString]) -> + PackageID = package_id(PackageString), + drop_dep(PackageID); start(["drop", "key", KeyID]) -> drop_key(KeyID); start(["verup", Level]) -> @@ -116,13 +123,13 @@ start(_) -> %% 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 provided should be a valid AppString of the form `realm-appname-version' +%% Identifier provided should be a valid PackageString of the form `realm-appname-version' %% where the realm and appname should follow standard realm and app 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 `vx' will sit in an `exec_wait' state, waiting for either a direct message from +%% 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 vx_lib to assist in environment discovery. %% %% If there is a problem anywhere in the locationg, discovery, building, and loading @@ -131,35 +138,37 @@ start(_) -> execute(Identifier, Args) -> true = register(zx, self()), ok = inets:start(), - AppID = {Realm, App, Version} = appstring_to_appid(Identifier), + PackageID = {Realm, Name, Version} = package_id(Identifier), ok = file:set_cwd(zomp_dir()), - AppRoot = filename:join("lib", Identifier), - ok = ensure_installed(AppID), - {ok, Meta} = file:consult(filename:join(AppRoot, "zomp.meta")), + PackageRoot = filename:join("lib", Identifier), + ok = ensure_installed(PackageID), + {ok, Meta} = file:consult(filename:join(PackageRoot, "zomp.meta")), {deps, Deps} = lists:keyfind(deps, 1, Meta), - Required = [AppID | Deps], + Required = [PackageID | Deps], Needed = scrub(Required), - ok = fetch(Needed), + Host = {"localhost", 11411}, + Socket = connect(Host, user), + ok = fetch(Socket, Needed), ok = lists:foreach(fun install/1, Needed), ok = lists:foreach(fun build/1, Required), - ok = file:set_cwd(AppRoot), + ok = file:set_cwd(PackageRoot), case lists:keyfind(type, 1, Meta) of {type, app} -> - ok = log(info, "Starting ~ts", [appid_to_appstring(AppID)]), - AppMod = list_to_atom(App), - {ok, Pid} = AppMod:start(normal, Args), + ok = log(info, "Starting ~ts", [package_string(PackageID)]), + PackageMod = list_to_atom(Name), + {ok, Pid} = PackageMod:start(normal, Args), Mon = monitor(process, Pid), Shell = spawn(shell, start, []), ok = log(info, "Your shell is ~p, application is: ~p", [Shell, Pid]), State = #s{realm = Realm, - app = App, + name = Name, version = Version, pid = Pid, mon = Mon}, exec_wait(State); {type, lib} -> Message = "Lib ~ts is available on the system, but is not a standalone app.", - ok = log(info, Message, [appid_to_appstring(AppID)]), + ok = log(info, Message, [package_string(PackageID)]), halt(0) end. @@ -168,26 +177,26 @@ execute(Identifier, Args) -> %%% Project initialization --spec initialize(Type, AppID) -> no_return() - when Type :: app | lib, - AppID :: app_id(). +-spec initialize(Type, PackageID) -> no_return() + when Type :: app | lib, + PackageID :: package_id(). %% @private -%% Initialize an application in the local directory based on the AppID provided. +%% Initialize an application in the local directory based on the PackageID provided. %% This function does not care about the name of the current directory and leaves -%% providing a complete, proper and accurate AppID. +%% providing a complete, proper and accurate PackageID. %% This function will check the current `lib/' directory for zomp-style dependencies. %% If this is not the intended function or if there are non-compliant directory names %% in `lib/' then the project will need to be rearranged to become zomp compliant or %% the `deps' section of the resulting meta file will need to be manually updated. -initialize(Type, AppID) -> - AppString = appid_to_appstring(AppID), - ok = log(info, "Initializing ~s...", [AppString]), - Meta = [{app_id, AppID}, - {deps, []}, - {type, Type}], +initialize(Type, PackageID) -> + PackageString = package_string(PackageID), + ok = log(info, "Initializing ~s...", [PackageString]), + Meta = [{package_id, PackageID}, + {deps, []}, + {type, Type}], ok = write_terms("zomp.meta", Meta), - ok = log(info, "Project ~tp initialized.", [AppString]), + ok = log(info, "Project ~tp initialized.", [PackageString]), Message = "NOTICE:~n" " This project is currently listed as having no dependencies.~n" @@ -201,12 +210,12 @@ initialize(Type, AppID) -> %%% Add a package from a local file --spec assimilate(PackageFile) -> AppID +-spec assimilate(PackageFile) -> PackageID when PackageFile :: file:filename(), - AppID :: app_id(). + PackageID :: package_id(). %% @private %% Receives a path to a file containing package data, examines it, and copies it to a -%% canonical location under a canonical name, returning the AppID of the package +%% canonical location under a canonical name, returning the PackageID of the package %% contents. assimilate(PackageFile) -> @@ -215,23 +224,22 @@ assimilate(PackageFile) -> ok = file:set_cwd(zomp_dir()), {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), Meta = binary_to_term(MetaBin), - {package_id, AppID} = lists:keyfind(package_id, 1, Meta), - TgzFile = namify_tgz(AppID), + {package_id, PackageID} = lists:keyfind(package_id, 1, Meta), + TgzFile = namify_tgz(PackageID), {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), {sig, {KeyID, Signature}} = lists:keyfind(sig, 1, Meta), - KeyFile = filename:join("key", KeyID), - {ok, PubKey} = loadkey(public, KeyFile), + {ok, PubKey} = loadkey(public, KeyID), ok = case public_key:verify(TgzData, sha512, Signature, PubKey) of true -> - ZrpPath = filename:join("zrp", namify_zrp(AppID)), + ZrpPath = filename:join("zrp", namify_zrp(PackageID)), erl_tar:create(ZrpPath, Files); false -> error_exit("Bad package signature: ~ts", [PackageFile], ?FILE, ?LINE) end, ok = file:set_cwd(CWD), Message = "~ts is now locally available.", - ok = log(info, Message, [appid_to_appstring(AppID)]), + ok = log(info, Message, [package_string(PackageID)]), halt(0). @@ -239,94 +247,94 @@ assimilate(PackageFile) -> %%% Set dependency --spec set_dep(AppString) -> no_return() - when AppString :: string(). +-spec set_dep(PackageString) -> no_return() + when PackageString :: string(). %% @private %% Set a specific dependency in the current project. If the project currently has a -%% dependency on the same Realm-App then the version of that dependency is updated to -%% reflect that in the AppString argument. The AppString is permitted to be incomplete. -%% Incomplete elements of the included VersionString (if included) will default to the -%% latest version available at the indicated level. +%% dependency on the same package then the version of that dependency is updated to +%% reflect that in the PackageString argument. The AppString is permitted to be +%% incomplete. Incomplete elements of the VersionString (if included) will default to +%% the latest version available at the indicated level. -set_dep(AppString) -> - AppID = appstring_to_appid(AppString), +set_dep(PackageString) -> + PackageID = package_id(PackageString), Meta = read_meta(), {deps, Deps} = lists:keyfind(deps, 1, Meta), - case lists:member(AppID, Deps) of + case lists:member(PackageID, Deps) of true -> - ok = log(info, "~ts is already a dependency", [AppString]), + ok = log(info, "~ts is already a dependency", [PackageString]), halt(0); false -> - set_dep(AppID, Deps, Meta) + set_dep(PackageID, Deps, Meta) end. --spec set_dep(AppID, Deps, Meta) -> no_return() - when AppID :: app_id(), - Deps :: [app_id()], - Meta :: [term()]. +-spec set_dep(PackageID, Deps, Meta) -> no_return() + when PackageID :: package_id(), + Deps :: [package_id()], + Meta :: [term()]. %% @private -%% Given the AppID, list of Deps and the current contents of the project Meta, add or -%% update Deps to include (or update) Deps to reflect a dependency on AppID, if such a -%% dependency is not already present. Then write the project meta back to its file and -%% exit. +%% Given the PackageID, list of Deps and the current contents of the project Meta, add +%% or update Deps to include (or update) Deps to reflect a dependency on PackageID, if +%% such a dependency is not already present. Then write the project meta back to its +%% file and exit. -set_dep(AppID = {Realm, App, NewVersion}, Deps, Meta) -> +set_dep(PackageID = {Realm, Name, NewVersion}, Deps, Meta) -> {ok, CWD} = file:get_cwd(), ok = file:set_cwd(zomp_dir()), - ok = ensure_installed(AppID), + ok = ensure_installed(PackageID), ok = file:set_cwd(CWD), - ExistingApp = fun ({R, A, _}) -> {R, A} == {Realm, App} end, + ExistingPackageIDs = fun ({R, N, _}) -> {R, N} == {Realm, Name} end, NewDeps = - case lists:partition(ExistingApp, Deps) of - {[{Realm, App, OldVersion}], Rest} -> + case lists:partition(ExistingPackageIDs, Deps) of + {[{Realm, Name, OldVersion}], Rest} -> Message = "Updating dep ~ts to ~ts", - OldAppString = appid_to_appstring({Realm, App, OldVersion}), - NewAppString = appid_to_appstring({Realm, App, NewVersion}), - ok = log(info, Message, [OldAppString, NewAppString]), - [AppID | Rest]; + OldPackageString = package_string({Realm, Name, OldVersion}), + NewPackageString = package_string({Realm, Name, NewVersion}), + ok = log(info, Message, [OldPackageString, NewPackageString]), + [PackageID | Rest]; {[], Deps} -> - ok = log(info, "Adding dep ~ts", [appid_to_appstring(AppID)]), - [AppID | Deps] + ok = log(info, "Adding dep ~ts", [package_string(PackageID)]), + [PackageID | Deps] end, NewMeta = lists:keystore(deps, 1, Meta, {deps, NewDeps}), ok = write_terms("zomp.meta", NewMeta), halt(0). --spec ensure_installed(app_id()) -> ok | no_return(). +-spec ensure_installed(package_id()) -> ok | no_return(). %% @private -%% Given an AppID, check whether it is installed on the system, and if not, ensure +%% 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(AppID) -> - AppString = appid_to_appstring(AppID), - AppDir = filename:join("lib", AppString), - case filelib:is_dir(AppDir) of +ensure_installed(PackageID) -> + PackageString = package_string(PackageID), + PackageDir = filename:join("lib", PackageString), + case filelib:is_dir(PackageDir) of true -> ok; - false -> ensure_dep(AppID) + false -> ensure_dep(PackageID) end. --spec ensure_dep(app_id()) -> ok | no_return(). +-spec ensure_dep(package_id()) -> ok | no_return(). %% @private -%% Given an AppID 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. +%% 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(AppID) -> - ZrpFile = filename:join("zrp", namify_zrp(AppID)), +ensure_dep(PackageID) -> + ZrpFile = filename:join("zrp", namify_zrp(PackageID)), ok = case filelib:is_regular(ZrpFile) of true -> ok; false -> - AppString = appid_to_appstring(AppID), - log(error, "Would fetch ~ts now, but not implemented", [AppString]), + PackageString = package_string(PackageID), + log(error, "Would fetch ~ts now, but not implemented", [PackageString]), halt(0) end, - install(AppID). + install(PackageID). @@ -341,7 +349,7 @@ ensure_dep(AppID) -> set_version(VersionString) -> NewVersion = - case check_version(VersionString) of + case string_to_version(VersionString) of {_, _, z} -> Message = "'set version' arguments must be complete, ex: 1.2.3", ok = log(error, Message), @@ -366,17 +374,17 @@ set_version(VersionString) -> update_version(Arg) -> Meta = read_meta(), - {app_id, AppID} = lists:keyfind(app_id, 1, Meta), - update_version(Arg, AppID, Meta). + {package_id, PackageID} = lists:keyfind(package_id, 1, Meta), + update_version(Arg, PackageID, Meta). --spec update_version(Level, AppID, Meta) -> no_return() - when Level :: major - | minor - | patch - | version(), - AppID :: app_id(), - Meta :: [{atom(), term()}]. +-spec update_version(Level, PackageID, Meta) -> no_return() + when Level :: major + | minor + | patch + | version(), + PackageID :: package_id(), + Meta :: [{atom(), term()}]. %% @private %% Update a project's `zomp.meta' file by either incrementing the indicated component, %% or setting the version number to the one specified in VersionString. @@ -384,22 +392,22 @@ update_version(Arg) -> %% convert the VersionString (if it is passed) to a `version()' type and check its %% validity (or halt if it is a bad string). -update_version(major, {Realm, App, OldVersion = {Major, _, _}}, OldMeta) -> +update_version(major, {Realm, Name, OldVersion = {Major, _, _}}, OldMeta) -> NewVersion = {Major + 1, 0, 0}, - update_version(Realm, App, OldVersion, NewVersion, OldMeta); -update_version(minor, {Realm, App, OldVersion = {Major, Minor, _}}, OldMeta) -> + update_version(Realm, Name, OldVersion, NewVersion, OldMeta); +update_version(minor, {Realm, Name, OldVersion = {Major, Minor, _}}, OldMeta) -> NewVersion = {Major, Minor + 1, 0}, - update_version(Realm, App, OldVersion, NewVersion, OldMeta); -update_version(patch, {Realm, App, OldVersion = {Major, Minor, Patch}}, OldMeta) -> + update_version(Realm, Name, OldVersion, NewVersion, OldMeta); +update_version(patch, {Realm, Name, OldVersion = {Major, Minor, Patch}}, OldMeta) -> NewVersion = {Major, Minor, Patch + 1}, - update_version(Realm, App, OldVersion, NewVersion, OldMeta); -update_version(NewVersion, {Realm, App, OldVersion}, OldMeta) -> - update_version(Realm, App, OldVersion, NewVersion, OldMeta). + update_version(Realm, Name, OldVersion, NewVersion, OldMeta); +update_version(NewVersion, {Realm, Name, OldVersion}, OldMeta) -> + update_version(Realm, Name, OldVersion, NewVersion, OldMeta). --spec update_version(Realm, App, OldVersion, NewVersion, OldMeta) -> no_return() - when Realm :: name(), - App :: name(), +-spec update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> no_return() + when Realm :: realm(), + Name :: name(), OldVersion :: version(), NewVersion :: version(), OldMeta :: [{atom(), term()}]. @@ -410,8 +418,9 @@ update_version(NewVersion, {Realm, App, OldVersion}, OldMeta) -> %% turns out to be possible. If successful it will indicate to the user what was %% changed. -update_version(Realm, App, OldVersion, NewVersion, OldMeta) -> - NewMeta = lists:keystore(app_id, 1, OldMeta, {app_id, {Realm, App, NewVersion}}), +update_version(Realm, Name, OldVersion, NewVersion, OldMeta) -> + PackageID = {Realm, Name, NewVersion}, + NewMeta = lists:keystore(package_id, 1, OldMeta, {package_id, PackageID}), ok = write_terms("zomp.meta", NewMeta), ok = log(info, "Version changed from ~s to ~s.", @@ -423,24 +432,24 @@ update_version(Realm, App, OldVersion, NewVersion, OldMeta) -> %%% Drop dependency --spec drop_dep(app_id()) -> no_return(). +-spec drop_dep(package_id()) -> no_return(). %% @private %% Remove the indicate dependency from the local project's zomp.meta record. -drop_dep(AppID) -> - AppString = appid_to_appstring(AppID), +drop_dep(PackageID) -> + PackageString = package_string(PackageID), Meta = read_meta(), {deps, Deps} = lists:keyfind(deps, 1, Meta), - case lists:member(AppID, Deps) of + case lists:member(PackageID, Deps) of true -> - NewDeps = lists:delete(AppID, Deps), + NewDeps = lists:delete(PackageID, Deps), NewMeta = lists:keystore(deps, 1, Meta, {deps, NewDeps}), ok = write_terms("zomp.meta", NewMeta), Message = "~ts removed from dependencies.", - ok = log(info, Message, [AppString]), + ok = log(info, Message, [PackageString]), halt(0); false -> - ok = log(info, "~ts not found in dependencies.", [AppString]), + ok = log(info, "~ts not found in dependencies.", [PackageString]), halt(0) end. @@ -449,24 +458,22 @@ drop_dep(AppID) -> %%% Drop key --spec drop_key(KeyID) -> no_return() - when KeyID :: file:filename(). +-spec drop_key(key_id()) -> no_return(). %% @private %% Given a KeyID, remove the related public and private keys from the keystore, if they %% exist. If not, exit with a message that no keys were found, but do not return an %% error exit value (this instruction is idempotent if used in shell scripts). -drop_key(KeyID) -> +drop_key({Realm, KeyName}) -> ok = file:set_cwd(zomp_dir()), - KeyDir = filename:join(zomp_dir(), "key"), - Pattern = KeyID ++ ".{key,pub}.der", - case filelib:wildcard(filename:join(KeyDir, Pattern)) of + Pattern = filename:join([zomp_dir(), "key", Realm, KeyName ++ ".{key,pub}.der"]), + case filelib:wildcard(Pattern) of [] -> - ok = log(warning, "KeyID ~ts not found", [KeyID]), + ok = log(warning, "Key ~ts/~ts not found", [Realm, KeyName]), halt(0); Files -> ok = lists:foreach(fun file:delete/1, Files), - ok = log(info, "Keyset ~ts removed", [KeyID]), + ok = log(info, "Keyset ~ts/~ts removed", [Realm, KeyName]), halt(0) end. @@ -504,33 +511,36 @@ run_local(Args) -> true = register(zx, self()), ok = inets:start(), {ok, ProjectRoot} = file:get_cwd(), - {ok, Meta} = file:consult("zomp.meta"), - {app_id, AppID = {Realm, App, Version}} = lists:keyfind(app_id, 1, Meta), + Meta = read_meta(), + {package_id, PackageID} = lists:keyfind(package_id, 1, Meta), + {Realm, Name, Version} = PackageID, ok = build(), ok = file:set_cwd(zomp_dir()), {deps, Deps} = lists:keyfind(deps, 1, Meta), Needed = scrub(Deps), - ok = fetch(Needed), + Host = {"localhost", 11411}, + Socket = connect(Host, user), + ok = fetch(Socket, Needed), ok = lists:foreach(fun install/1, Needed), ok = lists:foreach(fun build/1, Deps), ok = file:set_cwd(ProjectRoot), case lists:keyfind(type, 1, Meta) of {type, app} -> - ok = log(info, "Starting ~ts", [appid_to_appstring(AppID)]), - AppMod = list_to_atom(App), + ok = log(info, "Starting ~ts", [package_string(PackageID)]), + AppMod = list_to_atom(Name), {ok, Pid} = AppMod:start(normal, Args), Mon = monitor(process, Pid), Shell = spawn(shell, start, []), ok = log(info, "Your shell is ~p, application is: ~p", [Shell, Pid]), State = #s{realm = Realm, - app = App, + name = Name, version = Version, pid = Pid, mon = Mon}, exec_wait(State); {type, lib} -> Message = "Lib ~ts is available on the system, but is not a standalone app", - ok = log(info, Message, [appid_to_appstring(AppID)]), + ok = log(info, Message, [package_string(PackageID)]), halt(0) end. @@ -546,39 +556,41 @@ run_local(Args) -> package(TargetDir) -> ok = log(info, "Packaging ~ts", [TargetDir]), - KeyDir = filename:join(zomp_dir(), "key"), + {ok, Meta} = file:consult(filename:join(TargetDir, "zomp.meta")), + {package_id, {Realm, _, _}} = lists:keyfind(package_id, 1, Meta), + KeyDir = filename:join([zomp_dir(), "key", Realm]), ok = force_dir(KeyDir), Pattern = KeyDir ++ "/*.key.der", case [filename:basename(F, ".key.der") || F <- filelib:wildcard(Pattern)] of [] -> ok = log(info, "Need to generate key"), - KeyPrefix = prompt_keygen(KeyDir), - ok = generate_rsa(KeyPrefix), - package(KeyPrefix, TargetDir); - [KeyPrefix] -> - ok = log(info, "Using key: ~ts", [KeyPrefix]), - package(KeyPrefix, TargetDir); - KeyPrefixes -> - KeyPrefix = select_string(KeyPrefixes), - package(KeyPrefix, TargetDir) + KeyID = prompt_keygen(), + ok = generate_rsa(KeyID), + package(KeyID, TargetDir); + [KeyName] -> + KeyID = {Realm, KeyName}, + ok = log(info, "Using key: ~ts/~ts", [Realm, KeyName]), + package(KeyID, TargetDir); + KeyNames -> + KeyName = select_string(KeyNames), + package({Realm, KeyName}, TargetDir) end. --spec package(KeyPrefix, TargetDir) -> no_return() - when KeyPrefix :: string(), +-spec package(KeyID, TargetDir) -> no_return() + when KeyID :: key_id(), TargetDir :: file:filename(). %% @private %% Accept a KeyPrefix for signing and a TargetDir containing a project to package and %% build a zrp package file ready to be submitted to a repository. -package(KeyPrefix, TargetDir) -> - KeyID = KeyPrefix ++ ".key.der", - PubID = KeyPrefix ++ ".pub.der", +package(KeyID, TargetDir) -> {ok, Meta} = file:consult(filename:join(TargetDir, "zomp.meta")), - {app_id, AppID} = lists:keyfind(app_id, 1, Meta), - AppString = appid_to_appstring(AppID), - ZrpFile = AppString ++ ".zrp", - TgzFile = AppString ++ ".tgz", + {package_id, PackageID} = lists:keyfind(package_id, 1, Meta), + true = element(1, PackageID) == element(1, KeyID), + PackageString = package_string(PackageID), + ZrpFile = PackageString ++ ".zrp", + TgzFile = PackageString ++ ".tgz", ok = halt_if_exists(ZrpFile), ok = remove_binaries(TargetDir), {ok, Everything} = file:list_dir(TargetDir), @@ -587,13 +599,15 @@ package(KeyPrefix, TargetDir) -> Targets = lists:subtract(Everything, Ignores), {ok, CWD} = file:get_cwd(), ok = file:set_cwd(TargetDir), + ok = build(), + Modules = [filename:basename(M, ".beam") || M <- filelib:wildcard("*.beam", "ebin")], + ok = remove_binaries("."), ok = erl_tar:create(filename:join(CWD, TgzFile), Targets, [compressed]), ok = file:set_cwd(CWD), - KeyFile = filename:join([zomp_dir(), "key", KeyID]), - {ok, Key} = loadkey(private, KeyFile), + {ok, Key} = loadkey(private, KeyID), {ok, TgzBin} = file:read_file(TgzFile), Sig = public_key:sign(TgzBin, sha512, Key), - FinalMeta = [{sig, {PubID, Sig}} | Meta], + FinalMeta = [{modules, Modules}, {sig, {KeyID, Sig}} | Meta], ok = file:write_file("zomp.meta", term_to_binary(FinalMeta)), ok = erl_tar:create(ZrpFile, ["zomp.meta", TgzFile]), ok = file:delete(TgzFile), @@ -689,24 +703,23 @@ submit(PackageFile) -> {ok, PackageData} = file:read_file(PackageFile), {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), Meta = binary_to_term(MetaBin), - {app_id, {Realm, Package, Version}} = lists:keyfind(app_id, 1, Meta), - {sig, {PublicKeyFile, _}} = lists:keyfind(sig, 1, Meta), - KeyID = filename:rootname(PublicKeyFile, ".pub.der"), + {package_id, {Realm, Package, Version}} = lists:keyfind(package_id, 1, Meta), + {sig, {KeyID, _}} = lists:keyfind(sig, 1, Meta), true = ensure_keypair(KeyID), RealmData = realm_data(Realm), {prime, Prime} = lists:keyfind(prime, 1, RealmData), - Socket = connect(Prime), + Socket = connect(Prime, {auth, KeyID}), ok = send(Socket, {submit, {Realm, Package, Version}}), ok = receive - {tcp, Socket, Response} -> - case binary_to_term(Response, [safe]) of + {tcp, Socket, Response1} -> + case binary_to_term(Response1, [safe]) of ready -> ok; {error, Reason} -> ok = log(info, "Server refused with ~tp", [Reason]), halt(0) - end; + end after 5000 -> ok = log(warning, "Server timed out!"), halt(0) @@ -715,8 +728,8 @@ submit(PackageFile) -> ok = log(info, "Done sending contents of ~tp", [PackageFile]), ok = receive - {tcp, Socket, Response} -> - log(info, "Response: ~tp", [Response]); + {tcp, Socket, Response2} -> + log(info, "Response: ~tp", [Response2]); Other -> log(warning, "Unexpected message: ~tp", [Other]) after 5000 -> @@ -733,34 +746,56 @@ submit(PackageFile) -> %% Wrapper for the procedure necessary to send an internal message over the wire. send(Socket, Message) -> - BinMessage = term_to_binary(Message), - gen_tcp:send(Socket, Message). + Bin = term_to_binary(Message), + gen_tcp:send(Socket, Bin). --spec connect(peer()) -> inet:socket() | no_return(). +-spec connect(Node, Type) -> gen_tcp:socket() | no_return() + when Node :: host(), + Type :: user | {auth, key_id()}. %% @private %% Connect to one of the servers in the realm constellation. -connect({Host, Port}) -> +connect({Host, Port}, Type) -> Options = [{packet, 4}, {mode, binary}, {active, true}], case gen_tcp:connect(Host, Port, Options) of {ok, Socket} -> - confirm_server(Socket); + confirm_server(Socket, Type); {error, Error} -> ok = log(warning, "Connection problem: ~tp", [Error]), halt(0) end. --spec confirm_server(inet:socket()) -> inet:socket() | no_return(). +-spec confirm_server(Socket, Type) -> gen_tcp:socket() | no_return() + when Socket :: gen_tcp:socket(), + Type :: user | {auth, key_id()}. %% @private %% Send a protocol ID string to notify the server what we're up to, disconnect %% if it does not return an "OK" response within 5 seconds. -confirm_server(Socket) -> +confirm_server(Socket, user) -> {ok, {Addr, Port}} = inet:peername(Socket), Host = inet:ntoa(Addr), - ok = gen_tcp:send(Socket, <<"OTPR 1 CLIENT">>), + ok = gen_tcp:send(Socket, <<"OTPR USER 1">>), + receive + {tcp, Socket, <<"OK">>} -> + ok = log(info, "Connected to ~s:~p", [Host, Port]), + Socket; + Other -> + Message = "Unexpected response from ~s:~p:~n~tp", + ok = log(warning, Message, [Host, Port, Other]), + ok = disconnect(Socket), + halt(0) + after 5000 -> + ok = log(warning, "Host ~s:~p timed out.", [Host, Port]), + halt(0) + end; +confirm_server(Socket, {auth, KeyID}) -> + ok = log(info, "Would now be trying to connect as AUTH using ~tp", [KeyID]), + {ok, {Addr, Port}} = inet:peername(Socket), + Host = inet:ntoa(Addr), + ok = gen_tcp:send(Socket, <<"OTPR AUTH 1">>), receive {tcp, Socket, <<"OK">>} -> ok = log(info, "Connected to ~s:~p", [Host, Port]), @@ -776,7 +811,7 @@ confirm_server(Socket) -> end. --spec disconnect(inet:socket()) -> ok. +-spec disconnect(gen_tcp:socket()) -> ok. %% @private %% Gracefully shut down a socket, logging (but sidestepping) the case when the socket %% has already been closed by the other side. @@ -791,47 +826,46 @@ disconnect(Socket) -> end. --spec ensure_keypair(KeyID) -> true | no_return() - when KeyID :: string(). +-spec ensure_keypair(key_id()) -> true | no_return(). %% @private %% Check if both the public and private key based on KeyID exists. -ensure_keypair(KeyID) -> +ensure_keypair(KeyID = {Realm, KeyName}) -> case {have_public_key(KeyID), have_private_key(KeyID)} of {true, true} -> true; {false, true} -> - ok = log(error, "Public key for ~tp cannot be found", [KeyID]), + Message = "Public key for ~tp/~tp cannot be found", + ok = log(error, Message, [Realm, KeyName]), halt(1); {true, false} -> - ok = log(error, "Private key for ~tp cannot be found", [KeyID]), + Message = "Private key for ~tp/~tp cannot be found", + ok = log(error, Message, [Realm, KeyName]), halt(1); {false, false} -> - Message = "Key pair for ~tp cannot be found", - ok = log(error, Message, [KeyID]), + Message = "Key pair for ~tp/~tp cannot be found", + ok = log(error, Message, [Realm, KeyName]), halt(1) end. --spec have_public_key(KeyID) -> boolean() - when KeyID :: string(). +-spec have_public_key(key_id()) -> boolean(). %% @private %% Determine whether the public key indicated by KeyID is in the keystore. -have_public_key(KeyID) -> - PublicKeyFile = KeyID ++ ".pub.der", - PublicKeyPath = filename:join([zomp_dir(), "key", PublicKeyFile]), +have_public_key({Realm, KeyName}) -> + PublicKeyFile = KeyName ++ ".pub.der", + PublicKeyPath = filename:join([zomp_dir(), "key", Realm, PublicKeyFile]), filelib:is_regular(PublicKeyPath). --spec have_private_key(KeyID) -> boolean() - when KeyID :: string(). +-spec have_private_key(key_id()) -> boolean(). %% @private %% Determine whether the private key indicated by KeyID is in the keystore. -have_private_key(KeyID) -> - PrivateKeyFile = KeyID ++ ".key.der", - PrivateKeyPath = filename:join([zomp_dir(), "key", PrivateKeyFile]), +have_private_key({Realm, KeyName}) -> + PrivateKeyFile = KeyName ++ ".key.der", + PrivateKeyPath = filename:join([zomp_dir(), "key", Realm, PrivateKeyFile]), filelib:is_regular(PrivateKeyPath). @@ -844,7 +878,7 @@ have_private_key(KeyID) -> %% the file. realm_data(Realm) -> - RealmFile = filename:join([zomp_dir(), Realm ++ ".realm"]), + RealmFile = filename:join(zomp_dir(), Realm ++ ".realm"), case file:consult(RealmFile) of {ok, Data} -> Data; @@ -860,75 +894,43 @@ realm_data(Realm) -> %%% Key generation --spec prompt_keygen(KeyDir) -> KeyPrefix - when KeyDir :: file:filename(), - KeyPrefix :: string(). +-spec prompt_keygen() -> key_id(). %% @private %% Prompt the user for a valid KeyPrefix to use for naming a new RSA keypair. -prompt_keygen(KeyDir) -> +prompt_keygen() -> Message = " Enter a name for your new keys.~n" " Valid names must start with a lower-case letter, and can include~n" " only lower-case letters, numbers, and periods, but no series of~n" " consecutive periods.~n", ok = io:format(Message), - Input = io:get_line("(^C to quit): "), - case validate_prefix(Input) of - {ok, Value} -> - Value; - error -> - ok = io:format("Bad name. Try again.~n"), - prompt_keygen(KeyDir) + Input = string:trim(io:get_line("(^C to quit): ")), + {Realm, KeyName} = + case string:lexemes(Input, " ") of + [R, K] -> {R, K}; + [K] -> {"otpr", K} + end, + case {valid_lower0_9(Realm), valid_label(KeyName)} of + {true, true} -> + {Realm, KeyName}; + {false, _} -> + ok = io:format("Bad realm name ~tp. Try again.~n", [Realm]), + prompt_keygen(); + {true, false} -> + ok = io:format("Bad key name ~tp. Try again.~n", [KeyName]), + prompt_keygen() end. --spec validate_prefix(Prefix) -> {ok, Validated} | error - when Prefix :: string(), - Validated :: string(). -%% @private -%% Validate that a provided key prefix is legal according to the naming convention -%% provided in the prefix explanation prompt. - -validate_prefix([Char | Rest]) - when $a =< Char, Char =< $z -> - validate_prefix(Rest, Char, [Char]); -validate_prefix(_) -> - error. - - --spec validate_prefix(Prefix, Last, Accumulator) -> {ok, Validated} | error - when Prefix :: string(), - Last :: char(), - Accumulator :: [char()], - Validated :: string(). - -%% @private -%% Validate that a provided key prefix is legal according to the naming convention -%% provided in the prefix explanation prompt. - -validate_prefix([$. | _], $., _) -> - error; -validate_prefix([Char | Rest], _, Acc) - when $a =< Char, Char =< $z; - $0 =< Char, Char =< $9; - Char == $. -> - validate_prefix(Rest, Char, [Char | Acc]); -validate_prefix([$\n], _, Acc) -> - {ok, lists:reverse(Acc)}; -validate_prefix(_, _, _) -> - error. - - -spec keygen() -> no_return(). %% @private %% Execute the key generation procedure for 16k RSA keys once and then terminate. keygen() -> ok = file:set_cwd(zomp_dir()), - KeyDir = filename:join(zomp_dir(), "key"), - Prefix = prompt_keygen(KeyDir), - case generate_rsa(Prefix) of + KeyID = prompt_keygen(), + case generate_rsa(KeyID) of ok -> halt(0); Error -> @@ -936,8 +938,8 @@ keygen() -> end. --spec generate_rsa(Prefix) -> Result - when Prefix :: string(), +-spec generate_rsa(KeyID) -> Result + when KeyID :: key_id(), Result :: {ok, KeyFile, PubFile} | {error, keygen_fail}, KeyFile :: file:filename(), @@ -947,11 +949,12 @@ keygen() -> %% filenames derived from Prefix. %% NOTE: The current version of this command is likely to only work on a unix system. -generate_rsa(Prefix) -> - ZompDir = zomp_dir(), - PemFile = filename:join([ZompDir, "key", Prefix ++ ".pub.pem"]), - KeyFile = filename:join([ZompDir, "key", Prefix ++ ".key.der"]), - PubFile = filename:join([ZompDir, "key", Prefix ++ ".pub.der"]), +generate_rsa({Realm, KeyName}) -> + KeyDir = filename:join([zomp_dir(), "key", Realm]), + ok = force_dir(KeyDir), + PemFile = filename:join(KeyDir, KeyName ++ ".pub.pem"), + KeyFile = filename:join(KeyDir, KeyName ++ ".key.der"), + PubFile = filename:join(KeyDir, KeyName ++ ".pub.der"), ok = lists:foreach(fun halt_if_exists/1, [PemFile, KeyFile, PubFile]), ok = log(info, "Generating ~p and ~p. Please be patient...", [KeyFile, PubFile]), ok = gen_p_key(KeyFile), @@ -1057,7 +1060,7 @@ check_key(KeyFile, PubFile) -> openssl() -> OpenSSL = case os:type() of - {unix, _} -> "openssl"; + {unix, _} -> "openssl"; {win32, _} -> "openssl.exe" end, ok = @@ -1072,22 +1075,26 @@ openssl() -> OpenSSL. --spec loadkey(Type, File) -> Result - when Type :: private | public, - File :: file:filename(), - Result :: {ok, DecodedKey :: term()} - | {error, Reason :: term()}. +-spec loadkey(Type, KeyID) -> Result + when Type :: private | public, + KeyID :: key_id(), + Result :: {ok, DecodedKey :: term()} + | {error, Reason :: term()}. %% @private %% Hide the details behind reading and loading DER encoded RSA key files. -loadkey(Type, File) -> - DerType = +loadkey(Type, {Realm, KeyName}) -> + {DerType, Path} = case Type of - private -> 'RSAPrivateKey'; - public -> 'RSAPublicKey' + private -> + P = filename:join([zomp_dir(), "key", Realm, KeyName ++ "key.der"]), + {'RSAPrivateKey', P}; + public -> + P = filename:join([zomp_dir(), "key", Realm, KeyName ++ "pub.der"]), + {'RSAPublicKey', P} end, - ok = log(info, "Loading key from file ~ts", [File]), - case file:read_file(File) of + ok = log(info, "Loading key from file ~ts", [Path]), + case file:read_file(Path) of {ok, Bin} -> {ok, public_key:der_decode(DerType, Bin)}; Error -> Error end. @@ -1166,28 +1173,27 @@ dialyze() -> %%% Network operations and package utilities --spec install(app_id()) -> ok. +-spec install(package_id()) -> ok. %% @private %% Install a package from the cache into the local system. -install(AppID) -> - AppString = appid_to_appstring(AppID), - ok = log(info, "Installing ~ts", [AppString]), - ZrpFile = filename:join("zrp", namify_zrp(AppID)), +install(PackageID) -> + PackageString = package_string(PackageID), + ok = log(info, "Installing ~ts", [PackageString]), + ZrpFile = filename:join("zrp", namify_zrp(PackageID)), Files = extract_zrp(ZrpFile), - TgzFile = namify_tgz(AppID), + TgzFile = namify_tgz(PackageID), {TgzFile, TgzData} = lists:keyfind(TgzFile, 1, Files), {"zomp.meta", MetaBin} = lists:keyfind("zomp.meta", 1, Files), Meta = binary_to_term(MetaBin), {sig, {KeyID, Signature}} = lists:keyfind(sig, 1, Meta), - KeyFile = filename:join("key", KeyID), - {ok, PubKey} = loadkey(public, KeyFile), - ok = ensure_app_dirs(AppID), - AppDir = filename:join("lib", AppString), - ok = force_dir(AppDir), + {ok, PubKey} = loadkey(public, KeyID), + ok = ensure_package_dirs(PackageID), + PackageDir = filename:join("lib", PackageString), + ok = force_dir(PackageDir), ok = verify(TgzData, Signature, PubKey), - ok = erl_tar:extract({binary, TgzData}, [compressed, {cwd, AppDir}]), - log(info, "~ts installed", [AppString]). + ok = erl_tar:extract({binary, TgzData}, [compressed, {cwd, PackageDir}]), + log(info, "~ts installed", [PackageString]). -spec extract_zrp(FileName) -> Files | no_return() @@ -1244,15 +1250,16 @@ verify(Data, Signature, PubKey) -> end. --spec fetch([app_id()]) -> ok. +-spec fetch(gen_tcp:socket(), [package_id()]) -> ok. %% @private %% Download a list of deps to the local package cache. -fetch(Needed) -> +fetch(Socket, Needed) -> Namified = lists:map(fun namify_zrp/1, Needed), {Have, Lack} = lists:partition(fun filelib:is_regular/1, Namified), ok = lists:foreach(fun(A) -> log(info, "Have ~ts", [A]) end, Have), ok = lists:foreach(fun(A) -> log(info, "Lack ~ts", [A]) end, Lack), + ok = send(Socket, "Would be sending fetch requests now."), log(info, "Done fake fetching"). @@ -1305,13 +1312,13 @@ write_terms(Filename, List) -> file:write_file(Filename, Text). --spec build(app_id()) -> ok. +-spec build(package_id()) -> ok. %% @private %% Given an AppID, build the project from source and add it to the current lib path. -build(AppID) -> +build(PackageID) -> {ok, CWD} = file:get_cwd(), - ok = file:set_cwd(app_home(AppID)), + ok = file:set_cwd(package_home(PackageID)), ok = build(), file:set_cwd(CWD). @@ -1338,15 +1345,15 @@ build() -> -spec scrub(Deps) -> Scrubbed - when Deps :: [app_id()], - Scrubbed :: [app_id()]. + 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(Deps) -> {ok, Names} = file:list_dir("lib"), - Existing = lists:map(fun appstring_to_appid/1, Names), + Existing = lists:map(fun package_id/1, Names), Need = ordsets:from_list(Deps), Have = ordsets:from_list(Existing), ordsets:to_list(ordsets:subtract(Need, Have)). @@ -1355,58 +1362,68 @@ scrub(Deps) -> %%% Input argument mangling --spec check_name(string()) -> string() | no_return(). -%% @private -%% Ensure that the name provided adheres to the rules: -%% Name must start with a character between and including `a' and `z' -%% Name can only include lowercase `a' through `z', numbers and underscores. -%% This function halts execution with an error message to STDOUT if an invalid string -%% is provided as an argument. -check_name([Char | Rest]) +-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 -> - case check_name(Rest, [Char]) of - {ok, Name} -> Name; - error -> error_exit("Bad name.", ?FILE, ?LINE) - end; -check_name(_) -> - error_exit("Bad name.", ?FILE, ?LINE). + valid_lower0_9(Rest, Char); +valid_lower0_9(_) -> + false. --spec check_name(string(), list()) -> Result - when Result :: {ok, AppName :: string()} - | error. +-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 -> + valid_lower0_9(Rest, Char); +valid_lower0_9([], _) -> + true; +valid_lower0_9(_, _) -> + false. + + +-spec valid_label(string()) -> boolean(). %% @private -%% Does the work of checking whether the argument to check_name/1 is valid. +%% Check whether a provided string is a valid label. -check_name([Char | Rest], Acc) +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 == $_ -> - check_name(Rest, [Char | Acc]); -check_name("", Acc) -> - App = lists:reverse(Acc), - {ok, App}; -check_name(_, _) -> - error. + Char == $_; Char == $-; + Char == $. -> + valid_label(Rest, Char); +valid_label([], _) -> + true; +valid_label(_, _) -> + false. --spec check_version(string()) -> version() | no_return(). -%% @private -%% Checks that a version string is a valid string of the form `X.Y.Z'. Halts execution -%% on bad input after printing a message to STDOUT. - -check_version(String) -> - case string_to_version(String) of - {true, Version} -> Version; - false -> error_exit("Bad version ~tp", [String], ?FILE, ?LINE) - end. - - --spec string_to_version(String) -> Result - when String :: string(), - Result :: {true, version()} - | false. +-spec string_to_version(string()) -> version(). %% @private %% @equiv string_to_version(string(), "", {z, z, z}) @@ -1418,18 +1435,13 @@ string_to_version(String) -> when String :: string(), Acc :: list(), Version :: version(), - Result :: {true, version()} - | false. + Result :: version(). %% @private %% Accepts a full or partial version string of the form `X.Y.Z', `X.Y' or `X' and -%% returns an internal representation of the version indicated as `{true, Version}' or -%% `false'. The reason for this return type is to allow it to work smoothly with -%% functions from the `lists' module. +%% 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([$.], _, _) -> - false; string_to_version([$. | Rest], Acc, {z, z, z}) -> X = list_to_integer(lists:reverse(Acc)), string_to_version(Rest, "", {X, z, z}); @@ -1437,18 +1449,16 @@ 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("", "", Version) -> - {true, Version}; + Version; string_to_version([], Acc, {z, z, z}) -> X = list_to_integer(lists:reverse(Acc)), - {true, {X, z, z}}; + {X, z, z}; string_to_version([], Acc, {X, z, z}) -> Y = list_to_integer(lists:reverse(Acc)), - {true, {X, Y, z}}; + {X, Y, z}; string_to_version([], Acc, {X, Y, z}) -> Z = list_to_integer(lists:reverse(Acc)), - {true, {X, Y, Z}}; -string_to_version(_, _, _) -> - false. + {X, Y, Z}. -spec version_to_string(version()) -> string(). @@ -1465,78 +1475,79 @@ version_to_string({X, Y, Z}) -> lists:flatten(lists:join($., [integer_to_list(Element) || Element <- [X, Y, Z]])). --spec appstring_to_appid(string()) -> app_id(). +-spec package_id(string()) -> package_id(). %% @private -%% Converts a proper appstring to an app_id(). +%% Converts a proper package_string to a package_id(). %% This function takes into account missing version elements. %% Examples: -%% `{"foo", "bar", {1, 2, 3}} = appstring_to_appid("foo-bar-1.2.3")' -%% `{"foo", "bar", {1, 2, z}} = appstring_to_appid("foo-bar-1.2")' -%% `{"foo", "bar", {1, z, z}} = appstring_to_appid("foo-bar-1")' -%% `{"foo", "bar", {z, z, z}} = appstring_to_appid("foo-bar")' +%% `{"foo", "bar", {1, 2, 3}} = package_id("foo-bar-1.2.3")' +%% `{"foo", "bar", {1, 2, z}} = package_id("foo-bar-1.2")' +%% `{"foo", "bar", {1, z, z}} = package_id("foo-bar-1")' +%% `{"foo", "bar", {z, z, z}} = package_id("foo-bar")' -appstring_to_appid(String) -> +package_id(String) -> case string:lexemes(String, [$-]) of - [Realm, App, VersionString] -> - Realm = check_name(Realm), - App = check_name(App), - Version = check_version(VersionString), - {Realm, App, Version}; - [Realm, App] -> - Realm = check_name(Realm), - App = check_name(App), - {Realm, App, {z, z, z}}; - [App] -> - App = check_name(App), - {"otpr", App, {z, z, z}} + [Realm, Name, VersionString] -> + true = valid_lower0_9(Realm), + true = valid_lower0_9(Name), + Version = string_to_version(VersionString), + {Realm, Name, Version}; + [Realm, Name] -> + true = valid_lower0_9(Realm), + true = valid_lower0_9(Name), + {Realm, Name, {z, z, z}}; + [Name] -> + true = valid_lower0_9(Name), + {"otpr", Name, {z, z, z}} end. --spec appid_to_appstring(app_id()) -> string(). +-spec package_string(package_id()) -> string(). %% @private -%% Map an AppID to a correct string representation. +%% Map an PackageID to a correct string representation. %% This function takes into account missing version elements. %% Examples: -%% `"foo-bar-1.2.3" = appid_to_appstring({"foo", "bar", {1, 2, 3}})' -%% `"foo-bar-1.2" = appid_to_appstring({"foo", "bar", {1, 2, z}})' -%% `"foo-bar-1" = appid_to_appstring({"foo", "bar", {1, z, z}})' -%% `"foo-bar" = appid_to_appstring({"foo", "bar", {z, z, z}})' +%% `"foo-bar-1.2.3" = package_string({"foo", "bar", {1, 2, 3}})' +%% `"foo-bar-1.2" = package_string({"foo", "bar", {1, 2, z}})' +%% `"foo-bar-1" = package_string({"foo", "bar", {1, z, z}})' +%% `"foo-bar" = package_string({"foo", "bar", {z, z, z}})' -appid_to_appstring({Realm, App, {z, z, z}}) -> - lists:flatten(lists:join($-, [Realm, App])); -appid_to_appstring({Realm, App, Version}) -> +package_string({Realm, Name, {z, z, z}}) -> + lists:flatten(lists:join($-, [Realm, Name])); +package_string({Realm, Name, Version}) -> VersionString = version_to_string(Version), - lists:flatten(lists:join($-, [Realm, App, VersionString])). + lists:flatten(lists:join($-, [Realm, Name, VersionString])). --spec namify_zrp(AppID) -> ZrpFileName - when AppID :: app_id(), +-spec namify_zrp(PackageID) -> ZrpFileName + when PackageID :: package_id(), ZrpFileName :: file:filename(). %% @private -%% Map an AppID to its correct .zrp package file name. +%% Map an PackageID to its correct .zrp package file name. -namify_zrp(AppID) -> namify(AppID, "zrp"). +namify_zrp(PackageID) -> namify(PackageID, "zrp"). --spec namify_tgz(AppID) -> TgzFileName - when AppID :: app_id(), +-spec namify_tgz(PackageID) -> TgzFileName + when PackageID :: package_id(), TgzFileName :: file:filename(). %% @private -%% Map an AppID to its correct gzipped tarball source bundle filename. +%% Map an PackageID to its correct gzipped tarball source bundle filename. -namify_tgz(AppID) -> namify(AppID, "tgz"). +namify_tgz(PackageID) -> namify(PackageID, "tgz"). --spec namify(AppID, Suffix) -> FileName - when AppID :: app_id(), - Suffix :: string(), - FileName :: file:filename(). +-spec namify(PackageID, Suffix) -> FileName + when PackageID :: package_id(), + Suffix :: string(), + FileName :: file:filename(). %% @private -%% Converts an AppID to a canonical string, then appends the provided filename Suffix. +%% Converts an PackageID to a canonical string, then appends the provided +%% filename Suffix. -namify(AppID, Suffix) -> - AppString = appid_to_appstring(AppID), - AppString ++ "." ++ Suffix. +namify(PackageID, Suffix) -> + PackageString = package_string(PackageID), + PackageString ++ "." ++ Suffix. @@ -1661,44 +1672,45 @@ zomp_dir() -> end. --spec ensure_app_dirs(app_id()) -> ok. +-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_app_dirs(AppID) -> - AppHome = app_home(AppID), - AppData = app_dir("var", AppID), - AppConf = app_dir("etc", AppID), - Dirs = [AppHome, AppData, AppConf], +ensure_package_dirs(PackageID) -> + PackageHome = package_home(PackageID), + PackageData = package_dir("var", PackageID), + PackageConf = package_dir("etc", PackageID), + Dirs = [PackageHome, PackageData, PackageConf], ok = lists:foreach(fun force_dir/1, Dirs), log(info, "Created dirs:~n\t~ts~n\t~ts~n\t~ts", Dirs). --spec app_home(AppID) -> AppHome - when AppID :: app_id(), - AppHome :: file:filename(). +-spec package_home(PackageID) -> PackageHome + when PackageID :: package_id(), + PackageHome :: file:filename(). %% @private -%% Accept an AppID and return the installation directory for the indicated application. +%% Accept an PackageID and return the installation directory for the indicated +%% application. %% NOTE: %% This system does NOT anticipate symlinks of incomplete versions to their latest %% installed version (for example, an incomplete `{1, 2, z}' resolving to a symlink %% `lib/foo-bar-1.2' which is always updated to point to the latest version 1.2.x). -app_home(AppID) -> - filename:join([zomp_dir(), "lib", appid_to_appstring(AppID)]). +package_home(PackageID) -> + filename:join([zomp_dir(), "lib", package_string(PackageID)]). --spec app_dir(Prefix, AppID) -> AppDataDir - when Prefix :: string(), - AppID :: app_id(), - AppDataDir :: file:filename(). +-spec package_dir(Prefix, PackageID) -> PackageDataDir + when Prefix :: string(), + PackageID :: package_id(), + PackageDataDir :: file:filename(). %% @private %% Create an absolute path to an application directory prefixed by the inclued argument. -app_dir(Prefix, {Realm, App, _}) -> - RealmApp = Realm ++ "-" ++ App, - filename:join([zomp_dir(), Prefix, RealmApp]). +package_dir(Prefix, {Realm, Name, _}) -> + PackageName = Realm ++ "-" ++ Name, + filename:join([zomp_dir(), Prefix, PackageName]). -spec force_dir(Path) -> Result @@ -1807,30 +1819,30 @@ usage() -> "zx~n" "~n" "Usage:~n" - " zx help~n" - " zx run AppID [Args]~n" - " zx init Type AppID~n" - " zx install Package~n" - " zx set dep AppID~n" - " zx set version Version~n" - " zx drop dep AppID~n" - " zx drop key KeyID~n" - " zx verup Level~n" - " zx runlocal~n" - " zx package [Path]~n" - " zx submit Package~n" - " zx keygen~n" - " zx genplt~n" + " zx help~n" + " zx run PackageID [Args]~n" + " zx init Type PackageID~n" + " zx install PackageID~n" + " zx set dep PackageID~n" + " zx set version Version~n" + " zx drop dep PackageID~n" + " zx drop key Realm KeyName~n" + " zx verup Level~n" + " zx runlocal~n" + " zx package [Path]~n" + " zx submit Path~n" + " zx keygen~n" + " zx genplt~n" "~n" "Where~n" - " AppID :: A string of the form Realm-App-Version~n" - " Args :: Arguments to pass to the application~n" - " Package :: A .zrp package path/filename~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" - " KeyID :: 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~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" + " 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 .zrp file~n" "~n", io:format(T).