Skip to content

Commit b6ea714

Browse files
GazlerJosé Valim
authored andcommitted
Add :tar option for releases to create a tarball (#9290)
1 parent ffe7a57 commit b6ea714

File tree

4 files changed

+100
-4
lines changed

4 files changed

+100
-4
lines changed

lib/mix/lib/mix/release.ex

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ defmodule Mix.Release do
2525
and `term` is the value given to it on `c:Config.Provider.init/1`
2626
* `:options` - a keyword list with all other user supplied release options
2727
* `:steps` - a list of functions that receive the release and returns a release.
28-
Must also contain the atom `:assemble` which is the internal assembling step
28+
Must also contain the atom `:assemble` which is the internal assembling step.
29+
May also contain the atom `:tar` to create a tarball of the release.
2930
3031
"""
3132
defstruct [
@@ -329,12 +330,14 @@ defmodule Mix.Release do
329330
end
330331

331332
defp validate_steps!(steps) do
332-
if not is_list(steps) or Enum.any?(steps, &(&1 != :assemble and not is_function(&1, 1))) do
333+
valid_atoms = [:assemble, :tar]
334+
335+
if not is_list(steps) or Enum.any?(steps, &(&1 not in valid_atoms and not is_function(&1, 1))) do
333336
Mix.raise("""
334337
The :steps option must be a list of:
335338
336339
* anonymous function that receives one argument
337-
* the atom :assemble
340+
* the atom :assemble or :tar
338341
339342
Got: #{inspect(steps)}
340343
""")
@@ -344,6 +347,14 @@ defmodule Mix.Release do
344347
Mix.raise("The :steps option must contain the atom :assemble once, got: #{inspect(steps)}")
345348
end
346349

350+
if :assemble in Enum.drop_while(steps, &(&1 != :tar)) do
351+
Mix.raise("The :tar step must come after :assemble")
352+
end
353+
354+
if Enum.count(steps, &(&1 == :tar)) > 1 do
355+
Mix.raise("The :steps option can only contain the atom :tar once")
356+
end
357+
347358
:ok
348359
end
349360

lib/mix/lib/mix/tasks/release.ex

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,9 @@ defmodule Mix.Tasks.Release do
491491
can pass anonymous functions before and after the `:assemble` to
492492
customize your release assembling pipeline. Those anonymous functions
493493
will receive a `Mix.Release` struct and must return the same or
494-
an updated `Mix.Release` struct.
494+
an updated `Mix.Release` struct. It is also possible to build a tarball
495+
of the release by passing the `:tar` step anywhere after `:assemble`.
496+
The tarball is created in `_build/MIX_ENV/rel/RELEASE_NAME-RELEASE_VSN.tar.gz`
495497
496498
See `Mix.Release` for more documentation on the struct and which
497499
fields can be modified. Note that `:steps` field itself can be
@@ -953,6 +955,10 @@ defmodule Mix.Tasks.Release do
953955
end
954956
end
955957

958+
defp run_steps(%{steps: [:tar | steps]} = release) do
959+
%{release | steps: steps} |> make_tar() |> run_steps()
960+
end
961+
956962
defp run_steps(%{steps: [:assemble | steps]} = release) do
957963
%{release | steps: steps} |> assemble() |> run_steps()
958964
end
@@ -1006,6 +1012,34 @@ defmodule Mix.Tasks.Release do
10061012
release
10071013
end
10081014

1015+
defp make_tar(release) do
1016+
tar_filename = "#{release.name}-#{release.version}.tar.gz"
1017+
out_path = Path.join([release.path, "..", "..", tar_filename]) |> Path.expand()
1018+
info(release, [:green, "* building ", :reset, out_path])
1019+
1020+
lib_dirs =
1021+
Enum.reduce(release.applications, [], fn {name, app_config}, acc ->
1022+
vsn = Keyword.fetch!(app_config, :vsn)
1023+
[Path.join("lib", "#{name}-#{vsn}") | acc]
1024+
end)
1025+
1026+
release_files =
1027+
for basename <- File.ls!(Path.join(release.path, "releases")),
1028+
not File.dir?(Path.join([release.path, "releases", basename])),
1029+
do: Path.join("releases", basename)
1030+
1031+
dirs =
1032+
["bin", Path.join("releases", release.version), "erts-#{release.erts_version}"] ++
1033+
lib_dirs ++ release_files
1034+
1035+
files =
1036+
Enum.map(dirs, &{String.to_charlist(&1), String.to_charlist(Path.join(release.path, &1))})
1037+
1038+
File.rm(out_path)
1039+
:ok = :erl_tar.create(String.to_charlist(out_path), files, [:dereference, :compressed])
1040+
release
1041+
end
1042+
10091043
# build_rel
10101044

10111045
defp build_rel(release, config) do

lib/mix/test/mix/release_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ defmodule Mix.ReleaseTest do
168168
~r"The :steps option must contain the atom :assemble once",
169169
fn -> release(steps: [:assemble, :assemble]) end
170170

171+
assert_raise Mix.Error,
172+
~r"The :tar step must come after :assemble",
173+
fn -> release(steps: [:tar, :assemble]) end
174+
175+
assert_raise Mix.Error,
176+
~r"The :steps option can only contain the atom :tar once",
177+
fn -> release(steps: [:assemble, :tar, :tar]) end
178+
171179
assert_raise Mix.Error,
172180
~r"The :steps option must be",
173181
fn -> release(steps: [:foo]) end

lib/mix/test/mix/tasks/release_test.exs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,49 @@ defmodule Mix.Tasks.ReleaseTest do
3636
end)
3737
end
3838

39+
test "tar" do
40+
in_fixture("release_test", fn ->
41+
config = [releases: [demo: [steps: [:assemble, :tar]]]]
42+
43+
Mix.Project.in_project(:release_test, ".", config, fn _ ->
44+
root = Path.absname("_build/#{Mix.env()}/rel/demo")
45+
46+
ignored_app_path = Path.join([root, "lib", "ignored_app-0.1.0", "ebin"])
47+
File.mkdir_p!(ignored_app_path)
48+
File.touch(Path.join(ignored_app_path, "ignored_app.app"))
49+
50+
ignored_release_path = Path.join([root, "releases", "ignored_dir"])
51+
File.mkdir_p!(ignored_release_path)
52+
File.touch(Path.join(ignored_release_path, "ignored"))
53+
54+
Mix.Task.run("release")
55+
tar_path = Path.expand(Path.join([root, "..", "..", "demo-0.1.0.tar.gz"]))
56+
message = "* building #{tar_path}"
57+
assert_received {:mix_shell, :info, [^message]}
58+
assert File.exists?(tar_path)
59+
60+
{:ok, files} = String.to_charlist(tar_path) |> :erl_tar.table([:compressed])
61+
62+
files = Enum.map(files, &to_string/1)
63+
files_with_versions = File.ls!(Path.join(root, "lib"))
64+
65+
assert "bin/demo" in files
66+
assert "releases/0.1.0/sys.config" in files
67+
assert "releases/0.1.0/vm.args" in files
68+
assert "releases/COOKIE" in files
69+
assert "releases/start_erl.data" in files
70+
71+
for dir <- files_with_versions -- ["ignored_app-0.1.0"] do
72+
[name | _] = String.split(dir, "-")
73+
assert "lib/#{dir}/ebin/#{name}.app" in files
74+
end
75+
76+
refute "lib/ignored_app-0.1.0/ebin/ignored_app.app" in files
77+
refute "releases/ignored_dir/ignored" in files
78+
end)
79+
end)
80+
end
81+
3982
test "steps" do
4083
in_fixture("release_test", fn ->
4184
last_step = fn release ->

0 commit comments

Comments
 (0)