提供用Erlang编写的命令行工具的惯用方法

voase2hg  于 2022-12-16  发布在  Erlang
关注(0)|答案(2)|浏览(136)

问题是

我发现大多数关于Erlang的文章和书籍都集中在创建长时间运行的类似服务器的应用程序上,而没有涉及命令行工具的创建过程。
我有一个包含3个应用程序的多应用程序rebar3项目:

  • myweb-基于Cowboy的网络服务;
  • mycli-一个命令行工具,用于为myweb准备资产;
  • mylib-mywebmycli使用的库,依赖于NIF。

作为构建的结果,我希望得到这样的工件:
1.将要服务HTTP请求的Web部件的可执行文件;
1.用于资产准备的可执行命令行工具;
1.一组由上述使用的库。

要求

  • CLI应该像一个正常的非交互式命令行工具一样工作:处理参数、处理stdin/stdout、出错时返回非零退出代码等;
  • 服务器和CLI都应该能够使用NIF;
  • 将工件打包为一组deb/rpm包应该很容易,因此服务器和CLI都应该重用公共依赖项。

事情尝试至今

构建一个escript

我见过的一种方法是创建一个自包含的escript文件,至少rebar3relx是这样做的,所以我尝试了一下。

优点:

  • 支持命令行参数;
  • 如果出现错误,它将返回非零退出代码。
    缺点:
  • 将所有依赖项嵌入到单个文件中,使得不可能重用mylib;
  • 由于*.so文件被嵌入到生成的escript文件中,因此它们不能在运行时加载,因此NIF不起作用(参见erlang rebar escriptize & nifs);
  • rebar3 escriptize不能很好地处理依赖关系(请参见bug 1139)。
    未知:
  • CLI应用程序是否应成为正确的OTP应用程序?
  • 它是否应该有一个监督树?
  • 到底该不该开始?
  • 如果是,如何在处理完资产后停止它?

构建发布版本

Fred Hebert的How I start: Erlang文章中描述了另一种构建命令行工具的方法。

优点:

  • 每个依赖项应用程序都进入自己的目录,从而可以轻松地共享和打包它们。
    缺点:
  • 不存在像escriptmain/1那样的已定义入口点;
  • 因此,命令行参数和退出代码都必须手动处理。
    未知:
  • 如何以非交互方式对CLI OTP应用程序进行建模?
  • 如何在处理完资产后停止应用程序?

以上两种方法似乎都不适合我。
如果能两全其美就好了:获得escript提供的基础设施,如main/1入口点、命令行参数和退出代码处理,同时仍然具有易于打包且不妨碍使用NIF的良好目录结构。

cmssoen2

cmssoen21#

无论您是在Erlang中还是在CLI命令中启动一个长时间运行的类似守护进程的应用程序,您总是需要以下内容:

  1. erts应用程序-特定版本中的虚拟机和内核
  2. Erlang OTP应用程序
    1.应用程序的依赖项
  3. CLI入口点
    然后,在任何一种情况下,CLI入口点都必须启动Erlang VM并执行它在给定情况下应该执行的代码,然后它将退出或继续运行-对于长时间运行的应用程序,后者是后者。
    CLI入口点可以是启动Erlang VM的任何内容,例如escript脚本、shbash等。escript相对于通用shell的明显优势在于,escript已经在Erlang VM的上下文中执行,因此无需处理VM的启动/停止。
    您可以通过两种方式启动Erlang VM:
    1.使用系统范围的Erlang VM
    1.使用embedded Erlang版本
    在第一种情况下,您不提供erts,也不提供任何OTP应用程序,您只需要为您的应用程序创建一个特定的Erlang版本依赖项;在第二种情况下,您需要在您的软件包中提供erts和所有必需的OTP应用程序沿着应用程序的依赖项。
    在第二种情况下,您还需要在启动VM时正确设置代码根。但这很容易,请参阅Erlang用于启动系统范围VM的erl脚本:
# location: /usr/local/lib/erlang/bin/erl
ROOTDIR="/usr/local/lib/erlang"
BINDIR=$ROOTDIR/erts-7.2.1/bin
EMU=beam
PROGNAME=`echo $0 | sed 's/.*\///'`
export EMU
export ROOTDIR
export BINDIR
export PROGNAME
exec "$BINDIR/erlexec" ${1+"$@"}

