运送用 Erlang 编写的命令行工具的惯用方法

Idiomatic way to ship command line tools written in Erlang

问题

我能找到的大多数关于 Erlang 的文章和书籍都专注于创建长 运行 类服务器应用程序,而没有涵盖命令行工具的创建过程。

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

作为构建的结果,我想获得这样的工件:

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

要求

到目前为止尝试过的东西

建立 escript

我在野外看到的一种方法是创建一个独立的 escript 文件。至少 rebar3relx 这样做。所以我试了一下。

优点:

缺点:

未知:

构建版本

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

优点:

缺点:

未知:


以上两种方法似乎都不适合我。

最好能两全其美:获得 escript 提供的基础设施,例如 main/1 入口点、命令行参数和退出代码处理,同时仍然拥有一个很好的目录结构,易于打包并且不妨碍使用 NIF。

然后从 'conventional' 模块进入代码的小脚本可能是一个解决方案。

例如,Concuerror is expected to be used as a command line tool and uses an escript as its entry point. It handles command-line arguments via getopt。所有主要代码都在常规的 Erlang 模块中,这些模块包含在带有简单参数的 escript 路径中。

据我了解,然后可以使用常规 -onload 属性加载 NIF(Concuerror 不使用 NIF)。

无论您是在 Erlang 中启动一个 long-运行ning 类似守护进程的应用程序,还是一个 CLI 命令,您始终需要以下内容:

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

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

CLI 入口点可以是启动 Erlang VM 的任何东西,例如escript 脚本、shbash 等。escript 相对于通用 shell 的明显优势是 escript 已经在执行Erlang 虚拟机的上下文,因此无需处理 starting/stopping 虚拟机。

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

  1. 使用系统范围的 Erlang VM
  2. 使用 embedded Erlang 版本

在第一种情况下,您既不提供 erts 也不随包提供任何 OTP 应用程序,您只需将特定的 Erlang 版本作为您的应用程序的依赖项。在第二种情况下,您提供 erts 和所有必需的 OTP 应用程序以及您的程序包中的应用程序依赖项。

在第二种情况下,您还需要在启动 VM 时正确设置 code root。但这很容易,请参阅 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 [=10=] | sed 's/.*\///'`
export EMU
export ROOTDIR
export BINDIR
export PROGNAME
exec "$BINDIR/erlexec" ${1+"$@"}

这可以通过脚本来处理,例如 node_package tool that Basho uses to package their Riak database for all major operating systems. I am maintaining my own fork of it which I am using with my own build tool called builderl。我只是这么说,所以你知道如果我设法定制它,你也能做到:)

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

escript 例子

参见 this builderl.esh example how I handle loading other Erlang applications from builderl. That escript script assumes that the Erlang installation is relative to the folder from which it's executed. When it's a part of another application, like for example humbundee, the load_builderl.hrl include file compiles and loads bld_load, which in turn loads all remaining modules with bld_load:boot/3. Notice how I can use standard OTP applications without specifying where they are - builderl is being executed by escript and so all the applications are loaded from where they were installed (/usr/local/lib/erlang/lib/ on my system). If libraries used by your application, e.g. mylib, are installed somewhere else, all you need to do is add that location to the Erlang path, e.g. with 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
(...)

sh/bash

除了在启动 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 版本中,应用程序位于 code root/lib 文件夹中,Erlang 能够加载这些应用程序而无需指定任何其他路径。

总结及其他注意事项

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

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

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

编辑 - 参考要求

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

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

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

那么您的每个应用程序:mywebmycli 都需要在其自己的新上下文中执行,例如启动一个新的 VM 实例并执行所需的应用程序(来自相同的 Erlang 版本)。在 myweb 的情况下,入口点可以是 sh 脚本,根据版本启动一个新节点(类似于 Riak)。在 mycli 的情况下,入口点可以是 escript,一旦任务完成,它就会结束执行。

但是完全有可能创建一个 运行ning 任务来退出 VM,即使它是从 sh 启动的 - 请参见上面的示例。在这种情况下,mycli 将需要单独的发布文件 - scriptboot 来启动 VM。当然也可以从 escript.

启动一个 long-运行ning Erlang VM

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

  1. cmd 发布。
  2. humbundee 发布。
  3. builder.esh escript.

第一个用于启动节点进行安装,然后将其关闭。第二个用于启动一个 long-运行ning Erlang 应用程序。第三个是 install/configure 节点的构建工具。这是创建发布后项目的样子:

$:~/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 和一些 OTP 应用程序。标准 humbundee 入口点将使用除 builderldeploy 之外的所有应用程序。然后 builderl.esh 脚本将使用应用程序 deploy-0.0.1builderl-0.2.7。全部来自相同的嵌入式 Erlang OTP 安装。