用Erchi编写的命令行工具的惯用方法

Iho*_*nko 11 erlang rebar

问题

关于Erlang的大多数文章和书籍都可以找到专注于创建长期运行的类似服务器的应用程序,而不会覆盖命令行工具创建的过程.

我有一个包含3个应用程序的多应用程序rebar3项目:

  • myweb- cowboy基于Web的服务;
  • mycli- 准备资产的命令行工具myweb;
  • mylib-使用两个图书馆mywebmycli,取决于NIF.

由于构建我想得到这样的工件:

  1. 要为http请求提供服务的Web部件的可执行文件;
  2. 用于资产准备的可执行命令行工具;
  3. 上面使用的一组库.

要求

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

到目前为止尝试的事情

建立一个escript

我在野外看到的方法之一是创建一个自包含的escript文件.至少rebar并且relx这样做.所以我试一试.

优点:

  • 支持命令行参数;
  • 如果出现错误,则返回非零退出代码.

缺点:

  • 将所有依赖项嵌入到单个文件中,使其无法重用mylib;
  • 由于*.so文件嵌入到生成的escript文件中,因此无法在运行时加载,因此NIF不起作用(参见erlang rebar escriptize&nifs);
  • rebar3 escriptize不能很好地处理依赖关系(参见bug 1139).

未知数:

  • 应该cli应用程序成为适当的OTP应用程序;
  • 它应该有一个监督树;
  • 它应该开始吗?
  • 如果是这样,我怎样在资产处理后停止它?

建立一个版本

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

优点:

  • 每个依赖项应用程序都进入它们自己的目录,这样就可以轻松地共享和打包它们.

缺点:

  • 没有像escript那样定义的入口点main/1;
  • 因此,必须手动处理命令行参数和退出代码.

未知数:

  • 如何以非交互方式建模cli OTP应用程序;
  • 如何在处理资产时停止应用程序?

上述两种方法似乎都不适合我.

它将从两个世界中获得最佳:获取escript提供的基础结构,例如main/1入口点,命令行参数和退出代码处理,同时仍然具有易于打包且不妨碍使用的良好目录结构NIF.

Gre*_*reg 5

无论您是在Erlang中启动长期运行的守护程序类应用程序,还是CLI命令,您始终需要以下内容:

  1. erts 应用程序 - 特定版本中的VM和内核
  2. Erlang OTP应用程序
  3. 您的应用程序的依赖项
  4. CLI入口点

然后在任何一种情况下,CLI入口点都必须启动Erlang VM并执行它应该在给定情况下执行的代码.然后它将退出或继续运行 - 后者用于长时间运行的应用程序.

该CLI切入点可以是任何启动一个Erlang VM,例如一个escript脚本sh,bash等的明显优势escript超过通用的外壳是escript已经被在一个Erlang VM的上下文中执行,所以没有必要处理启动/停止VM.

您可以通过两种方式启动Erlang VM:

  1. 使用系统范围的Erlang VM
  2. 使用嵌入式Erlang版本

在第一种情况下,您不向erts程序包提供任何OTP应用程序,只能使特定的Erlang版本成为应用程序的依赖项.在第二种情况下,您将提供erts所有必需的OTP应用程序以及应用程序在程序包中的依赖项.

在第二种情况下,您还需要在启动VM时正确设置代码根.但这很容易,请参阅erlErlang用于启动系统范围VM 的脚本:

# 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+"$@"}
Run Code Online (Sandbox Code Playgroud)

这可以通过脚本来处理,例如node_packageBasho用于为所有主要操作系统打包其Riak数据库的工具.我正在使用我自己的构建工具来维护我自己的分支builderl.我只是说,所以你知道如果我设法定制它你也能够做到这一点:)

启动Erlang VM后,您的应用程序应该能够加载和启动任何应用程序,无论是随Erlang提供还是与您的应用程序一起提供(包括mylib您提到的库).以下是一些如何实现这一目标的示例:

escript

