Skip to content

Commit 998e66d

Browse files
Fix escript archive layout and support priv (#13730)
1 parent c887a8a commit 998e66d

File tree

3 files changed

+120
-29
lines changed

3 files changed

+120
-29
lines changed

lib/mix/lib/mix/tasks/escript.build.ex

+59-29
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,6 @@ defmodule Mix.Tasks.Escript.Build do
3636
the compiled `.beam` files to reduce the size of the escript.
3737
If this is not desired, check the `:strip_beams` option.
3838
39-
> #### `priv` directory support {: .warning}
40-
>
41-
> escripts do not support projects and dependencies
42-
> that need to store or read artifacts from the priv directory.
43-
4439
## Command line options
4540
4641
Expects the same command line options as `mix compile`.
@@ -94,6 +89,11 @@ defmodule Mix.Tasks.Escript.Build do
9489
* `:emu_args` - emulator arguments to embed in the escript file.
9590
Defaults to `""`.
9691
92+
* `:include_priv_for` - a list of application names (atoms) specifying
93+
applications which priv directory should be included in the resulting
94+
escript archive. Currently the expected way of accessing priv files
95+
in an escript is via `:escript.extract/2`. Defaults to `[]`.
96+
9797
There is one project-level option that affects how the escript is generated:
9898
9999
* `language: :elixir | :erlang` - set it to `:erlang` for Erlang projects
@@ -185,11 +185,16 @@ defmodule Mix.Tasks.Escript.Build do
185185

186186
escript_mod = String.to_atom(Atom.to_string(app) <> "_escript")
187187

188+
include_priv_for = MapSet.new(escript_opts[:include_priv_for] || [])
189+
188190
beam_paths =
189-
[project_files(), deps_files(), core_files(escript_opts, language)]
191+
[
192+
project_files(project, include_priv_for),
193+
deps_files(include_priv_for),
194+
core_files(escript_opts, language, include_priv_for)
195+
]
190196
|> Stream.concat()
191-
|> prepare_beam_paths()
192-
|> Map.merge(consolidated_paths(project))
197+
|> replace_consolidated_paths(project)
193198

194199
tuples = gen_main(project, escript_mod, main, app, language) ++ read_beams(beam_paths)
195200
tuples = if strip_options, do: strip_beams(tuples, strip_options), else: tuples
@@ -213,28 +218,40 @@ defmodule Mix.Tasks.Escript.Build do
213218
:ok
214219
end
215220

216-
defp project_files() do
217-
get_files(Mix.Project.app_path())
221+
defp project_files(project, include_priv_for) do
222+
get_files(Mix.Project.app_path(), project[:app] in include_priv_for)
218223
end
219224

220-
defp get_files(app) do
221-
Path.wildcard("#{app}/ebin/*.{app,beam}") ++
222-
(Path.wildcard("#{app}/priv/**/*") |> Enum.filter(&File.regular?/1))
225+
defp get_files(app_path, include_priv?) do
226+
paths = Path.wildcard("#{app_path}/ebin/*.{app,beam}")
227+
228+
paths =
229+
if include_priv? do
230+
paths ++ (Path.wildcard("#{app_path}/priv/**/*") |> Enum.filter(&File.regular?/1))
231+
else
232+
paths
233+
end
234+
235+
apps_dir = Path.dirname(app_path)
236+
237+
for path <- paths do
238+
{Path.relative_to(path, apps_dir), path}
239+
end
223240
end
224241

225242
defp set_perms(filename) do
226243
stat = File.stat!(filename)
227244
:ok = File.chmod(filename, stat.mode ||| 0o111)
228245
end
229246

230-
defp deps_files() do
247+
defp deps_files(include_priv_for) do
231248
deps = Mix.Dep.cached()
232-
Enum.flat_map(deps, fn dep -> get_files(dep.opts[:build]) end)
249+
Enum.flat_map(deps, fn dep -> get_files(dep.opts[:build], dep.app in include_priv_for) end)
233250
end
234251

235-
defp core_files(escript_opts, language) do
252+
defp core_files(escript_opts, language, include_priv_for) do
236253
if Keyword.get(escript_opts, :embed_elixir, language == :elixir) do
237-
Enum.flat_map([:elixir | extra_apps()], &app_files/1)
254+
Enum.flat_map([:elixir | extra_apps()], &app_files(&1, include_priv_for))
238255
else
239256
[]
240257
end
@@ -269,17 +286,13 @@ defmodule Mix.Tasks.Escript.Build do
269286
end
270287
end
271288

272-
defp app_files(app) do
289+
defp app_files(app, include_priv_for) do
273290
case :code.where_is_file(~c"#{app}.app") do
274291
:non_existing -> Mix.raise("Could not find application #{app}")
275-
file -> get_files(Path.dirname(Path.dirname(file)))
292+
file -> get_files(Path.dirname(Path.dirname(file)), app in include_priv_for)
276293
end
277294
end
278295

279-
defp prepare_beam_paths(paths) do
280-
for path <- paths, into: %{}, do: {Path.basename(path), path}
281-
end
282-
283296
defp read_beams(items) do
284297
Enum.map(items, fn {basename, beam_path} ->
285298
{String.to_charlist(basename), File.read!(beam_path)}
@@ -305,14 +318,31 @@ defmodule Mix.Tasks.Escript.Build do
305318
end
306319
end
307320

308-
defp consolidated_paths(config) do
321+
defp replace_consolidated_paths(files, config) do
322+
# We could write modules to a consolidated/ directory and prepend
323+
# it to code path using VM args. However, when Erlang Escript
324+
# boots, it prepends all second-level ebin/ directories to the
325+
# path, so the unconsolidated modules would take precedence.
326+
#
327+
# Instead of writing consolidated/ into the archive, we replace
328+
# the protocol modules with their consolidated version in their
329+
# usual location. As a side benefit, this reduces the Escript
330+
# file size, since we do not include the unconsolidated modules.
331+
309332
if config[:consolidate_protocols] do
310-
Mix.Project.consolidation_path(config)
311-
|> Path.join("*")
312-
|> Path.wildcard()
313-
|> prepare_beam_paths()
333+
consolidation_path = Mix.Project.consolidation_path(config)
334+
335+
consolidated =
336+
consolidation_path
337+
|> Path.join("*")
338+
|> Path.wildcard()
339+
|> Map.new(fn path -> {Path.basename(path), path} end)
340+
341+
for {zip_path, path} <- files do
342+
{zip_path, consolidated[Path.basename(path)] || path}
343+
end
314344
else
315-
%{}
345+
[]
316346
end
317347
end
318348

lib/mix/test/fixtures/escript_test/lib/escript_test.ex

+19
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,25 @@ defmodule EscriptTest do
1111
IO.inspect(Application.get_env(:foobar, :nesting, "TEST"))
1212
end
1313

14+
def main(["--app-paths"]) do
15+
elixir_path = Application.app_dir(:elixir) |> String.to_charlist()
16+
escript_test_path = Application.app_dir(:escript_test) |> String.to_charlist()
17+
18+
IO.inspect({
19+
elixir_path != escript_test_path,
20+
match?({:ok, _}, :erl_prim_loader.list_dir(elixir_path)),
21+
match?({:ok, _}, :erl_prim_loader.list_dir(escript_test_path))
22+
})
23+
end
24+
25+
def main(["--list-priv", app_name]) do
26+
# Note: :erl_prim_loader usage with Escript is currently deprecated,
27+
# but we use it only in tests for convenience
28+
29+
app = String.to_atom(app_name)
30+
:erl_prim_loader.list_dir(~c"#{:code.lib_dir(app)}/priv") |> IO.inspect()
31+
end
32+
1433
def main(_argv) do
1534
IO.puts(Application.get_env(:foobar, :value, "TEST"))
1635
end

lib/mix/test/mix/tasks/escript_test.exs

+42
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ defmodule Mix.Tasks.EscriptTest do
9696
end
9797
end
9898

99+
defmodule EscriptWithPrivs do
100+
def project do
101+
[
102+
app: :escript_test_with_priv,
103+
version: "0.0.1",
104+
escript: [main_module: EscriptTest, include_priv_for: [:escript_test_with_priv, :ok]],
105+
deps: [{:ok, path: fixture_path("deps_status/deps/ok")}]
106+
]
107+
end
108+
end
109+
99110
test "generate escript" do
100111
in_fixture("escript_test", fn ->
101112
Mix.Project.push(Escript)
@@ -104,6 +115,13 @@ defmodule Mix.Tasks.EscriptTest do
104115
assert_received {:mix_shell, :info, ["Generated escript escript_test with MIX_ENV=dev"]}
105116
assert System.cmd("escript", ["escript_test"]) == {"TEST\n", 0}
106117
assert count_abstract_code("escript_test") == 0
118+
119+
# Each app has a distinct, valid path
120+
assert System.cmd("escript", ["escript_test", "--app-paths"]) == {"{true, true, true}\n", 0}
121+
122+
# Does not include priv by default
123+
assert System.cmd("escript", ["escript_test", "--list-priv", "escript_test"]) ==
124+
{":error\n", 0}
107125
end)
108126
end
109127

@@ -229,6 +247,10 @@ defmodule Mix.Tasks.EscriptTest do
229247
assert_received {:mix_shell, :info, [^message]}
230248

231249
assert System.cmd("escript", ["escript_test_with_deps"]) == {"TEST\n", 0}
250+
251+
# Does not include priv for deps by default
252+
assert System.cmd("escript", ["escript_test_with_deps", "--list-priv", "ok"]) ==
253+
{":error\n", 0}
232254
end)
233255
after
234256
purge([Ok.MixProject])
@@ -280,6 +302,26 @@ defmodule Mix.Tasks.EscriptTest do
280302
end)
281303
end
282304

305+
test "generate escript with priv" do
306+
in_fixture("escript_test", fn ->
307+
Mix.Project.push(EscriptWithPrivs)
308+
309+
Mix.Tasks.Escript.Build.run([])
310+
311+
message = "Generated escript escript_test_with_priv with MIX_ENV=dev"
312+
assert_received {:mix_shell, :info, [^message]}
313+
314+
assert System.cmd("escript", [
315+
"escript_test_with_priv",
316+
"--list-priv",
317+
"escript_test_with_priv"
318+
]) == {~s/{:ok, [~c"hello"]}\n/, 0}
319+
320+
assert System.cmd("escript", ["escript_test_with_priv", "--list-priv", "ok"]) ==
321+
{~s/{:ok, [~c"sample"]}\n/, 0}
322+
end)
323+
end
324+
283325
test "escript install and uninstall" do
284326
File.rm_rf!(tmp_path(".mix/escripts"))
285327

0 commit comments

Comments
 (0)