From 01efaa6764088ee0df0d1ec6e5f561707af5ebe0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= <essen@ninenines.eu>
Date: Sun, 30 Oct 2016 16:56:29 +0200
Subject: Greatly improve the escript support

The plugin can now easily generate escripts as complex as relx
or rebar/rebar3. It generates a proper structure and allows
embedding extra files by extending the escript-zip target.

Documentation and tests have been added.
---
 doc/src/guide/escripts.asciidoc | 72 ++++++++++++++++++++++++++++++++-
 plugins/escript.mk              | 61 ++++++++++------------------
 test/plugin_escript.mk          | 89 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 180 insertions(+), 42 deletions(-)
 create mode 100644 test/plugin_escript.mk

diff --git a/doc/src/guide/escripts.asciidoc b/doc/src/guide/escripts.asciidoc
index 3d68c77..eebd6d8 100644
--- a/doc/src/guide/escripts.asciidoc
+++ b/doc/src/guide/escripts.asciidoc
@@ -1,6 +1,74 @@
 [[escript]]
 == Escripts
 
-// @todo Write it.
+Escripts are an alternative to release. They are meant to be
+used for small command line executables written in Erlang.
 
-Placeholder chapter.
+They are not self-contained, unlike xref:relx[releases].
+Erlang must be installed for them to run. This however means
+that they are fairly small compared to releases.
+
+For self-contained executables, check xref:sfx[self-extracting releases].
+
+Erlang.mk uses `p7zip` by default to generate the escript
+archive. Make sure it is installed.
+
+=== Generating an escript
+
+Run the following command to generate an escript:
+
+[source,bash]
+$ make escript
+
+This will by default create an escript with the same name as
+the project, in the project's directory. If the project is
+called `relx` then the escript will be in `./relx`.
+
+You can run the escript as you would any executable:
+
+[source,bash]
+$ ./relx
+
+=== Configuration
+
+You can change the name of the escript by setting `ESCRIPT_NAME`.
+The name determines both the default output file name and the
+entry module containing the function `main/1`.
+
+`ESCRIPT_FILE` can be set if you need a different file name
+or location.
+
+The escript header can be entirely customized. The first line
+is the shebang, set by `ESCRIPT_SHEBANG`. The second line is
+a comment, set by `ESCRIPT_COMMENT`. The third line is the
+arguments the VM will use when running the escript, set by
+`ESCRIPT_EMU_ARGS`.
+
+Finally, `ESCRIPT_ZIP` can be set to customize the command used
+to create the zip file. Read on for more information.
+
+=== Extra files
+
+Generating an escript is a two-part process. First, a zip file
+is created with the contents of the escript. Then a header is
+added to this file to create the escript.
+
+It is possible to add commands that will be executed between
+the two steps. You can for example add extra files to the zip
+archive:
+
+[source,make]
+----
+escript-zip::
+    $(verbose) $(ESCRIPT_ZIP) $(ESCRIPT_ZIP_FILE) priv/templates/*
+----
+
+The `ESCRIPT_ZIP` variable contains the command to run to add
+files to the zip archive `ESCRIPT_ZIP_FILE`.
+
+=== Optimizing for size
+
+Erlang.mk will by default compile BEAM files with debug
+information. You may want to disable this behavior to obtain
+smaller escript files. Simply set `ERLC_OPTS` to a value that
+does not include `+debug_info`.
diff --git a/plugins/escript.mk b/plugins/escript.mk
index 49fa989..6d70006 100644
--- a/plugins/escript.mk
+++ b/plugins/escript.mk
@@ -2,22 +2,19 @@
 # Copyright (c) 2014, Dave Cottlehuber <dch@skunkwerks.at>
 # This file is part of erlang.mk and subject to the terms of the ISC License.
 
-.PHONY: distclean-escript escript
+.PHONY: distclean-escript escript escript-zip
 
 # Configuration.
 
 ESCRIPT_NAME ?= $(PROJECT)
 ESCRIPT_FILE ?= $(ESCRIPT_NAME)
 
+ESCRIPT_SHEBANG ?= /usr/bin/env escript
 ESCRIPT_COMMENT ?= This is an -*- erlang -*- file
+ESCRIPT_EMU_ARGS ?= -escript main $(ESCRIPT_NAME)
 
-ESCRIPT_BEAMS ?= "ebin/*", "deps/*/ebin/*"
-ESCRIPT_SYS_CONFIG ?= "rel/sys.config"
-ESCRIPT_EMU_ARGS ?= -pa . \
-	-sasl errlog_type error \
-	-escript main $(ESCRIPT_NAME)
-ESCRIPT_SHEBANG ?= /usr/bin/env escript
-ESCRIPT_STATIC ?= "deps/*/priv/**", "priv/**"
+ESCRIPT_ZIP ?= 7z a -tzip -mx=9 -mtc=off $(if $(filter-out 0,$(V)),,> /dev/null)
+ESCRIPT_ZIP_FILE ?= $(ERLANG_MK_TMP)/escript.zip
 
 # Core targets.
 
@@ -30,38 +27,22 @@ help::
 
 # Plugin-specific targets.
 
-# Based on https://github.com/synrc/mad/blob/master/src/mad_bundle.erl
-# Copyright (c) 2013 Maxim Sokhatsky, Synrc Research Center
-# Modified MIT License, https://github.com/synrc/mad/blob/master/LICENSE :
-# Software may only be used for the great good and the true happiness of all
-# sentient beings.
-
-define ESCRIPT_RAW
-'Read = fun(F) -> {ok, B} = file:read_file(filename:absname(F)), B end,'\
-'Files = fun(L) -> A = lists:concat([filelib:wildcard(X)||X<- L ]),'\
-'  [F || F <- A, not filelib:is_dir(F) ] end,'\
-'Squash = fun(L) -> [{filename:basename(F), Read(F) } || F <- L ] end,'\
-'Zip = fun(A, L) -> {ok,{_,Z}} = zip:create(A, L, [{compress,all},memory]), Z end,'\
-'Ez = fun(Escript) ->'\
-'  Static = Files([$(ESCRIPT_STATIC)]),'\
-'  Beams = Squash(Files([$(ESCRIPT_BEAMS), $(ESCRIPT_SYS_CONFIG)])),'\
-'  Archive = Beams ++ [{ "static.gz", Zip("static.gz", Static)}],'\
-'  escript:create(Escript, [ $(ESCRIPT_OPTIONS)'\
-'    {archive, Archive, [memory]},'\
-'    {shebang, "$(ESCRIPT_SHEBANG)"},'\
-'    {comment, "$(ESCRIPT_COMMENT)"},'\
-'    {emu_args, " $(ESCRIPT_EMU_ARGS)"}'\
-'  ]),'\
-'  file:change_mode(Escript, 8#755)'\
-'end,'\
-'Ez("$(ESCRIPT_FILE)"),'\
-'halt().'
-endef
-
-ESCRIPT_COMMAND = $(subst ' ',,$(ESCRIPT_RAW))
-
-escript:: distclean-escript deps app
-	$(gen_verbose) $(ERL) -eval $(ESCRIPT_COMMAND)
+escript-zip:: deps app
+	$(verbose) mkdir -p $(dir $(ESCRIPT_ZIP))
+	$(verbose) rm -f $(ESCRIPT_ZIP_FILE)
+	$(gen_verbose) cd .. && $(ESCRIPT_ZIP) $(ESCRIPT_ZIP_FILE) $(PROJECT)/ebin/*
+ifneq ($(DEPS),)
+	$(verbose) cd $(DEPS_DIR) && $(ESCRIPT_ZIP) $(ESCRIPT_ZIP_FILE) \
+		`cat $(ERLANG_MK_TMP)/deps.log | sed 's/^$(subst /,\/,$(DEPS_DIR))\///' | sed 's/$$/\/ebin\/\*/'`
+endif
+
+escript:: escript-zip
+	$(gen_verbose) printf "%s\n" \
+		"#!$(ESCRIPT_SHEBANG)" \
+		"%% $(ESCRIPT_COMMENT)" \
+		"%%! $(ESCRIPT_EMU_ARGS)" > $(ESCRIPT_FILE)
+	$(verbose) cat $(ESCRIPT_ZIP_FILE) >> $(ESCRIPT_FILE)
+	$(verbose) chmod +x $(ESCRIPT_FILE)
 
 distclean-escript:
 	$(gen_verbose) rm -f $(ESCRIPT_NAME)
