Forma idiomática de enviar herramientas de línea de comandos escritas en Erlang
rebar (2)
El problema
La mayoría de los artículos y libros sobre Erlang I podrían enfocarse en la creación de aplicaciones de servidor de larga ejecución, dejando el proceso de creación de herramientas de línea de comandos sin cobertura.
Tengo un proyecto multi-app de rebar3 que consta de 3 aplicaciones:
-
myweb
- un servicio web basado encowboy
; -
mycli
- una herramienta de línea de comandos para preparar activos paramyweb
; -
mylib
: una biblioteca utilizada pormyweb
ymycli
, depende de un NIF.
Como resultado de la construcción, quiero obtener tales artefactos:
- un ejecutable para el elemento web que va a servir solicitudes HTTP;
- una herramienta de línea de comandos ejecutable para la preparación de los activos;
- un conjunto de bibliotecas usadas por el anterior.
Requisitos
- cli debe comportarse como una herramienta de línea de comandos sana no interactiva: manejar argumentos, lidiar con stdin / stdout, devolver código de salida distinto de cero en caso de error, etc .;
- tanto el servidor como cli deberían poder usar NIF;
- debería ser fácil empaquetar los artefactos como un conjunto de paquetes deb / rpm, por lo que tanto el servidor como el cli deberían reutilizar dependencias comunes.
Las cosas intentaron hasta ahora
construyendo un escript
Una de las formas que he visto en la vida salvaje es crear un archivo escript autónomo. Al menos relx
rebar
y relx
lo hacen. Así que lo probé.
Pros:
- tiene soporte para argumentos de línea de comando;
- en caso de errores devuelve un código de salida distinto de cero.
Contras:
- incorpora todas las dependencias en un solo archivo, lo que hace que sea imposible reutilizar
mylib
; - dado que los archivos
*.so
se incrustan en el archivo escript resultante, no se pueden cargar en tiempo de ejecución, por lo tanto, los NIF no funcionan (consulte la versión de erlang rebar escriptize & nifs ); -
rebar3 escriptize
no maneja bien las dependencias (ver error 1139 ).
Desconocidos:
- si la aplicación cli se convierte en una aplicación OTP adecuada;
- debería tener un árbol de supervisión;
- debería ser comenzado en absoluto;
- de ser así, ¿cómo lo detengo cuando los activos han sido procesados?
construyendo un lanzamiento
Otra forma de construir una herramienta de línea de comando se describió en un Cómo empiezo: artículo de Erlang de Fred Hebert.
Pros:
- cada una de las aplicaciones de dependencia entra en su propio directorio, lo que facilita compartirlas y empaquetarlas.
Contras:
- no hay un punto de entrada definido como
main/1
escript; - como consecuencia, tanto los argumentos de línea de comando como el código de salida deben manejarse manualmente.
Desconocidos:
- cómo modelar la aplicación cli OTP de una manera no interactiva;
- cómo detener la aplicación cuando los activos han sido procesados?
Ninguno de los enfoques anteriores parece funcionar para mí.
Sería obtener lo mejor de ambos mundos: obtener la infraestructura que es proporcionada por escript como main/1
punto de entrada, parámetros de línea de comando y manejo de código de salida mientras que todavía tiene una buena estructura de directorio que es fácil de empaquetar y que no obstaculizar el uso de NIF.
Un pequeño escript que luego entra en el código de los módulos ''convencionales'' podría ser una solución.
Como ejemplo, se espera que Concuerror se use como una herramienta de línea de comando y use un escript como su punto de entrada. Maneja argumentos de línea de comandos a través de getopt . Todo el código principal está en módulos regulares de Erlang, que están incluidos en la ruta con argumentos simples para el escript.
Por lo que entiendo, los NIF pueden cargarse con atributos regulares de -onload
(Concuerror no usa NIF).
Independientemente de si está iniciando una aplicación similar a un daemon de larga ejecución en Erlang, o un comando CLI, siempre necesita lo siguiente:
- aplicación
erts
- la máquina virtual y el núcleo en una versión particular - Aplicaciones Erlang OTP
- Las dependencias de sus aplicaciones
- Punto de entrada CLI
Luego, en cualquier caso, el punto de entrada CLI debe iniciar la máquina virtual Erlang y ejecutar el código que se supone que debe ejecutarse en una situación dada. Luego, saldrá o continuará ejecutándose; el último para una aplicación de larga ejecución.
El punto de entrada CLI puede ser cualquier cosa que inicie una VM Erlang, por ejemplo, un script de escript
, sh
, bash
, etc. La ventaja obvia de escript
sobre el shell genérico es que escript
ya se está ejecutando en el contexto de una VM Erlang, por lo que no es necesario para manejar el inicio / parada de la máquina virtual.
Puede iniciar Erlang VM de dos maneras:
- Usar VM de Erlang en todo el sistema
- Use una versión incrustada de Erlang
En el primer caso, usted no proporciona erts
ni ninguna aplicación OTP con su paquete, solo hace que una versión particular de Erlang sea una dependencia para su aplicación. En el segundo caso, usted suministra erts
y todas las aplicaciones OTP requeridas junto con las dependencias de su aplicación en su paquete.
En el segundo caso, también debe manejar la configuración correcta de la raíz del código al iniciar la máquina virtual. Pero esto es bastante fácil, consulte la erl
comandos erl
que Erlang usa para iniciar la máquina virtual en todo el sistema:
# 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+"$@"}
Esto puede ser manejado por scripts, por ejemplo, la herramienta node_package
que Basho usa para empaquetar su base de datos Riak para todos los sistemas operativos principales. Estoy manteniendo mi propio tenedor que estoy usando con mi propia herramienta de construcción llamada builderl
. Solo digo eso para que sepas que si lograra personalizarlo, también podrás hacer eso :)
Una vez que se inicia la máquina virtual de Erlang, su aplicación debería poder cargar e iniciar cualquier aplicación, ya sea que se suministre con Erlang o con su aplicación (y eso incluye la biblioteca de mylib
que mencionó). Aquí hay algunos ejemplos de cómo se podría lograr esto:
ejemplo escript
Vea este ejemplo de builderl.esh
cómo manejo la carga de otras aplicaciones de Erlang desde builderl
. Esa escript
comandos escript
asume que la instalación de Erlang es relativa a la carpeta desde la que se ejecuta. Cuando es parte de otra aplicación, como por ejemplo humbundee
, el archivo de carga bld_load
compila y carga bld_load
, que a su vez carga todos los módulos restantes con bld_load:boot/3
. Observe cómo puedo usar aplicaciones OTP estándar sin especificar dónde están: builderl
es ejecutado por escript
y todas las aplicaciones se cargan desde donde se instalaron ( /usr/local/lib/erlang/lib/
en mi sistema). Si las bibliotecas utilizadas por su aplicación, por ejemplo, mylib
, están instaladas en otro lugar, todo lo que necesita hacer es agregar esa ubicación a la ruta de Erlang, por ejemplo, con el code:add_path
. Erlang cargará automáticamente los módulos utilizados en el código de las carpetas agregadas a la lista de rutas del código.
Erlang integrado
Sin embargo, lo mismo ocurriría si la aplicación fuera una versión apropiada de OTP instalada independientemente de la instalación de Erlang en todo el sistema. Esto se debe a que, en ese caso, la secuencia de comandos se ejecuta mediante un escript
perteneciente a esa versión incrustada de Erlang en lugar de a la versión de todo el sistema (incluso si está instalada). Por lo tanto, conoce la ubicación de todas las aplicaciones que pertenecen a esa versión (incluidas sus aplicaciones). Por ejemplo, riak
hace exactamente eso: en su paquete, suministran una versión incorporada de Erlang que contiene sus propios erts
y todas las aplicaciones de Erlang dependientes. De esa forma, riak
se puede iniciar sin que Erlang esté instalado en el sistema operativo host. Este es un extracto de un paquete riak
en 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
Esto no difiere mucho en principio de lo anterior, aparte de tener que llamar explícitamente a la función que desea ejecutar al iniciar la máquina virtual Erlang (el punto de entrada o la función main
como la llamó).
Considere este script que builderl
genera para iniciar una aplicación Erlang solo para ejecutar una tarea específica (generar el archivo RELEASES
), después de lo cual el nodo se apaga:
#!/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()/""
Este es un script similar pero no inicia ningún código o aplicación específica. En su lugar, inicia una versión adecuada de OTP, por lo tanto, qué aplicaciones se inician y en qué orden depende de la versión (especificada por la opción -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"
En el archivo vm.args
puede proporcionar rutas adicionales a sus aplicaciones si es necesario, por ejemplo:
-pa lib/humbundee/ebin lib/yolf/ebin deps/goldrush/ebin deps/lager/ebin deps/yajler/ebin
En este ejemplo, estos son relativos, pero podrían ser absolutos si su aplicación se instala en una ubicación conocida estándar. Además, esto solo sería necesario si está utilizando la instalación de Erlang en todo el sistema y necesita agregar las rutas adicionales para ubicar sus aplicaciones Erlang, o si sus aplicaciones Erlang están ubicadas en una ubicación no estándar (por ejemplo, no en la carpeta lib
, como Erlang OTP requiere). En una versión de Erlang integrada adecuada, donde las aplicaciones se encuentran en la carpeta de código raíz / lib
, Erlang puede cargar esas aplicaciones sin especificar ninguna ruta adicional.
Resumiendo y otras consideraciones
La implementación de las aplicaciones de Erlang no difiere mucho de otros proyectos escritos en lenguajes de scripts, por ejemplo, proyectos ruby o python. Todos esos proyectos tienen que lidiar con problemas similares y creo que la administración de paquetes de cada sistema operativo los trata de una manera u otra:
Conozca cómo su sistema operativo trata con proyectos de empaque que tienen dependencias de tiempo de ejecución.
Vea cómo se empaquetan otras aplicaciones de Erlang para su sistema operativo, hay muchas de ellas que generalmente se distribuyen en todos los sistemas principales: RabbitMQ, Ejabberd, Riak, entre otros. Simplemente descargue el paquete y descomprímalo en una carpeta, luego verá dónde están ubicados todos los archivos.
EDITAR - referencia los requisitos
Volviendo a sus requisitos, tiene las siguientes opciones:
Instale Erlang como una versión de OTP en todo el sistema, como un Erlang incorporado, o como una bolsa con aplicaciones en algunas carpetas aleatorias (lo siento Rebar)
Puede tener múltiples puntos de entrada en forma de scripts
sh
oescript
ejecutando una selección de aplicaciones desde la versión instalada. Ambos funcionarán siempre que configure correctamente el código raíz y las rutas a esas aplicaciones (como se describe anteriormente).
Entonces, cada una de sus aplicaciones: myweb
y mycli
, deberían ejecutarse en su propio contexto nuevo, por ejemplo, iniciar una nueva instancia de VM y ejecutar la aplicación requerida (desde la misma versión de Erlang). En el caso de myweb
el punto de entrada puede ser un sh
scripts que inicia un nuevo nodo según el lanzamiento (similar a Riak). En el caso de mycli
el punto de entrada puede ser un escript
, que termina de ejecutarse una vez que se completa la tarea.
Pero es completamente posible crear una tarea de ejecución breve que salga de la máquina virtual, incluso si se inició desde sh
- ver el ejemplo anterior. En ese caso, mycli
requeriría archivos de lanzamiento separados: el script
y el boot
para arrancar la máquina virtual. Y, por supuesto, también es posible iniciar una VM de Erlang de larga ejecución desde escript
.
Proporcioné un proyecto de ejemplo que utiliza todos estos métodos a la vez, humbundee . Una vez compilado, proporciona tres puntos de acceso:
- El lanzamiento de
cmd
. - La liberación de
humbundee
. - The
builder.esh
escript
.
El primero se usa para iniciar el nodo para la instalación y luego cerrarlo. El segundo se usa para iniciar una aplicación Erlang de larga ejecución. El tercero es una herramienta de compilación para instalar / configurar el nodo. Así es como se ve el proyecto una vez que se ha creado la versión:
$:~/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
El punto de entrada del cmd
usará la aplicación deploy-0.0.1
y builderl-0.2.7
y también lanzará los archivos cmd.boot
, cmd.script
y algunas aplicaciones OTP. El punto de entrada de humbundee
estándar usará todas las aplicaciones además de builderl
y deploy
. A continuación, el builderl.esh
escript utilizará la aplicación deploy-0.0.1
y builderl-0.2.7
. Todo desde la misma instalación incorporada de Erlang OTP.