这可以通过脚本来处理,例如Basho用来为所有主要操作系统打包Riak数据库的node_package工具。我正在维护它的my own fork,我正在使用我自己的构建工具builderl。我这么说只是为了让你知道,如果我设法定制它,你也将能够做到这一点:)
Erlang VM启动后,您的应用程序应该能够加载和启动任何应用程序,无论是Erlang提供的还是您的应用程序(包括您提到的mylib库)。

escript示例

请参阅this builderl.esh example我如何处理从builderl加载其他Erlang应用程序。escript脚本假定Erlang安装相对于执行它的文件夹。当它是另一个应用程序的一部分时,例如humbundeeload_builderl.hrl包含文件编译并加载bld_load。这反过来又会使用bld_load:boot/3加载所有剩余模块。请注意,我可以使用标准OTP应用程序而无需指定它们的位置-builderlescript执行,因此所有应用程序都从它们的安装位置加载(/usr/local/lib/erlang/lib/ on my system)。如果您的应用程序使用的库(例如mylib)安装在其他位置,您只需将该位置添加到Erlang路径(例如code:add_path)。Erlang将自动从添加到代码路径列表的文件夹中加载代码中使用的模块。

嵌入式Erlang语言

但是,如果应用程序是独立于系统范围的Erlang安装而安装的正确的OTP版本,则情况也是如此,因为在这种情况下,脚本是由属于该嵌入式Erlang版本而不是系统范围版本的escript执行的(即使它已经安装)。因此它知道属于该版本的所有应用程序的位置例如riak就是这样做的--在他们的软件包中他们提供了一个embedded Erlang release,它包含了自己的erts和所有依赖的Erlang应用程序。这样riak就可以在主机操作系统上没有安装Erlang的情况下启动。这是FreeBSD上一个riak软件包的摘录:

% tar -tf riak2-2.1.1_1.txz
/usr/local/sbin/riak
/usr/local/lib/riak/releases/start_erl.data
/usr/local/lib/riak/releases/2.1.0/riak.rel
/usr/local/lib/riak/releases/RELEASES
/usr/local/lib/riak/erts-5.10.3/bin/erl
/usr/local/lib/riak/erts-5.10.3/bin/beam
/usr/local/lib/riak/erts-5.10.3/bin/erlc
/usr/local/lib/riak/lib/stdlib-1.19.3/ebin/re.beam
/usr/local/lib/riak/lib/ssl-5.3.1/ebin/tls_v1.beam
/usr/local/lib/riak/lib/crypto-3.1/ebin/crypto.beam
/usr/local/lib/riak/lib/inets-5.9.6/ebin/inets.beam
/usr/local/lib/riak/lib/bitcask-1.7.0/ebin/bitcask.app
/usr/local/lib/riak/lib/bitcask-1.7.0/ebin/bitcask.beam
(...)

一米三十至一米/一米三十一至一米

除了必须显式调用您在启动Erlang VM时要执行的函数(入口点或您所调用的main函数)之外,这在原则上与上面的方法没有太大区别。
考虑builderl为启动Erlang应用程序而生成的脚本,该脚本只是为了执行指定的任务(生成RELEASES文件),之后节点关闭:

