diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 850f8cb9..040a6ec4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,6 +20,8 @@ jobs: matrix: erlang: - '27' + elixir: + - '1.17' os: # - macos-latest - ubuntu-latest @@ -31,6 +33,7 @@ jobs: - core-compat # Trick GH runners into picking this slower job early. - ACME=1 c=core-deps + - core-elixir - core-makedep - core-misc - core-plugins @@ -78,6 +81,7 @@ jobs: uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.erlang }} + elixir-version: ${{ matrix.elixir }} version-type: loose - name: Setup MSYS2 (Windows) @@ -94,6 +98,10 @@ jobs: gnu-netcat diffutils + - name: Install libsodium (Linux, Elixir) + if: matrix.os == 'ubuntu-latest' && matrix.suite == 'core-elixir' + run: sudo apt-get -y install libsodium-dev + - name: Run tests (Linux) if: matrix.os == 'ubuntu-latest' run: make check c=${{ matrix.suite }} -j4 -k ${{ matrix.extra }} @@ -143,7 +151,7 @@ jobs: uses: erlef/setup-beam@v1 with: otp-version: ${{ matrix.erlang }} - elixir-version: 1.17 + elixir-version: '1.17' version-type: loose - name: Setup MSYS2 (Windows) diff --git a/build.config b/build.config index 5f1c1932..27b3dc9a 100644 --- a/build.config +++ b/build.config @@ -15,6 +15,7 @@ core/deps # Core modules, continued. core/beam-cache core/erlc +core/elixir core/docs core/rel core/test diff --git a/core/core.mk b/core/core.mk index 5d5eca00..222faebf 100644 --- a/core/core.mk +++ b/core/core.mk @@ -36,7 +36,7 @@ PROJECT ?= $(notdir $(CURDIR)) PROJECT := $(strip $(PROJECT)) PROJECT_VERSION ?= rolling -PROJECT_MOD ?= $(PROJECT)_app +PROJECT_MOD ?= PROJECT_ENV ?= [] # Verbosity. diff --git a/core/deps-tools.mk b/core/deps-tools.mk index 47f3b8e8..b29bf2a5 100644 --- a/core/deps-tools.mk +++ b/core/deps-tools.mk @@ -78,9 +78,7 @@ endif ifeq ($(IS_APP)$(IS_DEP),) $(verbose) sort < $(ERLANG_MK_RECURSIVE_TMP_LIST) | \ uniq > $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted - $(verbose) cmp -s $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $@ \ - || mv $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $@ - $(verbose) rm -f $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted + $(verbose) mv $(ERLANG_MK_RECURSIVE_TMP_LIST).sorted $@ $(verbose) rm $(ERLANG_MK_RECURSIVE_TMP_LIST) endif endif # ifneq ($(SKIP_DEPS),) diff --git a/core/deps.mk b/core/deps.mk index d557957d..6e4b7cc1 100644 --- a/core/deps.mk +++ b/core/deps.mk @@ -118,6 +118,8 @@ dep_name = $(call query_name,$(1)) # Application directories. LOCAL_DEPS_DIRS = $(foreach a,$(LOCAL_DEPS),$(if $(wildcard $(APPS_DIR)/$a),$(APPS_DIR)/$a)) +# Elixir is handled specially as it must be built before all other deps +# when Mix autopatching is necessary. ALL_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(BUILD_DEPS) $(DEPS)),$(call query_name,$(dep)))) # When we are calling an app directly we don't want to include it here @@ -210,9 +212,11 @@ endif ifneq ($(SKIP_DEPS),) deps:: else -deps:: $(ALL_DEPS_DIRS) apps clean-tmp-deps.log | $(ERLANG_MK_TMP) -ifneq ($(ALL_DEPS_DIRS),) - $(verbose) set -e; for dep in $(ALL_DEPS_DIRS); do \ +ALL_DEPS_DIRS_TO_BUILD = $(if $(filter-out $(DEPS_DIR)/elixir,$(ALL_DEPS_DIRS)),$(filter-out $(DEPS_DIR)/elixir,$(ALL_DEPS_DIRS)),$(ALL_DEPS_DIRS)) + +deps:: $(ALL_DEPS_DIRS_TO_BUILD) apps clean-tmp-deps.log | $(ERLANG_MK_TMP) +ifneq ($(ALL_DEPS_DIRS_TO_BUILD),) + $(verbose) set -e; for dep in $(ALL_DEPS_DIRS_TO_BUILD); do \ if grep -qs ^$$dep$$ $(ERLANG_MK_TMP)/deps.log; then \ :; \ else \ @@ -236,34 +240,41 @@ endif # Deps related targets. -# @todo rename GNUmakefile and makefile into Makefile first, if they exist -# While Makefile file could be GNUmakefile or makefile, -# in practice only Makefile is needed so far. -define dep_autopatch +autopatch_verbose_0 = @echo " PATCH " $(subst autopatch-,,$@) "(method: $(AUTOPATCH_METHOD))"; +autopatch_verbose_2 = set -x; +autopatch_verbose = $(autopatch_verbose_$(V)) + +define dep_autopatch_detect if [ -f $(DEPS_DIR)/$1/erlang.mk ]; then \ - rm -rf $(DEPS_DIR)/$1/ebin/; \ - $(call erlang,$(call dep_autopatch_appsrc.erl,$1)); \ - $(call dep_autopatch_erlang_mk,$1); \ + echo erlang.mk; \ + elif [ -f $(DEPS_DIR)/$1/mix.exs -a -d $(DEPS_DIR)/$1/lib ]; then \ + echo mix; \ elif [ -f $(DEPS_DIR)/$1/Makefile ]; then \ if [ -f $(DEPS_DIR)/$1/rebar.lock ]; then \ - $(call dep_autopatch2,$1); \ - elif [ 0 != `grep -c "include ../\w*\.mk" $(DEPS_DIR)/$1/Makefile` ]; then \ - $(call dep_autopatch2,$1); \ - elif [ 0 != `grep -ci "^[^#].*rebar" $(DEPS_DIR)/$1/Makefile` ]; then \ - $(call dep_autopatch2,$1); \ - elif [ -n "`find $(DEPS_DIR)/$1/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;`" ]; then \ - $(call dep_autopatch2,$1); \ - fi \ - else \ - if [ ! -d $(DEPS_DIR)/$1/src/ ]; then \ - $(call dep_autopatch_noop,$1); \ + echo rebar3; \ + elif [ 0 != \`grep -c "include ../\w*\.mk" $(DEPS_DIR)/$1/Makefile\` ]; then \ + echo rebar3; \ + elif [ 0 != \`grep -ci "^[^#].*rebar" $(DEPS_DIR)/$1/Makefile\` ]; then \ + echo rebar3; \ + elif [ -n "\`find $(DEPS_DIR)/$1/ -type f -name \*.mk -not -name erlang.mk -exec grep -i "^[^#].*rebar" '{}' \;\`" ]; then \ + echo rebar3; \ else \ - $(call dep_autopatch2,$1); \ + echo noop; \ fi \ + elif [ ! -d $(DEPS_DIR)/$1/src/ ]; then \ + echo noop; \ + else \ + echo rebar3; \ fi endef -define dep_autopatch2 +define dep_autopatch_for_erlang.mk + rm -rf $(DEPS_DIR)/$1/ebin/; \ + $(call erlang,$(call dep_autopatch_appsrc.erl,$1)); \ + $(call dep_autopatch_erlang_mk,$1) +endef + +define dep_autopatch_for_rebar3 ! test -f $(DEPS_DIR)/$1/ebin/$1.app || \ mv -n $(DEPS_DIR)/$1/ebin/$1.app $(DEPS_DIR)/$1/src/$1.app.src; \ rm -f $(DEPS_DIR)/$1/ebin/$1.app; \ @@ -279,8 +290,22 @@ define dep_autopatch2 fi endef -define dep_autopatch_noop - printf "noop:\n" > $(DEPS_DIR)/$1/Makefile +define dep_autopatch_for_mix + $(call dep_autopatch_mix,$1) +endef + +define dep_autopatch_for_noop + test -f $(DEPS_DIR)/$1/Makefile || printf "noop:\n" > $(DEPS_DIR)/$1/Makefile +endef + +define maybe_flock + if command -v flock >/dev/null; then \ + flock $1 sh -c "$2"; \ + elif command -v lockf >/dev/null; then \ + lockf $1 sh -c "$2"; \ + else \ + $2; \ + fi endef # Replace "include erlang.mk" with a line that will load the parent Erlang.mk @@ -307,13 +332,7 @@ endef # We use flock/lockf when available to avoid concurrency issues. define dep_autopatch_fetch_rebar - if command -v flock >/dev/null; then \ - flock $(ERLANG_MK_TMP)/rebar.lock sh -c "$(call dep_autopatch_fetch_rebar2)"; \ - elif command -v lockf >/dev/null; then \ - lockf $(ERLANG_MK_TMP)/rebar.lock sh -c "$(call dep_autopatch_fetch_rebar2)"; \ - else \ - $(call dep_autopatch_fetch_rebar2); \ - fi + $(call maybe_flock,$(ERLANG_MK_TMP)/rebar.lock,$(call dep_autopatch_fetch_rebar2)) endef define dep_autopatch_fetch_rebar2 @@ -397,7 +416,6 @@ define dep_autopatch_rebar.erl GetHexVsn2 = fun(N, NP) -> case file:consult("$(call core_native_path,$(DEPS_DIR)/$1/rebar.lock)") of {ok, Lock} -> - io:format("~p~n", [Lock]), LockPkgs = case lists:keyfind("1.2.0", 1, Lock) of {_, LP} -> LP; @@ -411,10 +429,8 @@ define dep_autopatch_rebar.erl end, if is_list(LockPkgs) -> - io:format("~p~n", [LockPkgs]), case lists:keyfind(atom_to_binary(N, latin1), 1, LockPkgs) of {_, {pkg, _, Vsn}, _} -> - io:format("~p~n", [Vsn]), {N, {hex, NP, binary_to_list(Vsn)}}; _ -> false @@ -806,6 +822,37 @@ define hex_get_tarball.erl end endef +# Unfortunately this currently requires Elixir. +define hex_version_resolver.erl + HexVersionResolve = fun(Name, Req) -> + application:ensure_all_started(ssl), + application:ensure_all_started(inets), + Config = $(hex_config.erl), + case hex_repo:get_package(Config, atom_to_binary(Name)) of + {ok, {200, _RespHeaders, Package}} -> + #{releases := List} = Package, + {value, #{version := Version}} = lists:search(fun(#{version := Vsn}) -> + M = list_to_atom("Elixir.Version"), + F = list_to_atom("match?"), + M:F(Vsn, Req) + end, List), + {ok, Version}; + {ok, {Status, _, Errors}} -> + {error, Status, Errors} + end + end, + HexVersionResolveAndPrint = fun(Name, Req) -> + case HexVersionResolve(Name, Req) of + {ok, Version} -> + io:format("~s", [Version]), + halt(0); + {error, Status, Errors} -> + io:format("Error ~b: ~0p~n", [Status, Errors]), + halt(77) + end + end +endef + ifeq ($(CACHE_DEPS),1) # Hex only has a package version. No need to look in the Erlang.mk packages. @@ -835,7 +882,7 @@ define dep_fetch_fail endef define dep_target -$(DEPS_DIR)/$(call query_name,$1): | $(if $(filter hex,$(call query_fetch_method,$1)),hex-core) $(ERLANG_MK_TMP) +$(DEPS_DIR)/$(call query_name,$1): $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),$(if $(filter-out elixir,$1),$(DEPS_DIR)/elixir/ebin/dep_built)) $(if $(filter hex,$(call query_fetch_method,$1)),$(if $(wildcard $(DEPS_DIR)/$(call query_name,$1)),,$(DEPS_DIR)/hex_core/ebin/dep_built)) | $(ERLANG_MK_TMP) $(eval DEP_NAME := $(call query_name,$1)) $(eval DEP_STR := $(if $(filter $1,$(DEP_NAME)),$1,"$1 ($(DEP_NAME))")) $(verbose) if test -d $(APPS_DIR)/$(DEP_NAME); then \ @@ -854,30 +901,39 @@ $(DEPS_DIR)/$(call query_name,$1): | $(if $(filter hex,$(call query_fetch_method cd $(DEPS_DIR)/$(DEP_NAME) && ./configure; \ fi ifeq ($(filter $1,$(NO_AUTOPATCH)),) - $(verbose) $$(MAKE) --no-print-directory autopatch-$(DEP_NAME) + $(verbose) $$(MAKE) --no-print-directory autopatch-$(DEP_NAME) AUTOPATCH_METHOD=`$(call dep_autopatch_detect,$1)` endif .PHONY: autopatch-$(call query_name,$1) +ifeq ($1,elixir) +autopatch-elixir:: + ln -s lib/elixir/ebin $(DEPS_DIR)/elixir/ +else autopatch-$(call query_name,$1):: - $(verbose) if [ "$1" = "elixir" -a "$(ELIXIR_PATCH)" ]; then \ - ln -s lib/elixir/ebin $(DEPS_DIR)/elixir/; \ - else \ - $$(call dep_autopatch,$(call query_name,$1)) \ - fi + $$(autopatch_verbose) $$(call dep_autopatch_for_$(AUTOPATCH_METHOD),$(call query_name,$1)) +endif endef # We automatically depend on hex_core when the project isn't already. $(if $(filter hex_core,$(DEPS) $(BUILD_DEPS) $(DOC_DEPS) $(REL_DEPS) $(TEST_DEPS)),,\ $(eval $(call dep_target,hex_core))) -.PHONY: hex-core - -hex-core: $(DEPS_DIR)/hex_core - $(verbose) if [ ! -e $(DEPS_DIR)/hex_core/ebin/dep_built ]; then \ - $(MAKE) -C $(DEPS_DIR)/hex_core IS_DEP=1; \ - touch $(DEPS_DIR)/hex_core/ebin/dep_built; \ - fi +$(DEPS_DIR)/hex_core/ebin/dep_built: | $(ERLANG_MK_TMP) + $(verbose) $(call maybe_flock,$(ERLANG_MK_TMP)/hex_core.lock,\ + if [ ! -e $(DEPS_DIR)/hex_core/ebin/dep_built ]; then \ + $(MAKE) $(DEPS_DIR)/hex_core; \ + $(MAKE) -C $(DEPS_DIR)/hex_core IS_DEP=1; \ + touch $(DEPS_DIR)/hex_core/ebin/dep_built; \ + fi) + +$(DEPS_DIR)/elixir/ebin/dep_built: | $(ERLANG_MK_TMP) + $(verbose) $(call maybe_flock,$(ERLANG_MK_TMP)/elixir.lock,\ + if [ ! -e $(DEPS_DIR)/elixir/ebin/dep_built ]; then \ + $(MAKE) $(DEPS_DIR)/elixir; \ + $(MAKE) -C $(DEPS_DIR)/elixir; \ + touch $(DEPS_DIR)/elixir/ebin/dep_built; \ + fi) $(foreach dep,$(BUILD_DEPS) $(DEPS),$(eval $(call dep_target,$(dep)))) diff --git a/core/elixir.mk b/core/elixir.mk new file mode 100644 index 00000000..fb6b5a14 --- /dev/null +++ b/core/elixir.mk @@ -0,0 +1,145 @@ +# Copyright (c) 2024, Tyler Hughes +# Copyright (c) 2024, Loïc Hoguin +# This file is part of erlang.mk and subject to the terms of the ISC License. + +ELIXIR ?= $(if $(filter elixir,$(BUILD_DEPS) $(DEPS)),dep,system) +export ELIXIR + +ifeq ($(ELIXIR),system) +# We expect 'elixir' to be on the path. +# @todo Only if there are EX_FILES +ELIXIR_LIBS ?= $(dir $(shell readlink -f `which elixir`))/../lib +ELIXIR_LIBS := $(ELIXIR_LIBS) +export ELIXIR_LIBS +ERL_LIBS := $(ERL_LIBS):$(ELIXIR_LIBS) +else +ERL_LIBS := $(ERL_LIBS):$(DEPS_DIR)/elixir/lib/ +endif + +elixirc_verbose_0 = @echo " EXC $(words $(EX_FILES)) files" +elixirc_verbose_2 = set -x; +elixirc_verbose = $(elixirc_verbose_$(V)) + +define dep_autopatch_mix.erl + $(call hex_version_resolver.erl), + {ok, _} = application:ensure_all_started(elixir), + {ok, _} = application:ensure_all_started(mix), + MixFile = <<"$(call core_native_path,$(DEPS_DIR)/$1/mix.exs)">>, + [{Mod, Bin}] = elixir_compiler:file(MixFile, fun(_File, _LexerPid) -> ok end), + {module, Mod} = code:load_binary(Mod, binary_to_list(MixFile), Bin), + Project = Mod:project(), + Application = try Mod:application() catch error:undef -> [] end, + StartMod = case lists:keyfind(mod, 1, Application) of + {mod, {StartMod0, _StartArgs}} -> + StartMod0; + _ -> + "" + end, + Write = fun (Text) -> + file:write_file("$(call core_native_path,$(DEPS_DIR)/$1/Makefile)", Text, [append]) + end, + Write([ + "PROJECT = ", atom_to_list(proplists:get_value(app, Project)), "\n" + "PROJECT_DESCRIPTION = ", proplists:get_value(description, Project, ""), "\n" + "PROJECT_VERSION = ", proplists:get_value(version, Project, ""), "\n" + "PROJECT_MOD = ", StartMod, "\n" + "define PROJECT_ENV\n", + io_lib:format("~p", [proplists:get_value(env, Application, [])]), "\n" + "endef\n\n"]), + ExtraApps = lists:usort([eex, elixir, logger, mix] ++ proplists:get_value(extra_applications, Application, [])), + Write(["LOCAL_DEPS += ", lists:join(" ", [atom_to_list(App) || App <- ExtraApps]), "\n\n"]), + Deps = proplists:get_value(deps, Project, []) -- [elixir_make], + IsRequiredProdDep = fun(Opts) -> + (proplists:get_value(optional, Opts) =/= true) + andalso + case proplists:get_value(only, Opts, prod) of + prod -> true; + L when is_list(L) -> lists:member(prod, L); + _ -> false + end + end, + lists:foreach(fun + ({Name, Req}) when is_binary(Req) -> + {ok, Vsn} = HexVersionResolve(Name, Req), + Write(["DEPS += ", atom_to_list(Name), "\n"]), + Write(["dep_", atom_to_list(Name), " = hex ", Vsn, " ", atom_to_list(Name), "\n"]); + ({Name, Opts}) when is_list(Opts) -> + Path = proplists:get_value(path, Opts), + case IsRequiredProdDep(Opts) of + true when Path =/= undefined -> + Write(["DEPS += ", atom_to_list(Name), "\n"]), + Write(["dep_", atom_to_list(Name), " = ln ", Path, "\n"]); + true when Path =:= undefined -> + Write(["DEPS += ", atom_to_list(Name), "\n"]), + io:format(standard_error, "Warning: No version given for ~p.", [Name]); + false -> + ok + end; + ({Name, Req, Opts}) -> + case IsRequiredProdDep(Opts) of + true -> + {ok, Vsn} = HexVersionResolve(Name, Req), + Write(["DEPS += ", atom_to_list(Name), "\n"]), + Write(["dep_", atom_to_list(Name), " = hex ", Vsn, " ", atom_to_list(Name), "\n"]); + false -> + ok + end; + (_) -> + ok + end, Deps), + case lists:member(elixir_make, proplists:get_value(compilers, Project, [])) of + false -> + ok; + true -> + Write("# https://hexdocs.pm/elixir_make/Mix.Tasks.Compile.ElixirMake.html\n"), + MakeVal = fun(Key, Proplist, DefaultVal, DefaultReplacement) -> + case proplists:get_value(Key, Proplist, DefaultVal) of + DefaultVal -> DefaultReplacement; + Value -> Value + end + end, + MakeMakefile = binary_to_list(MakeVal(make_makefile, Project, default, <<"Makefile">>)), + MakeExe = MakeVal(make_executable, Project, default, "$$\(MAKE)"), + MakeCwd = MakeVal(make_cwd, Project, undefined, <<".">>), + MakeTargets = MakeVal(make_targets, Project, [], []), + MakeArgs = MakeVal(make_args, Project, undefined, []), + case file:rename("$(DEPS_DIR)/$1/" ++ MakeMakefile, "$(DEPS_DIR)/$1/elixir_make.mk") of + ok -> ok; + Err = {error, _} -> + io:format(standard_error, "Failed to copy Makefile with error ~p~n", [Err]), + halt(90) + end, + Write(["app::\n" + "\t", MakeExe, " -C ", MakeCwd, " -f $(DEPS_DIR)/$1/elixir_make.mk", + lists:join(" ", MakeTargets), + lists:join(" ", MakeArgs), + "\n\n"]), + case MakeVal(make_clean, Project, nil, undefined) of + undefined -> + ok; + Clean -> + Write(["clean::\n\t", Clean, "\n\n"]) + end + end, + Write("ERLC_OPTS = +debug_info\n\n"), + Write("include $$\(if $$\(ERLANG_MK_FILENAME),$$\(ERLANG_MK_FILENAME),erlang.mk)"), + halt() +endef + +define dep_autopatch_mix + sed 's|\(defmodule.*do\)|\1\n try do\n Code.compiler_options(on_undefined_variable: :warn)\n rescue _ -> :ok\n end\n|g' -i $(DEPS_DIR)/$(1)/mix.exs; \ + $(MAKE) $(DEPS_DIR)/hex_core/ebin/dep_built; \ + MIX_ENV="$(if $(MIX_ENV),$(strip $(MIX_ENV)),prod)" \ + $(call erlang,$(call dep_autopatch_mix.erl,$1)) +endef + +define compile_ex.erl + {ok, _} = application:ensure_all_started(elixir), + {ok, _} = application:ensure_all_started(mix), + ModCode = list_to_atom("Elixir.Code"), + ModCode:put_compiler_option(ignore_module_conflict, true), + ModComp = list_to_atom("Elixir.Kernel.ParallelCompiler"), + {ok, Modules, _} = ModComp:compile_to_path([$(call comma_list,$(patsubst %,<<"%">>,$(EX_FILES)))], <<"ebin/">>), + lists:foreach(fun(E) -> io:format("~p ", [E]) end, Modules), + halt() +endef diff --git a/core/erlc.mk b/core/erlc.mk index f3b499a6..b4233412 100644 --- a/core/erlc.mk +++ b/core/erlc.mk @@ -49,7 +49,7 @@ mib_verbose_0 = @echo " MIB " $(filter %.bin %.mib,$(?F)); mib_verbose_2 = set -x; mib_verbose = $(mib_verbose_$(V)) -ifneq ($(wildcard src/),) +ifneq ($(wildcard src/)$(wildcard lib/),) # Targets. @@ -57,34 +57,21 @@ app:: $(if $(wildcard ebin/test),beam-cache-restore-app) deps $(verbose) $(MAKE) --no-print-directory $(PROJECT).d $(verbose) $(MAKE) --no-print-directory app-build -ifeq ($(wildcard src/$(PROJECT_MOD).erl),) -define app_file -{application, '$(PROJECT)', [ - {description, "$(PROJECT_DESCRIPTION)"}, - {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), - {id$(comma)$(space)"$1"}$(comma)) - {modules, [$(call comma_list,$2)]}, - {registered, []}, - {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(OPTIONAL_DEPS) $(foreach dep,$(DEPS),$(call query_name,$(dep))))]}, - {optional_applications, [$(call comma_list,$(OPTIONAL_DEPS))]}, - {env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),) -]}. -endef -else +PROJECT_MOD := $(if $(PROJECT_MOD),$(PROJECT_MOD),$(if $(wildcard src/$(PROJECT)_app.erl),$(PROJECT)_app)) + define app_file {application, '$(PROJECT)', [ {description, "$(PROJECT_DESCRIPTION)"}, {vsn, "$(PROJECT_VERSION)"},$(if $(IS_DEP), {id$(comma)$(space)"$1"}$(comma)) {modules, [$(call comma_list,$2)]}, - {registered, [$(call comma_list,$(PROJECT)_sup $(PROJECT_REGISTERED))]}, + {registered, [$(if $(PROJECT_MOD),$(call comma_list,$(if $(filter $(PROJECT_MOD),$(PROJECT)_app),$(PROJECT)_sup) $(PROJECT_REGISTERED)))]}, {applications, [$(call comma_list,kernel stdlib $(OTP_DEPS) $(LOCAL_DEPS) $(OPTIONAL_DEPS) $(foreach dep,$(DEPS),$(call query_name,$(dep))))]}, - {optional_applications, [$(call comma_list,$(OPTIONAL_DEPS))]}, - {mod, {$(PROJECT_MOD), []}}, + {optional_applications, [$(call comma_list,$(OPTIONAL_DEPS))]},$(if $(PROJECT_MOD), + {mod$(comma)$(space){$(patsubst %,'%',$(PROJECT_MOD))$(comma)$(space)[]}}$(comma)) {env, $(subst \,\\,$(PROJECT_ENV))}$(if $(findstring {,$(PROJECT_APP_EXTRA_KEYS)),$(comma)$(newline)$(tab)$(subst \,\\,$(PROJECT_APP_EXTRA_KEYS)),) ]}. endef -endif app-build: ebin/$(PROJECT).app $(verbose) : @@ -96,6 +83,10 @@ ALL_SRC_FILES := $(sort $(call core_find,src/,*)) ERL_FILES := $(filter %.erl,$(ALL_SRC_FILES)) CORE_FILES := $(filter %.core,$(ALL_SRC_FILES)) +# @todo Must be defined first. +ALL_LIB_FILES := $(sort $(call core_find,lib/,*)) +EX_FILES := $(filter-out lib/mix/%,$(filter %.ex,$(ALL_SRC_FILES) $(ALL_LIB_FILES))) + # ASN.1 files. ifneq ($(wildcard asn1/),) @@ -271,21 +262,21 @@ define makedep.erl endef ifeq ($(if $(NO_MAKEDEP),$(wildcard $(PROJECT).d),),) -$(PROJECT).d:: $(ERL_FILES) $(call core_find,include/,*.hrl) $(MAKEFILE_LIST) +$(PROJECT).d:: $(ERL_FILES) $(EX_FILES) $(call core_find,include/,*.hrl) $(MAKEFILE_LIST) $(makedep_verbose) $(call erlang,$(call makedep.erl,$@)) endif ifeq ($(IS_APP)$(IS_DEP),) -ifneq ($(words $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES)),0) +ifneq ($(words $(ERL_FILES) $(EX_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES) $(EX_FILES)),0) # Rebuild everything when the Makefile changes. $(ERLANG_MK_TMP)/last-makefile-change: $(MAKEFILE_LIST) | $(ERLANG_MK_TMP) $(verbose) if test -f $@; then \ - touch $(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES); \ + touch $(ERL_FILES) $(EX_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES) $(EX_FILES); \ touch -c $(PROJECT).d; \ fi $(verbose) touch $@ -$(ERL_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(ERLANG_MK_TMP)/last-makefile-change +$(ERL_FILES) $(EX_FILES) $(CORE_FILES) $(ASN1_FILES) $(MIB_FILES) $(XRL_FILES) $(YRL_FILES):: $(ERLANG_MK_TMP)/last-makefile-change ebin/$(PROJECT).app:: $(ERLANG_MK_TMP)/last-makefile-change endif endif @@ -312,13 +303,14 @@ define validate_app_file end endef -ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.src) - $(eval FILES_TO_COMPILE := $(filter-out src/$(PROJECT).app.src,$?)) +ebin/$(PROJECT).app:: $(ERL_FILES) $(CORE_FILES) $(wildcard src/$(PROJECT).app.src) $(EX_FILES) + $(eval FILES_TO_COMPILE := $(filter-out $(EX_FILES) src/$(PROJECT).app.src,$?)) $(if $(strip $(FILES_TO_COMPILE)),$(call compile_erl,$(FILES_TO_COMPILE))) + $(if $(filter $?,$(EX_FILES)),$(elixirc_verbose) $(eval MODULES := $(shell $(call erlang,$(call compile_ex.erl,$(EX_FILES)))))) # Older git versions do not have the --first-parent flag. Do without in that case. $(eval GITDESCRIBE := $(shell git describe --dirty --abbrev=7 --tags --always --first-parent 2>/dev/null \ || git describe --dirty --abbrev=7 --tags --always 2>/dev/null || true)) - $(eval MODULES := $(patsubst %,'%',$(sort $(notdir $(basename \ + $(eval MODULES := $(MODULES) $(patsubst %,'%',$(sort $(notdir $(basename \ $(filter-out $(ERLC_EXCLUDE_PATHS),$(ERL_FILES) $(CORE_FILES) $(BEAM_FILES))))))) ifeq ($(wildcard src/$(PROJECT).app.src),) $(app_verbose) printf '$(subst %,%%,$(subst $(newline),\n,$(subst ','\'',$(call app_file,$(GITDESCRIBE),$(MODULES)))))' \ diff --git a/index/elixir.mk b/index/elixir.mk new file mode 100644 index 00000000..86f89e79 --- /dev/null +++ b/index/elixir.mk @@ -0,0 +1,7 @@ +PACKAGES += elixir +pkg_elixir_name = elixir +pkg_elixir_description = Elixir is a dynamic, functional language for building scalable and maintainable applications. +pkg_elixir_homepage = https://elixir-lang.org +pkg_elixir_fetch = git +pkg_elixir_repo = https://github.com/elixir-lang/elixir +pkg_elixir_commit = main diff --git a/plugins/escript.mk b/plugins/escript.mk index 1790dcbc..4eac352f 100644 --- a/plugins/escript.mk +++ b/plugins/escript.mk @@ -27,17 +27,45 @@ help:: # Plugin-specific targets. -escript-zip:: FULL=1 -escript-zip:: deps app +ALL_ESCRIPT_DEPS_DIRS = $(LOCAL_DEPS_DIRS) $(addprefix $(DEPS_DIR)/,$(foreach dep,$(filter-out $(IGNORE_DEPS),$(DEPS)),$(call query_name,$(dep)))) + +ESCRIPT_RUNTIME_DEPS_FILE ?= $(ERLANG_MK_TMP)/escript-deps.log + +escript-list-runtime-deps: +ifeq ($(IS_DEP),) + $(verbose) rm -f $(ESCRIPT_RUNTIME_DEPS_FILE) +endif + $(verbose) touch $(ESCRIPT_RUNTIME_DEPS_FILE) + $(verbose) set -e; for dep in $(ALL_ESCRIPT_DEPS_DIRS) ; do \ + if ! grep -qs ^$$dep$$ $(ESCRIPT_RUNTIME_DEPS_FILE); then \ + echo $$dep >> $(ESCRIPT_RUNTIME_DEPS_FILE); \ + if grep -qs -E "^[[:blank:]]*include[[:blank:]]+(erlang\.mk|.*/erlang\.mk|.*ERLANG_MK_FILENAME.*)$$" \ + $$dep/GNUmakefile $$dep/makefile $$dep/Makefile; then \ + $(MAKE) -C $$dep escript-list-runtime-deps \ + IS_DEP=1 \ + ESCRIPT_RUNTIME_DEPS_FILE=$(ESCRIPT_RUNTIME_DEPS_FILE); \ + fi \ + fi \ + done +ifeq ($(IS_DEP),) + $(verbose) sort < $(ESCRIPT_RUNTIME_DEPS_FILE) | uniq > $(ESCRIPT_RUNTIME_DEPS_FILE).sorted + $(verbose) mv $(ESCRIPT_RUNTIME_DEPS_FILE).sorted $(ESCRIPT_RUNTIME_DEPS_FILE) +endif + +escript-prepare: deps app + $(MAKE) escript-list-runtime-deps + +escript-zip:: escript-prepare $(verbose) mkdir -p $(dir $(abspath $(ESCRIPT_ZIP_FILE))) $(verbose) rm -f $(abspath $(ESCRIPT_ZIP_FILE)) - $(gen_verbose) cd .. && $(ESCRIPT_ZIP) $(abspath $(ESCRIPT_ZIP_FILE)) $(PROJECT)/ebin/* + $(gen_verbose) cd .. && $(ESCRIPT_ZIP) $(abspath $(ESCRIPT_ZIP_FILE)) $(notdir $(CURDIR))/ebin/* ifneq ($(DEPS),) $(verbose) cd $(DEPS_DIR) && $(ESCRIPT_ZIP) $(abspath $(ESCRIPT_ZIP_FILE)) \ $(subst $(DEPS_DIR)/,,$(addsuffix /*,$(wildcard \ - $(addsuffix /ebin,$(shell cat $(ERLANG_MK_TMP)/deps.log))))) + $(addsuffix /ebin,$(shell cat $(ESCRIPT_RUNTIME_DEPS_FILE)))))) endif +# @todo Only generate the zip file if there were changes. escript:: escript-zip $(gen_verbose) printf "%s\n" \ "#!$(ESCRIPT_SHEBANG)" \ diff --git a/plugins/hex.mk b/plugins/hex.mk index 44beb658..2b52f4e5 100644 --- a/plugins/hex.mk +++ b/plugins/hex.mk @@ -19,7 +19,7 @@ define hex_user_create.erl endef # The $(info ) call inserts a new line after the password prompt. -hex-user-create: hex-core +hex-user-create: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username))) $(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info ))) $(if $(HEX_EMAIL),,$(eval HEX_EMAIL := $(shell read -p "Email: " email; echo $$email))) @@ -49,7 +49,7 @@ define hex_key_add.erl end endef -hex-key-add: hex-core +hex-key-add: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_USERNAME),,$(eval HEX_USERNAME := $(shell read -p "Username: " username; echo $$username))) $(if $(HEX_PASSWORD),,$(eval HEX_PASSWORD := $(shell stty -echo; read -p "Password: " password; stty echo; echo $$password) $(info ))) $(gen_verbose) $(call erlang,$(call hex_key_add.erl,$(HEX_USERNAME),$(HEX_PASSWORD),\ @@ -127,7 +127,7 @@ hex_tar_verbose_0 = @echo " TAR $(notdir $(ERLANG_MK_TMP))/$(@F)"; hex_tar_verbose_2 = set -x; hex_tar_verbose = $(hex_tar_verbose_$(V)) -$(HEX_TARBALL_OUTPUT_FILE): hex-core app +$(HEX_TARBALL_OUTPUT_FILE): $(DEPS_DIR)/hex_core/ebin/dep_built app $(hex_tar_verbose) $(call erlang,$(call hex_tarball_create.erl)) hex-tarball-create: $(HEX_TARBALL_OUTPUT_FILE) @@ -178,14 +178,14 @@ define hex_release_publish.erl end endef -hex-release-tarball: hex-core $(HEX_TARBALL_OUTPUT_FILE) +hex-release-tarball: $(DEPS_DIR)/hex_core/ebin/dep_built $(HEX_TARBALL_OUTPUT_FILE) $(verbose) $(call erlang,$(call hex_release_publish_summary.erl)) -hex-release-publish: hex-core hex-release-tarball +hex-release-publish: $(DEPS_DIR)/hex_core/ebin/dep_built hex-release-tarball $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),false)) -hex-release-replace: hex-core hex-release-tarball +hex-release-replace: $(DEPS_DIR)/hex_core/ebin/dep_built hex-release-tarball $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_publish.erl,$(HEX_SECRET),true)) @@ -204,7 +204,7 @@ define hex_release_delete.erl end endef -hex-release-delete: hex-core +hex-release-delete: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_delete.erl,$(HEX_SECRET))) @@ -224,7 +224,7 @@ define hex_release_retire.erl end endef -hex-release-retire: hex-core +hex-release-retire: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_retire.erl,$(HEX_SECRET),\ $(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION)),\ @@ -246,7 +246,7 @@ define hex_release_unretire.erl end endef -hex-release-unretire: hex-core +hex-release-unretire: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_release_unretire.erl,$(HEX_SECRET),\ $(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION)))) @@ -255,7 +255,7 @@ HEX_DOCS_DOC_DIR ?= doc/ HEX_DOCS_TARBALL_FILES ?= $(sort $(call core_find,$(HEX_DOCS_DOC_DIR),*)) HEX_DOCS_TARBALL_OUTPUT_FILE ?= $(ERLANG_MK_TMP)/$(PROJECT)-docs.tar.gz -$(HEX_DOCS_TARBALL_OUTPUT_FILE): hex-core app docs +$(HEX_DOCS_TARBALL_OUTPUT_FILE): $(DEPS_DIR)/hex_core/ebin/dep_built app docs $(hex_tar_verbose) tar czf $(HEX_DOCS_TARBALL_OUTPUT_FILE) -C $(HEX_DOCS_DOC_DIR) \ $(HEX_DOCS_TARBALL_FILES:$(HEX_DOCS_DOC_DIR)%=%) @@ -279,7 +279,7 @@ define hex_docs_publish.erl end endef -hex-docs-publish: hex-core hex-docs-tarball-create +hex-docs-publish: $(DEPS_DIR)/hex_core/ebin/dep_built hex-docs-tarball-create $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_docs_publish.erl,$(HEX_SECRET))) @@ -299,7 +299,7 @@ define hex_docs_delete.erl end endef -hex-docs-delete: hex-core +hex-docs-delete: $(DEPS_DIR)/hex_core/ebin/dep_built $(if $(HEX_SECRET),,$(eval HEX_SECRET := $(shell stty -echo; read -p "Secret: " secret; stty echo; echo $$secret) $(info ))) $(gen_verbose) $(call erlang,$(call hex_docs_delete.erl,$(HEX_SECRET),\ $(if $(HEX_VERSION),$(HEX_VERSION),$(PROJECT_VERSION)))) diff --git a/test/core_elixir.mk b/test/core_elixir.mk new file mode 100644 index 00000000..19899f5f --- /dev/null +++ b/test/core_elixir.mk @@ -0,0 +1,151 @@ +# Core: Miscellaneous. +# +# The miscellaneous tests use the prefix "core-", not "core-misc-". + +CORE_ELIXIR_TARGETS = $(call list_targets,core-elixir) + +.PHONY: core-elixir $(CORE_ELIXIR_TARGETS) + +core-elixir: $(CORE_ELIXIR_TARGETS) + +core-elixir-from-dep: init + + $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 Elixir, Lager, Jason, Phoenix to the list of dependencies" + $t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = elixir lager jason phoenix\ndep_elixir_commit = v1.17.3\ndep_lager = git https://github.com/erlang-lager/lager master\ndep_jason = git https://github.com/michalmuskala/jason.git master\ndep_phoenix = hex 1.7.2\n"}' $(APP)/Makefile + + $i "Add the lager_transform parse_transform to ERLC_OPTS" + $t echo "ERLC_OPTS += +'{parse_transform, lager_transform}'" >> $(APP)/Makefile + +ifdef LEGACY + $i "Add Elixir, Lager, Jason and Phoenix to the applications key in the .app.src file" + $t perl -ni.bak -e 'print;if ($$.==7) {print "\t\telixir,\n\t\tlager,\n\t\tjason,\n\t\tphoenix,\n"}' $(APP)/src/$(APP).app.src +endif + + $i "Build the application" + $t $(MAKE) -C $(APP) $v + + $i "Check that all dependencies were fetched and built" + $t test -f $(APP)/deps/elixir/ebin/dep_built + $t test -f $(APP)/deps/lager/ebin/dep_built + $t test -f $(APP)/deps/jason/ebin/dep_built + $t test -f $(APP)/deps/phoenix/ebin/dep_built + + $i "Check that the application was compiled correctly" + $t $(ERL) -pa $(APP)/ebin/ -pa $(APP)/deps/*/ebin -pa $(APP)/deps/elixir/lib/*/ebin -eval " \ + {ok, Apps} = application:ensure_all_started('$(APP)'), \ + true = lists:member(elixir, Apps), \ + true = lists:member(lager, Apps), \ + true = lists:member(jason, Apps), \ + true = lists:member(phoenix, Apps), \ + halt()" + + $i "Check that the Jason application depends on Elixir builtins" + $t $(ERL) -pa $(APP)/ebin/ -pa $(APP)/deps/*/ebin -pa $(APP)/deps/elixir/lib/*/ebin -eval " \ + {ok, Apps} = application:ensure_all_started(jason), \ + true = lists:member(elixir, Apps), \ + true = lists:member(eex, Apps), \ + true = lists:member(logger, Apps), \ + true = lists:member(mix, Apps), \ + halt()" + +core-elixir-from-system: init + + $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 Lager, Jason, Phoenix to the list of dependencies" + $t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = lager jason phoenix\ndep_lager = git https://github.com/erlang-lager/lager master\ndep_jason = git https://github.com/michalmuskala/jason.git master\ndep_phoenix = hex 1.7.2\n"}' $(APP)/Makefile + + $i "Add the lager_transform parse_transform to ERLC_OPTS" + $t echo "ERLC_OPTS += +'{parse_transform, lager_transform}'" >> $(APP)/Makefile + +ifdef LEGACY + $i "Add Lager, Jason and Phoenix to the applications key in the .app.src file" + $t perl -ni.bak -e 'print;if ($$.==7) {print "\t\telixir,\n\t\tlager,\n\t\tjason,\n\t\tphoenix,\n"}' $(APP)/src/$(APP).app.src +endif + + $i "Build the application" + $t $(MAKE) -C $(APP) $v + + $i "Check that all dependencies were fetched and built" + $t ! test -e $(APP)/deps/elixir + $t test -f $(APP)/deps/lager/ebin/dep_built + $t test -f $(APP)/deps/jason/ebin/dep_built + $t test -f $(APP)/deps/phoenix/ebin/dep_built + + $i "Check that the application was compiled correctly" + $t $(ERL) -pa $(APP)/ebin/ -pa $(APP)/deps/*/ebin -pa $(dir $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))'))/*/ebin -eval " \ + {ok, Apps} = application:ensure_all_started('$(APP)'), \ + true = lists:member(lager, Apps), \ + true = lists:member(jason, Apps), \ + true = lists:member(phoenix, Apps), \ + halt()" + + $i "Check that the Jason application depends on Elixir builtins" + $t $(ERL) -pa $(APP)/ebin/ -pa $(APP)/deps/*/ebin -pa $(dir $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))'))/*/ebin -eval " \ + {ok, Apps} = application:ensure_all_started(jason), \ + true = lists:member(elixir, Apps), \ + true = lists:member(eex, Apps), \ + true = lists:member(logger, Apps), \ + true = lists:member(mix, Apps), \ + halt()" + +core-elixir-nif: init + + $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 Libsalty2 to the list of dependencies" + $t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = libsalty2\ndep_libsalty2 = git https://github.com/Ianleeclark/libsalty2.git b11e544\n"}' $(APP)/Makefile + +ifdef LEGACY + $i "Add Libsalty2 to the applications key in the .app.src file" + $t perl -ni.bak -e 'print;if ($$.==7) {print "\t\tlibsalty2,\n"}' $(APP)/src/$(APP).app.src +endif + + $i "Build the application" + $t $(MAKE) -C $(APP) $v + + $i "Check that the application was compiled correctly" + $t $(ERL) -pa $(APP)/ebin/ -pa $(APP)/deps/*/ebin -pa $(dir $(shell elixir -e 'IO.puts(:code.lib_dir(:elixir))'))/*/ebin -eval " \ + {ok, Apps} = application:ensure_all_started('$(APP)'), \ + true = lists:member(libsalty2, Apps), \ + halt()" + +core-elixir-rel: init + + $i "Bootstrap a new release named $(APP)" + $t mkdir $(APP)/ + $t cp ../erlang.mk $(APP)/ + $t $(MAKE) -C $(APP) -f erlang.mk bootstrap-lib bootstrap-rel $v + + $i "Add Lager, Jason, Phoenix to the list of dependencies" + $t perl -ni.bak -e 'print;if ($$.==1) {print "DEPS = lager jason phoenix\ndep_lager = git https://github.com/erlang-lager/lager master\ndep_jason = git https://github.com/michalmuskala/jason.git master\ndep_phoenix = hex 1.7.2\n"}' $(APP)/Makefile + + $i "Add the lager_transform parse_transform to ERLC_OPTS" + $t echo "ERLC_OPTS += +'{parse_transform, lager_transform}'" >> $(APP)/Makefile + +ifdef LEGACY + $i "Add Lager, Jason and Phoenix to the applications key in the .app.src file" + $t perl -ni.bak -e 'print;if ($$.==7) {print "\t\tlager,\n\t\tjason,\n\t\tphoenix,\n"}' $(APP)/src/$(APP).app.src +endif + + $i "Build the release" + $t $(MAKE) -C $(APP) $v + + $i "Check that the release was built" + $t test -d $(APP)/_rel + $t test -d $(APP)/_rel/$(APP)_release + $t test -d $(APP)/_rel/$(APP)_release/bin + $t test -d $(APP)/_rel/$(APP)_release/lib + $t test -d $(APP)/_rel/$(APP)_release/releases + $t test -d $(APP)/_rel/$(APP)_release/releases/1