请参阅builderl.esh示例我如何处理从中加载其他Erlang应用程序builderl.该escript脚本假定Erlang安装是相对于执行它的文件夹.当它是另一个应用程序的一部分时,例如humbundee,load_builderl.hrl包含文件编译和加载bld_load,然后加载所有剩余的模块bld_load:boot/3.请注意我如何使用标准OTP应用程序而不指定它们的位置 - builderl正在执行escript,因此所有应用程序都从它们的安装位置(/usr/local/lib/erlang/lib/在我的系统上)加载.如果您的应用程序使用的库(例如mylib,安装在其他位置),您需要做的就是将该位置添加到Erlang路径,例如使用code:add_path.Erlang将自动从添加到代码路径列表的文件夹中加载代码中使用的模块.

嵌入式Erlang

但是,如果应用程序是独立于系统范围的Erlang安装而安装的正确OTP版本,则同样适用.那是因为在这种情况下,脚本是通过escript属于嵌入式Erlang版本而不是系统版本(即使它已安装)来执行的.因此,它知道属于该版本的所有应用程序的位置(包括您的应用程序).例如riak确实如此 - 在他们的包中,他们提供了一个嵌入式Erlang版本,其中包含自己的erts和所有依赖的Erlang应用程序.这种方式riak可以在没有Erlang甚至安装在主机操作系统上的情况下启动.这是riakFreeBSD上的一个软件包的摘录:

% 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
(...)
Run Code Online (Sandbox Code Playgroud)

sh/bash

除了必须在启动Erlang VM(入口点或您调用的函数)时显式调用要执行的函数时,原则上与上面没有多大区别main.

考虑这个builderl生成的脚本,只是为了执行指定的任务(生成RELEASES文件)而启动Erlang应用程序,之后节点关闭:

#!/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()\""
Run Code Online (Sandbox Code Playgroud)

这是一个类似的脚本,但不会启动任何特定的代码或应用程序.相反,它会启动正确的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"
Run Code Online (Sandbox Code Playgroud)

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

-pa lib/humbundee/ebin lib/yolf/ebin deps/goldrush/ebin deps/lager/ebin deps/yajler/ebin
Run Code Online (Sandbox Code Playgroud)

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

总结和其他考虑因素

Erlang应用程序的部署与脚本语言编写的其他项目没有太大差别,例如ruby或python项目.所有这些项目都必须处理类似的问题,我相信每个操作系统的包管理都会以这样或那样的方式处理它们:

  1. 了解您的操作系统如何处理具有运行时依赖性的打包项目.

  2. 了解如何为您的操作系统打包其他Erlang应用程序,其中有很多通常由所有主要系统分发:RabbitMQ,Ejabberd,Riak等.只需下载软件包并将其解压缩到一个文件夹,然后您就会看到所有文件的放置位置.

编辑 - 参考要求

回到您的要求,您有以下选择:

  1. 在系统范围内安装Erlang作为OTP版本,作为嵌入式Erlang安装,或者在一些随机文件夹中安装应用程序(对不起Rebar)

  2. 您可以从安装版本中执行选定应用程序的形式shescript脚本中有多个入口点.只要您正确配置代码根目录和路径(如上所述),两者都可以正常工作.

然后,每个应用程序的:mywebmycli,就需要在自己的新背景下执行,例如启动一个新的虚拟机实例,并执行所需的应用程序(来自同一二郎释放).如果myweb入口点可以是sh根据版本启动新节点的脚本(类似于Riak).如果mycli入口点可以是a escript,则在任务完成后完成执行.

但是完全有可能创建一个短期运行的任务,即使它从一开始就退出VM sh- 参见上面的例子.在这种情况下mycli,需要单独的发布文件 - scriptboot启动VM.当然,也可以从中启动长期运行的Erlang VM escript.

我提供了一个示例项目,它同时使用了所有这些方法,humbundee.编译后,它提供了三个访问点:

  1. cmd版本.
  2. humbundee版本.
  3. builder.esh escript.

第一个用于启动节点进行安装,然后将其关闭.第二个用于启动长期运行的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
Run Code Online (Sandbox Code Playgroud)

cmd入口点会使用应用程序deploy-0.0.1builderl-0.2.7以及发布文件cmd.boot,cmd.script以及一些OTP应用.标准humbundee入口点将使用除builderl和之外的所有应用程序deploy.然后builderl.eshescript将使用应用程序deploy-0.0.1builderl-0.2.7.全部来自同一嵌入式Erlang OTP安装.