Current section

Files

Jump to
exscript lib exscript.ex
Raw

lib/exscript.ex

# Copyright 2012 Plataformatec
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This file is derived work from Mix.Tasks.Escript.Build for supporting flexible escript generation
defmodule Exscript do
use Bitwise, only_operators: true
@shortdoc "Generate escript"
def escriptize(project, language, escript_opts, force, should_consolidate) do
script_name = to_string(escript_opts[:name] || project[:app])
filename = escript_opts[:path] || script_name
main = escript_opts[:main_module]
app = Keyword.get(escript_opts, :app, project[:app])
files = project_files()
escript_mod = String.to_atom(Atom.to_string(app) <> "_escript")
cond do
!script_name ->
Mix.raise "Could not generate escript, no name given, " <>
"set :name escript option or :app in the project settings"
!main or !Code.ensure_loaded?(main)->
Mix.raise "Could not generate escript, please set :main_module " <>
"in your project configuration (under `:escript` option) to a module that implements main/1"
force || Mix.Utils.stale?(files, [filename]) ->
beam_paths =
[files, deps_files(), core_files(escript_opts, language)]
|> Stream.concat
|> prepare_beam_paths
beam_paths = if should_consolidate do
Path.wildcard(consolidated_path <> "/*")
|> prepare_beam_paths(beam_paths)
else
beam_paths
end
tuples = gen_main(escript_mod, main, app, language) ++
read_beams(beam_paths)
case :zip.create 'mem', tuples, [:memory] do
{:ok, {'mem', zip}} ->
shebang = escript_opts[:shebang] || "#! /usr/bin/env escript\n"
comment = build_comment(escript_opts[:comment])
emu_args = build_emu_args(escript_opts[:emu_args], escript_mod)
script = IO.iodata_to_binary([shebang, comment, emu_args, zip])
File.mkdir_p!(Path.dirname(filename))
File.write!(filename, script)
set_perms(filename)
{:error, error} ->
Mix.raise "Error creating escript: #{error}"
end
Mix.shell.info "Generated escript #{filename} with MIX_ENV=#{Mix.env}"
:ok
true ->
:noop
end
end
defp project_files() do
get_files(Mix.Project.app_path)
end
defp get_files(app) do
Path.wildcard("#{app}/ebin/*.{app,beam}") ++
(Path.wildcard("#{app}/priv/**/*") |> Enum.filter(&File.regular?/1))
end
defp set_perms(filename) do
stat = File.stat!(filename)
:ok = File.chmod(filename, stat.mode ||| 0o111)
end
defp deps_files() do
deps = Mix.Dep.loaded(env: Mix.env) || []
Enum.flat_map(deps, fn dep -> get_files(dep.opts[:build]) end)
end
defp core_files(escript_opts, language) do
if Keyword.get(escript_opts, :embed_elixir, language == :elixir) do
Enum.flat_map [:elixir|extra_apps()], &app_files/1
else
[]
end
end
defp extra_apps() do
mod = Mix.Project.get!
extra_apps =
if function_exported?(mod, :application, 0) do
mod.application[:applications]
end
Enum.filter(extra_apps || [], &(&1 in [:eex, :ex_unit, :mix, :iex, :logger]))
end
defp app_files(app) do
case :code.where_is_file('#{app}.app') do
:non_existing -> Mix.raise "Could not find application #{app}"
file -> get_files(Path.dirname(Path.dirname(file)))
end
end
defp prepare_beam_paths(paths, dict \\ HashDict.new) do
paths
|> Enum.map(&{Path.basename(&1), &1})
|> Enum.into(dict)
end
defp read_beams(items) do
items
|> Enum.map(fn {basename, beam_path} ->
{String.to_char_list(basename), File.read!(beam_path)}
end)
end
defp consolidated_path, do: Mix.Project.consolidation_path(Mix.Project.config)
defp build_comment(user_comment) do
"%% #{user_comment}\n"
end
defp build_emu_args(user_args, escript_mod) do
"%%! -escript main #{escript_mod} #{user_args}\n"
end
defp gen_main(name, module, app, language) do
config =
if File.regular?("config/config.exs") do
Mix.Config.read!("config/config.exs")
else
[]
end
module_body = quote do
@module unquote(module)
@config unquote(config)
@app unquote(app)
@spec main(OptionParser.argv) :: any
def main(args) do
unquote(main_body_for(language))
end
defp load_config(config) do
:lists.foreach(fn {app, kw} ->
:lists.foreach(fn {k, v} ->
:application.set_env(app, k, v, persistent: true)
end, kw)
end, config)
:ok
end
def start_app(nil) do
:ok
end
def start_app(app) do
case :application.ensure_all_started(app) do
{:ok, _} -> :ok
{:error, {app, reason}} ->
formatted_error = case :code.ensure_loaded(Application) do
{:module, Application} -> Application.format_error(reason)
{:error, _} -> :io_lib.format('~p', [reason])
end
io_error ["Could not start application ",
:erlang.atom_to_binary(app, :utf8),
": ", formatted_error, ?\n]
:erlang.halt(1)
end
end
defp io_error(message) do
:io.put_chars(:standard_error, message)
end
end
{:module, ^name, binary, _} = Module.create(name, module_body, Macro.Env.location(__ENV__))
[{'#{name}.beam', binary}]
end
defp main_body_for(:elixir) do
quote do
erl_version = :erlang.system_info(:otp_release)
case :string.to_integer(erl_version) do
{num, _} when num >= 17 -> nil
_ ->
io_error ["Incompatible Erlang/OTP release: ", erl_version,
".\nThis escript requires at least Erlang/OTP 17.0.\n"]
:erlang.halt(1)
end
case :application.ensure_all_started(:elixir) do
{:ok, _} ->
load_config(@config)
args = Enum.map(args, &List.to_string(&1))
Kernel.CLI.run fn _ -> @module.main(args) end, true
error ->
io_error ["Failed to start Elixir.\n", :io_lib.format('error: ~p~n', [error])]
:erlang.halt(1)
end
end
end
defp main_body_for(:erlang) do
quote do
load_config(@config)
@module.main(args)
end
end
end