├── .gitignore ├── Makefile ├── README.md ├── pkgx.config.sample ├── rebar.config ├── rebar.lock ├── src ├── pkgx.app.src ├── pkgx.erl ├── pkgx_app.erl ├── pkgx_sup.erl └── pkgx_target_deb.erl └── templates ├── deb_debian_changelog.dtl ├── deb_debian_control.dtl ├── deb_debian_copyright.dtl ├── deb_debian_init.dtl ├── deb_debian_install.dtl ├── deb_debian_postinst.dtl ├── deb_debian_postrm.dtl ├── deb_debian_rules.dtl └── package_config.dtl /.gitignore: -------------------------------------------------------------------------------- 1 | rebar 2 | ebin/*.beam 3 | ebin/*.app 4 | pkgx 5 | /.rebar/erlcinfo 6 | deps 7 | _build 8 | /rebar3 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR := ./rebar3 2 | REBAR_URL := https://s3.amazonaws.com/rebar3/rebar3 3 | ERL ?= erl 4 | 5 | .PHONY: compile test 6 | 7 | all: script 8 | 9 | script: compile 10 | $(REBAR) escriptize 11 | cp _build/default/bin/pkgx . 12 | 13 | compile: $(REBAR) 14 | $(REBAR) compile 15 | 16 | clean: $(REBAR) 17 | $(REBAR) clean 18 | rm -f pkgx 19 | 20 | $(REBAR): 21 | $(ERL) -noshell -s inets -s ssl \ 22 | -eval '{ok, saved_to_file} = httpc:request(get, {"$(REBAR_URL)", []}, [], [{stream, "$(REBAR)"}])' \ 23 | -s init stop 24 | chmod +x $(REBAR) 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pkgx - Easy packaging of Erlang releases 2 | ======================================== 3 | 4 | This program can be used to make OS packages for an Erlang application 5 | bundled as an OTP release. 6 | 7 | Getting started 8 | --------------- 9 | 10 | Create a ``pkgx.config`` in the root of your project. Look at the 11 | ``pkgx.config.sample`` file for inspiration. Also, make sure that 12 | you're all set with generating a release using ``rebar3``. 13 | 14 | Typical workflow scenario: 15 | 16 | $> rebar3 release 17 | $> pkgx deb 18 | 19 | Now, you have a `*.deb` file in your ``_build/prod/rel/`` directory 20 | which holds the Erlang release and can be easily installed on a target 21 | system. 22 | 23 | The service will automatically start (using an ``init.d`` script), and 24 | run under a new user account. Log files are found in 25 | `/var/log/(package)`. A default, empty, config file is created in 26 | `/etc/(package)/(package).config`, from which application environment 27 | variables are read from. 28 | 29 | -------------------------------------------------------------------------------- /pkgx.config.sample: -------------------------------------------------------------------------------- 1 | %% The name of the package 2 | {package_name, "example"}. 3 | 4 | %% The details of the package author 5 | {package_author_name, "Arjan Scherpenisse"}. 6 | {package_author_email, "arjan@scherpenisse.net"}. 7 | 8 | %% The user under which the service is going to run 9 | {package_install_user, "example"}. 10 | {package_install_user_desc, "Example user"}. 11 | 12 | %% Package description 13 | {package_shortdesc, "Example erlang app"}. 14 | {package_desc, "This is an example Erlang application, packaged using pkgx."}. 15 | 16 | %% License and copyright info for the package metadata 17 | {license_type, "MIT"}. 18 | {copyright, "2014 MiracleThings"}. 19 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %% -*- mode: Erlang; fill-column: 80; comment-column: 75; -*- 2 | 3 | {deps, 4 | [ 5 | {erlydtl, {git, "https://github.com/erlydtl/erlydtl.git", {branch, "master"}}} 6 | ]}. 7 | 8 | {plugins, 9 | [ 10 | {rebar3_erlydtl_plugin, ".*", {git, "https://github.com/tsloughter/rebar3_erlydtl_plugin.git", {branch, "master"}}} 11 | ]}. 12 | 13 | {provider_hooks, 14 | [ 15 | {pre, [{compile, {erlydtl, compile}}]} 16 | ]}. 17 | 18 | {escript_incl_apps, [erlydtl]}. 19 | 20 | {erlydtl_opts, 21 | [ 22 | {doc_root, "templates/"} 23 | ]}. 24 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | [{<<"erlydtl">>, 2 | {git,"https://github.com/erlydtl/erlydtl.git", 3 | {ref,"0b1cbebc68c86f02608c3c041a2361adf9acbc17"}}, 4 | 0}]. 5 | -------------------------------------------------------------------------------- /src/pkgx.app.src: -------------------------------------------------------------------------------- 1 | {application, pkgx, 2 | [ 3 | {description, ""}, 4 | {vsn, "1"}, 5 | {registered, []}, 6 | {applications, [ 7 | kernel, 8 | stdlib, 9 | erlydtl 10 | ]}, 11 | {mod, { pkgx_app, []}}, 12 | {env, []} 13 | ]}. 14 | -------------------------------------------------------------------------------- /src/pkgx.erl: -------------------------------------------------------------------------------- 1 | %% @author Arjan Scherpenisse 2 | %% @copyright 2014 Arjan Scherpenisse 3 | %% @doc Main 4 | 5 | -module(pkgx). 6 | 7 | -export([main/1]). 8 | 9 | main(Targets) -> 10 | ok = application:load(erlydtl), 11 | ok = application:load(pkgx), 12 | VarsFile = "pkgx.config", 13 | case filelib:is_regular(VarsFile) of 14 | false -> 15 | cli_error("File not found: " ++ VarsFile); 16 | true -> 17 | {ok, PkgVars} = file:consult(VarsFile), 18 | RelxVars = relx_vars(), 19 | {package_name, PkgName} = proplists:lookup(package_name, PkgVars), 20 | {release, {RelName, RelVer}, _RelDeps} = lists:keyfind(release, 1, RelxVars), 21 | BaseDir = lists:flatten(io_lib:format("_build/prod/rel/~s", [RelName])), 22 | io:format("Release name: '~s', version: '~s'\n", [RelName, RelVer]), 23 | ReleasesFile = BaseDir ++ "/releases/RELEASES", 24 | case filelib:is_regular(ReleasesFile) of 25 | false -> 26 | cli_error("No RELEASE file found for " ++ PkgName ++ ". Run 'rebar3 release' first. " ++ ReleasesFile); 27 | true -> 28 | {ok, [ReleasesList0]} = file:consult(ReleasesFile), 29 | [Release|_] = lists:sort(ReleasesList0), 30 | {release, AppName, Vsn, ErtsVsn, _Deps, _Permanent} = Release, 31 | io:format(user, "Using release: ~s ~s, ERTS ~s~n", [AppName, Vsn, ErtsVsn]), 32 | WithErts = filelib:is_dir(BaseDir ++ "/erts-" ++ ErtsVsn), 33 | Vars = [{app, AppName}, {version, Vsn}, {erts_version, ErtsVsn}, {basedir, BaseDir}, {relx, RelxVars}, {with_erts, WithErts} | PkgVars], 34 | [ok = run_target(AppName, Vsn, Vars, T) || T <- Targets], 35 | ok 36 | end 37 | end. 38 | 39 | relx_vars() -> 40 | File = "relx.config", 41 | case filelib:is_regular(File) of 42 | true -> 43 | {ok, Cfg} = file:consult(File), 44 | Cfg; 45 | false -> 46 | case filelib:is_regular("rebar.config") of 47 | true -> 48 | {ok, RebarCfg} = file:consult("rebar.config"), 49 | proplists:get_value(relx, RebarCfg, []); 50 | false -> 51 | [] 52 | end 53 | end. 54 | 55 | run_target(AppName, Vsn, PkgVars, T) -> 56 | Target = try 57 | list_to_existing_atom("pkgx_target_" ++ T) 58 | catch 59 | _:badarg -> 60 | throw({error, {unknown_target, T}}) 61 | end, 62 | ok = Target:run(AppName, Vsn, PkgVars). 63 | 64 | usage() -> 65 | io:format(user, "usage: pkgx ~n", []), 66 | ok. 67 | 68 | cli_error(Msg) -> 69 | io:format(user, "error: ~s~n", [Msg]), 70 | ok. 71 | 72 | -------------------------------------------------------------------------------- /src/pkgx_app.erl: -------------------------------------------------------------------------------- 1 | -module(pkgx_app). 2 | 3 | -behaviour(application). 4 | 5 | %% Application callbacks 6 | -export([start/2, stop/1]). 7 | 8 | %% =================================================================== 9 | %% Application callbacks 10 | %% =================================================================== 11 | 12 | start(_StartType, _StartArgs) -> 13 | pkgx_sup:start_link(). 14 | 15 | stop(_State) -> 16 | ok. 17 | -------------------------------------------------------------------------------- /src/pkgx_sup.erl: -------------------------------------------------------------------------------- 1 | -module(pkgx_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | %% API 6 | -export([start_link/0]). 7 | 8 | %% Supervisor callbacks 9 | -export([init/1]). 10 | 11 | %% Helper macro for declaring children of supervisor 12 | -define(CHILD(I, Type), {I, {I, start_link, []}, permanent, 5000, Type, [I]}). 13 | 14 | %% =================================================================== 15 | %% API functions 16 | %% =================================================================== 17 | 18 | start_link() -> 19 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 20 | 21 | %% =================================================================== 22 | %% Supervisor callbacks 23 | %% =================================================================== 24 | 25 | init([]) -> 26 | {ok, { {one_for_one, 5, 10}, []} }. 27 | 28 | -------------------------------------------------------------------------------- /src/pkgx_target_deb.erl: -------------------------------------------------------------------------------- 1 | %% @author Arjan Scherpenisse 2 | %% @copyright 2014 Arjan Scherpenisse 3 | %% @doc Debian package generation for relx 4 | 5 | -module(pkgx_target_deb). 6 | 7 | -export([run/3]). 8 | 9 | run(_AppName, _Vsn, PkgVars) -> 10 | PkgName = proplists:get_value(package_name, PkgVars), 11 | FileMap = 12 | [ 13 | {"debian/changelog", deb_debian_changelog_dtl}, 14 | {"debian/compat", <<"7">>}, 15 | {"debian/control", deb_debian_control_dtl}, 16 | {"debian/copyright", deb_debian_copyright_dtl}, 17 | {"debian/postinst", deb_debian_postinst_dtl}, 18 | {"debian/postrm", deb_debian_postrm_dtl}, 19 | {"debian/rules", deb_debian_rules_dtl}, 20 | {"debian/" ++ PkgName ++ ".init", deb_debian_init_dtl}, 21 | {"debian/" ++ PkgName ++ ".install", deb_debian_install_dtl}, 22 | {PkgName ++ ".config", package_config_dtl} 23 | ], 24 | Basedir = proplists:get_value(basedir, PkgVars), 25 | lists:map(fun ({F, V}) -> 26 | TargetFile = filename:join(Basedir, F), 27 | filelib:ensure_dir(TargetFile), 28 | process_file_entry(TargetFile, V, PkgVars) 29 | end, FileMap), 30 | Output = os:cmd("cd \"" ++ Basedir ++ "\" && debuild --no-tgz-check --no-lintian -i -us -uc -b"), 31 | io:format(user, "~s~n", [unicode:characters_to_binary(Output)]), 32 | ok. 33 | 34 | process_file_entry(File, Module, Vars) when is_atom(Module) -> 35 | {ok, Output} = Module:render(Vars), 36 | process_file_entry(File, iolist_to_binary(Output), Vars); 37 | 38 | process_file_entry(File, Output, _Vars) when is_binary(Output) -> 39 | io:format(user, "Created ~s~n", [File]), 40 | ok = file:write_file(File, Output). 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /templates/deb_debian_changelog.dtl: -------------------------------------------------------------------------------- 1 | {{ package_name }} ({{ version }}-1) unstable; urgency=low 2 | 3 | * Create release {{ version }} 4 | 5 | -- {{ package_author_name }} <{{ package_author_email }}> Wed, 23 Apr 2014 21:33:15 +0200 6 | -------------------------------------------------------------------------------- /templates/deb_debian_control.dtl: -------------------------------------------------------------------------------- 1 | Source: {{ package_name }} 2 | Section: net 3 | Priority: optional 4 | Maintainer: {{ package_author_name }} <{{ package_author_email }}> 5 | Build-Depends: debhelper (>= 7.0) 6 | Standards-Version: 3.9.2 7 | {% if package_url %}Homepage: {{ package_url }}{% endif %} 8 | {% if package_git %}Vcs-Git: {{ package_git }}{% endif %} 9 | 10 | Package: {{ package_name }} 11 | Architecture: amd64 12 | Depends: bash, adduser{% if package_depends %}, {{ package_depends }}{% endif %} 13 | Description: {{ package_shortdesc }} 14 | {{ package_desc }} -------------------------------------------------------------------------------- /templates/deb_debian_copyright.dtl: -------------------------------------------------------------------------------- 1 | This package is copyright {{ copyright }}. 2 | -------------------------------------------------------------------------------- /templates/deb_debian_init.dtl: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | ### BEGIN INIT INFO 4 | # Provides: {{ package_name }} 5 | # Required-Start: $local_fs $remote_fs $network $time 6 | # Required-Stop: $local_fs $remote_fs $network $time 7 | # Should-Start: 8 | # Should-Stop: 9 | # Default-Start: 2 3 4 5 10 | # Default-Stop: 0 1 6 11 | # Short-Description: {{ package_shortdesc }} 12 | ### END INIT INFO 13 | 14 | /usr/bin/sudo -u {{ package_install_user }} RELX_CONFIG_PATH=/etc/{{ package_name }}/{{ package_name }}.config -i /usr/lib/{{ package_name }}/bin/{{ app }} $@ 15 | -------------------------------------------------------------------------------- /templates/deb_debian_install.dtl: -------------------------------------------------------------------------------- 1 | bin/ /usr/lib/{{ package_name }} 2 | {% if with_erts %} 3 | erts-{{ erts_version }} /usr/lib/{{ package_name }} 4 | {% endif %} 5 | lib/ /usr/lib/{{ package_name }} 6 | releases/ /usr/lib/{{ package_name }} 7 | {{ package_name }}.config /etc/{{ package_name }} 8 | -------------------------------------------------------------------------------- /templates/deb_debian_postinst.dtl: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # postinst script for zotonic 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | . /usr/share/debconf/confmodule 9 | 10 | do_setup() 11 | { 12 | chown {{ package_install_user }}:{{ package_install_user }} -R /var/lib/{{ package_name }} 13 | mkdir -p /var/log/{{ package_name }} 14 | chown {{ package_install_user }}:{{ package_install_user }} /var/log/{{ package_name }} 15 | if [ ! -L "/usr/lib/{{ package_name }}/log" ]; then 16 | cd /usr/lib/{{ package_name }} && ln -s /var/log/{{ package_name }} ./log 17 | fi 18 | } 19 | 20 | case "$1" in 21 | configure|reconfigure) 22 | adduser --quiet --system --shell /bin/bash --group --home /var/lib/{{ package_install_user }} {{ package_install_user }} 23 | do_setup "$@" 24 | ;; 25 | 26 | abort-upgrade|abort-remove|abort-deconfigure) 27 | 28 | ;; 29 | 30 | *) 31 | echo "postinst called with unknown argument \`$1'" >&2 32 | exit 0 33 | ;; 34 | esac 35 | 36 | # dh_installdeb will replace this with shell code automatically 37 | # generated by other debhelper scripts. 38 | 39 | #DEBHELPER# 40 | 41 | exit 0 42 | -------------------------------------------------------------------------------- /templates/deb_debian_postrm.dtl: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # postrm script for {{ package_install_user }} 3 | # 4 | # see: dh_installdeb(1) 5 | 6 | set -e 7 | 8 | if [ -f /usr/share/debconf/confmodule ]; then 9 | . /usr/share/debconf/confmodule 10 | fi 11 | 12 | case "$1" in 13 | remove|upgrade|failed-upgrade|abort-install|abort-upgrade|disappear) 14 | ;; 15 | 16 | purge) 17 | rm -f /usr/lib/{{ package_name }}/log || true 18 | if which deluser >/dev/null ; then 19 | deluser {{ package_install_user }} 2>/dev/null || true 20 | delgroup {{ package_install_user }} 2>/dev/null || true 21 | fi 22 | ;; 23 | 24 | *) 25 | echo "postrm called with unknown argument \`$1'" >&2 26 | exit 1 27 | ;; 28 | esac 29 | 30 | # dh_installdeb will replace this with shell code automatically 31 | # generated by other debhelper scripts. 32 | 33 | #DEBHELPER# 34 | 35 | -------------------------------------------------------------------------------- /templates/deb_debian_rules.dtl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | override_dh_auto_clean: 4 | 5 | override_dh_auto_install: 6 | 7 | override_dh_auto_build: 8 | 9 | %: 10 | dh $@ 11 | -------------------------------------------------------------------------------- /templates/package_config.dtl: -------------------------------------------------------------------------------- 1 | %% Generated by pkg, default config file for {{ package_name }}. 2 | [ 3 | { 4 | {{ app }}, 5 | [ 6 | ] 7 | } 8 | ]. 9 | --------------------------------------------------------------------------------