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

时间:2016-04-01 14:34:29

标签: erlang rebar

问题

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

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

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

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

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

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

    迄今为止尝试的事情

    构建escript

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

    优点:

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

    缺点:

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

    未知

    • cli app应该成为正确的OTP应用程序;
    • 是否应该有监督树;
    • 是否应该开始;
    • 如果是,我如何在处理资产时停止它?

    构建版本

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

    优点:

    • 每个依赖项应用程序都会进入它们自己的目录,从而可以轻松地共享和打包它们。

    缺点:

    • 没有定义的输入点,例如escript' s main/1;
    • 因此必须手动处理命令行参数和退出代码。

    未知

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

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

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

2 个答案:

答案 0 :(得分:5)

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

  1. erts应用程序 - 特定版本的VM和内核
  2. Erlang OTP应用程序
  3. 您的应用程序'依赖
  4. CLI入口点
  5. 然后在任何一种情况下,CLI入口点都必须启动Erlang VM并执行它应该在给定情况下执行的代码。然后它将退出或继续运行 - 后者用于长时间运行的应用程序。

    CLI入口点可以是启动Erlang VM的任何内容,例如:一个escript脚本,shbash等。escript优于通用shell的明显优势是escript已经在一个上下文中执行Erlang VM,因此无需处理启动/停止VM。

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

    1. 使用系统范围的Erlang VM
    2. 使用embedded Erlang版本
    3. 在第一种情况下,您不会向您的软件包提供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+"$@"}
      

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

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

      escript示例

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

      嵌入式Erlang

      但是,如果应用程序是独立于系统范围的Erlang安装而安装的正确OTP版本,则同样适用。这是因为在这种情况下,脚本由属于该嵌入式Erlang版本的riak执行而不是系统版本(即使它已安装)。因此,它知道属于该版本的所有应用程序的位置(包括您的应用程序)。例如,erts就是这样 - 在他们的包中,它们提供了一个embedded Erlang release,其中包含自己的riak和所有依赖的Erlang应用程序。这样就可以在没有Erlang甚至安装在主机操作系统上的情况下启动riak。这是FreeBSD上% 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 / main

      除了必须显式调用启动Erlang VM时要执行的函数(入口点或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()\"" 文件),之后节点关闭:

      -boot

      这是一个类似的脚本,但不会启动任何特定的代码或应用程序。相反,它会启动正确的OTP版本,因此启动哪些应用程序以及依赖于版本的顺序(由#!/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 文件中,您可以根据需要提供应用程序的其他路径,例如:

      lib

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

      总结和其他注意事项

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

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

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

      3. 编辑 - 参考要求

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

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

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

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

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

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

          1. cmd发布。
          2. humbundee发布。
          3. builder.esh escript
          4. 第一个用于启动节点进行安装,然后将其关闭。第二个用于启动长期运行的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安装。

答案 1 :(得分:0)

然后从“传统”模块进入代码的小escript可能是一种解决方案。

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

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