diff --git a/test/plugin_escript.mk b/test/plugin_escript.mk
new file mode 100644
index 0000000..3916094
--- /dev/null
+++ b/test/plugin_escript.mk
@@ -0,0 +1,89 @@
+# Escript plugin.
+
+ESCRIPT_CASES = build deps extra
+ESCRIPT_TARGETS = $(addprefix escript-,$(ESCRIPT_CASES))
+
+.PHONY: escript $(ESCRIPT_TARGETS)
+
+escript: $(ESCRIPT_TARGETS)
+
+escript-build: build clean
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Generate a module containing a function main/1"
+	$t printf "%s\n" \
+		"-module($(APP))." \
+		"-export([main/1])." \
+		'main(_) -> io:format("good~n").' > $(APP)/src/$(APP).erl
+
+	$i "Build the escript"
+	$t $(MAKE) -C $(APP) escript $v
+
+	$i "Check that the escript exists"
+	$t test -f $(APP)/$(APP)
+
+	$i "Check that the escript runs"
+	$t $(APP)/$(APP) | grep -q good
+
+	$i "Distclean the application"
+	$t $(MAKE) -C $(APP) distclean $v
+
+	$i "Check that the escript was removed"
+	$t test ! -e $(APP)/$(APP)
+
+escript-deps: build clean
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Add Ranch to the list of dependencies"
+	$t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = ranch\n"}' $(APP)/Makefile
+
+	$i "Generate a module containing a function main/1"
+	$t printf "%s\n" \
+		"-module($(APP))." \
+		"-export([main/1])." \
+		'main(_) -> io:format("good~n").' > $(APP)/src/$(APP).erl
+
+	$i "Build the escript"
+	$t $(MAKE) -C $(APP) escript $v
+
+	$i "Check that the escript runs"
+	$t $(APP)/$(APP) | grep -q good
+
+	$i "Check that the escript contains the dependency"
+	$t zipinfo $(APP)/$(APP) 2> /dev/null | grep -q ranch
+
+escript-extra: build clean
+
+	$i "Bootstrap a new OTP library named $(APP)"
+	$t mkdir $(APP)/
+	$t cp ../erlang.mk $(APP)/
+	$t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib $v
+
+	$i "Instruct Erlang.mk to add extra files to the escript"
+	$t printf "%s\n" \
+		"escript-zip::" \
+		'	$$(verbose) $$(ESCRIPT_ZIP) $$(ESCRIPT_ZIP_FILE) Makefile erlang.mk' >> $(APP)/Makefile
+
+	$i "Generate a module containing a function main/1"
+	$t printf "%s\n" \
+		"-module($(APP))." \
+		"-export([main/1])." \
+		'main(_) -> io:format("good~n").' > $(APP)/src/$(APP).erl
+
+	$i "Build the escript"
+	$t $(MAKE) -C $(APP) escript $v
+
+	$i "Check that the escript runs"
+	$t $(APP)/$(APP) | grep -q good
+
+	$i "Check that the escript contains the extra files"
+	$t zipinfo $(APP)/$(APP) 2> /dev/null | grep -q Makefile
+	$t zipinfo $(APP)/$(APP) 2> /dev/null | grep -q erlang.mk
-- 
cgit v1.2.3