#!/bin/sh
START_ERL=`cat releases/start_erl.data`
APP_VSN=${START_ERL#* }
run_erl -daemon ../hbd/shell/ ../hbd/log "exec erl ../hbd releases releases/start_erl.data -config releases/$APP_VSN/hbd.config -args_file ../hbd/etc/vm.args -boot releases/$APP_VSN/humbundee -noshell -noinput -eval \"{ok, Cwd} = file:get_cwd(), release_handler:create_RELEASES(Cwd, \\\"releases\\\", \\\"releases/$APP_VSN/humbundee.rel\\\", []), init:stop()\""

这是一个类似的脚本,但不启动任何特定的代码或应用程序,而是启动一个正确的OTP版本,因此启动哪些应用程序以及启动顺序取决于版本(由-boot选项指定)。

#!/bin/sh
START_ERL=`cat releases/start_erl.data`
APP_VSN=${START_ERL#* }
run_erl -daemon ../hbd/shell/ ../hbd/log "exec erl ../hbd releases releases/start_erl.data -config releases/$APP_VSN/hbd.config -args_file ../hbd/etc/vm.args -boot releases/$APP_VSN/humbundee"

vm.args文件中,您可以根据需要提供应用程序的其他路径,例如:

-pa lib/humbundee/ebin lib/yolf/ebin deps/goldrush/ebin deps/lager/ebin deps/yajler/ebin

在此示例中,这些路径是相对的,但如果应用程序安装在标准的已知位置,则可以是绝对的。此外,只有在使用系统范围的Erlang安装并需要添加其他路径来定位Erlang应用程序时,或者Erlang应用程序位于非标准位置时,才需要执行此操作(例如,不在lib文件夹中,因为Erlang OTP需要)。在适当的嵌入式Erlang版本中,应用程序位于代码根/lib文件夹中,Erlang能够加载这些应用程序,而无需指定任何附加路径。

总结和其他考虑

Erlang应用程序的部署与其他用脚本语言编写的项目(如ruby或python项目)没有太大区别,所有这些项目都必须处理类似的问题,我相信每个操作系统的包管理都以这样或那样的方式处理它们:
1.了解操作系统如何处理具有运行时依赖项的打包项目。
1.看看其他Erlang应用程序是如何为您的操作系统打包的,有很多Erlang应用程序通常由所有主要系统分发:RabbitMQ,Ejabberd,Riak等等。只需下载包并将其解包到一个文件夹中,然后你就会看到所有文件的位置。

编辑-参考要求

回到您的要求,您有以下选择:
1.将Erlang安装为系统范围内的OTP版本、嵌入式Erlang或随机文件夹中的应用程序包(对不起,Rebar)
1.您可以使用shescript脚本形式的多个入口点来执行已安装版本中的选定应用程序。只要您正确配置了这些应用程序的代码根和路径(如上所述),这两个入口点都可以工作。
然后,您的每个应用程序:X1 M41 N1 X和X1 M42 N1 X,将需要在其自己的新上下文中执行,例如,启动新的虚拟机示例并执行所需的应用程序(来自相同的Erlang发行版)。对于myweb,入口点可以是sh脚本,该脚本根据发行版启动新节点在mycli的情况下,入口点可以是escript,它在任务完成时结束执行。
但是,即使虚拟机是从sh启动的,也完全可以创建一个退出虚拟机的短期运行任务-参见上面的示例。在这种情况下,mycli将需要单独的发布文件-scriptboot来 Boot 虚拟机。当然,也可以从escript启动一个长期运行的Erlang虚拟机。
我提供了一个同时使用所有这些方法的示例项目humbundee,编译后它提供了三个访问点:

  1. cmd发行版。
  2. humbundee发行版。
    1.一米五十四奈一米五十五奈一米。
    第一个用于启动节点进行安装,然后关闭它;第二个用于启动一个长时间运行的Erlang应用程序;第三个是一个构建工具,用于安装/配置节点。下面是创建发布版本后的项目外观:
$:~/work/humbundee/tmp/rel % ls | tr " " "\n"
bin
erts-7.3
etc
lib
releases

$:~/work/humbundee/tmp/rel % ls bin | tr " " "\n"   
builderl.esh
cmd.boot
humbundee.boot
epmd
erl
escript
run_erl
to_erl
(...)

$:~/work/humbundee/tmp/rel % ls lib | tr " " "\n"
builderl-0.2.7
compiler-6.0.3
deploy-0.0.1
goldrush-0.1.7
humbundee-0.0.1
kernel-4.2
lager-3.0.1
mnesia-4.13.3
sasl-2.7
stdlib-2.8
syntax_tools-1.7
yajler-0.0.1
yolf-0.1.1

$:~/work/humbundee/tmp/rel % ls releases/hbd-0.0.1 | tr " " "\n"
builderl.config
cmd.boot
cmd.rel
cmd.script
humbundee.boot
humbundee.rel
humbundee.script
sys.config.src

cmd入口点将使用应用程序deploy-0.0.1builderl-0.2.7以及发布文件cmd.bootcmd.script标准的humbundee入口点将使用除builderldeploy之外的所有应用程序。然后,builderl.esh脚本将使用应用程序deploy-0.0.1builderl-0.2.7。所有这些应用程序都来自同一个嵌入式Erlang OTP安装。

q0qdq0h2

q0qdq0h22#

一个小的脚本,然后进入代码从'常规'模块可能是一个解决方案。
例如,Concuerror预期用作命令行工具,并使用escript作为其入口点。它通过getopt处理命令行参数。所有主要代码都在常规Erlang模块中,这些模块包含在escript的简单参数路径中。
据我所知,可以使用常规的-onload属性加载NIF(Concuerror不使用NIF)。

相关问题