【问题标题】:Idiomatic way to ship command line tools written in Erlang交付用 Erlang 编写的命令行工具的惯用方式
【发布时间】:2016-07-21 10:05:12
【问题描述】:

问题

我发现大部分关于 Erlang 的文章和书籍都集中在创建长时间运行的类似服务器的应用程序上,而没有涉及到命令行工具的创建过程。

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

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

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

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

要求

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

到目前为止尝试过的事情

构建一个脚本

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

优点:

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

缺点:

  • 将所有依赖项嵌入到单个文件中,从而无法重用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 的使用。

【问题讨论】:

    标签: erlang rebar


    【解决方案1】:

    一个小脚本,然后从“常规”模块进入代码可能是一个解决方案。

    例如,Concuerror 预计将用作命令行工具,并使用 escript 作为其入口点。它通过getopt 处理命令行参数。所有主要代码都在常规 Erlang 模块中,这些模块包含在路径中,并带有简单的 escript 参数。

    据我所知,NIF 可以使用常规的-onload 属性加载(Concuerror 不使用 NIF)。

    【讨论】:

      【解决方案2】:

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

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

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

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

      您可以通过两种方式启动 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 $0 | sed 's/.*\///'`
      export EMU
      export ROOTDIR
      export BINDIR
      export PROGNAME
      exec "$BINDIR/erlexec" ${1+"$@"}
      

      这可以通过脚本来处理,例如 node_package 工具,Basho 使用它来为所有主要操作系统打包他们的 Riak 数据库。我正在维护其中的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 应用程序 - builderl 正在由 escript 执行,因此所有应用程序都从它们的安装位置加载(/usr/local/lib/erlang/lib/ 在我的系统上)。如果您的应用程序使用了库,例如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
      (...)
      

      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,一旦任务完成就完成执行。

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

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

      1. cmd 发布。
      2. humbundee 发布。
      3. builder.eshescript.

      第一个用于启动节点进行安装,然后将其关闭。第二个用于启动长时间运行的 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 和一些OTP 应用程序。标准的humbundee 入口点将使用除builderldeploy 之外的所有应用程序。然后builderl.esh escript 将使用应用程序deploy-0.0.1builderl-0.2.7。全部来自同一个嵌入式 Erlang OTP 安装。

      【讨论】:

      • 感谢您的详细解答。据我了解,您建议采用一种混合方法:构建一个版本,以便每个应用程序进入自己的目录,然后使用一个小的 escript 作为入口点。我做对了吗?
      • 不是真的,我概述了我已知的所有可能的实现你想要的方法 :-) 你可以有一个适当的 OTP 版本,一个由 Rebar 创建的混合应用程序包或介于两者之间的东西。在每种情况下,您都可以通过正确设置代码根目录和所有应用程序的路径来启动 erl 来实现您想要的。我概述了如何使用escriptsh 来做到这一点。 OTP 版本(嵌入式 Erlang)是最简单的,因为它的默认设置需要最少的配置。但它也可以按照我概述的方式与escriptsh 一起使用。我希望这是有道理的?
      • 是的,这是有道理的,谢谢。虽然我仍然不确定惯用的方式是什么。有吗?
      • 惯用的方式取决于操作系统,但一般遵循Erlang本身的安装方式。考虑一下 Erlang 是 VM (erts) + Erlang 应用程序。和你的应用完全一样,Erlang VM + Erlang 应用。 Erlang 本身是一个安装在系统范围内的标准 OTP 版本(路径中包含 Erlang 可执行文件)。 Riak 紧随其后,我敢打赌其他 Erlang 应用程序也以类似的方式安装。 Riak 然后使用sh 脚本启动节点,但如果它更适合您,您可以使用escript(即我建议的)。
      • 刚刚添加了对答案的编辑。也许重新措辞会帮助您更好地理解它。
      猜你喜欢
      • 1970-01-01
      • 2017-10-26
      • 1970-01-01
      • 1970-01-01
      • 2015-03-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多