Skip to content

Commit a54e901

Browse files
author
José Valim
committed
Add support for overlays in releases, closes #9651
1 parent 05be7aa commit a54e901

File tree

3 files changed

+193
-94
lines changed

3 files changed

+193
-94
lines changed

lib/mix/lib/mix/release.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ defmodule Mix.Release do
2424
first element is a module that implements the `Config.Provider` behaviour
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
27+
* `:overlays` - a list of extra files added to the release. If you have a custom
28+
step adding extra files to a release, you can add these files to the `:overlays`
29+
field so they are also considered on further commands, such as tar/zip. Each entry
30+
in overlays is the relative path to the release root of each file
2731
* `:steps` - a list of functions that receive the release and returns a release.
2832
Must also contain the atom `:assemble` which is the internal assembling step.
2933
May also contain the atom `:tar` to create a tarball of the release.
@@ -40,6 +44,7 @@ defmodule Mix.Release do
4044
:erts_version,
4145
:config_providers,
4246
:options,
47+
:overlays,
4348
:steps
4449
]
4550

@@ -147,6 +152,7 @@ defmodule Mix.Release do
147152
boot_scripts: %{start: start_boot, start_clean: start_clean_boot},
148153
config_providers: config_providers,
149154
options: opts,
155+
overlays: [],
150156
steps: steps
151157
}
152158
end

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

Lines changed: 79 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -431,12 +431,55 @@ defmodule Mix.Tasks.Release do
431431
]
432432
]
433433
434+
* `:overlays` - a directory with extra files to be copied as is to the
435+
release. See the "Overlays" section for more information. Defaults to
436+
"rel/overlays" if said directory exists.
437+
434438
* `:steps` - a list of steps to execute when assembling the release. See
435439
the "Steps" section for more information.
436440
437441
Besides the options above, it is possible to customize the generated
438-
release with custom template files or by tweaking the release steps.
439-
We will detail both approaches next.
442+
release with custom files, by tweaking the release steps or by running
443+
custom options and commands on boot. We will detail both approaches next.
444+
445+
### Overlays
446+
447+
Often it is necessary to copy extra files to the release root after
448+
the release is assembled. This can be easily done by placing such
449+
files in the `rel/overlays` directory. Any file in there is copied
450+
as is to the release root. For example, if you have place a
451+
"rel/overlays/Dockerfile" file, the "Dockerfile" will be copied as
452+
is to the release root. If you need to copy files dynamically, see
453+
the "Steps" section.
454+
455+
### Steps
456+
457+
It is possible to add one or more steps before and after the release is
458+
assembled. This can be done with the `:steps` option:
459+
460+
releases: [
461+
demo: [
462+
steps: [&set_configs/1, :assemble, &copy_extra_files/1]
463+
]
464+
]
465+
466+
The `:steps` option must be a list and it must always include the
467+
atom `:assemble`, which does most of the release assembling. You
468+
can pass anonymous functions before and after the `:assemble` to
469+
customize your release assembling pipeline. Those anonymous functions
470+
will receive a `Mix.Release` struct and must return the same or
471+
an updated `Mix.Release` struct. It is also possible to build a tarball
472+
of the release by passing the `:tar` step anywhere after `:assemble`.
473+
The tarball is created in `_build/MIX_ENV/RELEASE_NAME-RELEASE_VSN.tar.gz`
474+
475+
See `Mix.Release` for more documentation on the struct and which
476+
fields can be modified. Note that `:steps` field itself can be
477+
modified and it is updated every time a step is called. Therefore,
478+
if you need to execute a command before and after assembling the
479+
release, you only need to declare the first steps in your pipeline
480+
and then inject the last step into the release struct. The steps
481+
field can also be used to verify if the step was set before or
482+
after assembling the release.
440483
441484
### vm.args and env.sh (env.bat)
442485
@@ -489,35 +532,6 @@ defmodule Mix.Tasks.Release do
489532
set ELIXIR_ERL_OPTIONS="-kernel inet_dist_listen_min %BEAM_PORT% inet_dist_listen_max %BEAM_PORT%"
490533
)
491534
492-
### Steps
493-
494-
It is possible to add one or more steps before and after the release is
495-
assembled. This can be done with the `:steps` option:
496-
497-
releases: [
498-
demo: [
499-
steps: [&set_configs/1, :assemble, &copy_extra_files/1]
500-
]
501-
]
502-
503-
The `:steps` option must be a list and it must always include the
504-
atom `:assemble`, which does most of the release assembling. You
505-
can pass anonymous functions before and after the `:assemble` to
506-
customize your release assembling pipeline. Those anonymous functions
507-
will receive a `Mix.Release` struct and must return the same or
508-
an updated `Mix.Release` struct. It is also possible to build a tarball
509-
of the release by passing the `:tar` step anywhere after `:assemble`.
510-
The tarball is created in `_build/MIX_ENV/RELEASE_NAME-RELEASE_VSN.tar.gz`
511-
512-
See `Mix.Release` for more documentation on the struct and which
513-
fields can be modified. Note that `:steps` field itself can be
514-
modified and it is updated every time a step is called. Therefore,
515-
if you need to execute a command before and after assembling the
516-
release, you only need to declare the first steps in your pipeline
517-
and then inject the last step into the release struct. The steps
518-
field can also be used to verify if the step was set before or
519-
after assembling the release.
520-
521535
## Application configuration
522536
523537
Releases provides two mechanisms for configuring OTP applications:
@@ -1029,7 +1043,7 @@ defmodule Mix.Tasks.Release do
10291043
|> Task.async_stream(&copy(&1, release), ordered: false, timeout: :infinity)
10301044
|> Stream.run()
10311045

1032-
release
1046+
copy_overlays(release)
10331047
end
10341048

10351049
defp make_tar(release) do
@@ -1055,6 +1069,7 @@ defmodule Mix.Tasks.Release do
10551069
files =
10561070
dirs
10571071
|> Enum.filter(&File.exists?(Path.join(release.path, &1)))
1072+
|> Kernel.++(release.overlays)
10581073
|> Enum.map(&{String.to_charlist(&1), String.to_charlist(Path.join(release.path, &1))})
10591074

10601075
File.rm(out_path)
@@ -1202,6 +1217,38 @@ defmodule Mix.Tasks.Release do
12021217
Mix.shell().info([:yellow, "* skipping ", :reset, message])
12031218
end
12041219

1220+
## Overlays
1221+
1222+
defp copy_overlays(release) do
1223+
target = release.path
1224+
overlays = release.options[:overlays]
1225+
1226+
copied =
1227+
cond do
1228+
is_nil(overlays) and File.dir?("rel/overlays") ->
1229+
File.cp_r!("rel/overlays", target)
1230+
1231+
is_nil(overlays) ->
1232+
[]
1233+
1234+
is_binary(overlays) and File.dir?(overlays) ->
1235+
File.cp_r!(overlays, target)
1236+
1237+
true ->
1238+
Mix.raise(
1239+
":overlays release configuration must be a string pointing to an existing directory, " <>
1240+
"got: #{inspect(overlays)}"
1241+
)
1242+
end
1243+
1244+
relative =
1245+
copied
1246+
|> List.delete(target)
1247+
|> Enum.map(&Path.relative_to(&1, target))
1248+
1249+
update_in(release.overlays, &(relative ++ &1))
1250+
end
1251+
12051252
## Copy operations
12061253

12071254
defp copy(:erts, release) do

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

Lines changed: 108 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ defmodule Mix.Tasks.ReleaseTest do
99
defmacrop release_node(name), do: :"#{name}@#{@hostname}"
1010

1111
describe "customize" do
12-
test "rel with EEx" do
12+
test "env and vm.args with EEx" do
1313
in_fixture("release_test", fn ->
1414
Mix.Project.in_project(:release_test, ".", fn _ ->
1515
File.mkdir_p!("rel")
@@ -36,7 +36,106 @@ defmodule Mix.Tasks.ReleaseTest do
3636
end)
3737
end
3838

39-
test "tar" do
39+
test "steps" do
40+
in_fixture("release_test", fn ->
41+
last_step = fn release ->
42+
send(self(), {:last_step, release})
43+
release
44+
end
45+
46+
first_step = fn release ->
47+
send(self(), {:first_step, release})
48+
update_in(release.steps, &(&1 ++ [last_step]))
49+
end
50+
51+
config = [releases: [demo: [steps: [first_step, :assemble]]]]
52+
53+
Mix.Project.in_project(:release_test, ".", config, fn _ ->
54+
Mix.Task.run("release")
55+
assert_received {:mix_shell, :info, ["* assembling demo-0.1.0 on MIX_ENV=dev"]}
56+
57+
# Discard info messages from inbox for upcoming assertions
58+
Mix.shell().flush(& &1)
59+
60+
{:messages,
61+
[
62+
{:first_step, %Mix.Release{steps: [:assemble]}},
63+
{:last_step, %Mix.Release{steps: []}}
64+
]} = Process.info(self(), :messages)
65+
end)
66+
end)
67+
end
68+
69+
test "include_executables_for" do
70+
in_fixture("release_test", fn ->
71+
config = [releases: [release_test: [include_executables_for: []]]]
72+
73+
Mix.Project.in_project(:release_test, ".", config, fn _ ->
74+
root = Path.absname("_build/dev/rel/release_test")
75+
Mix.Task.run("release")
76+
assert_received {:mix_shell, :info, ["* assembling release_test-0.1.0 on MIX_ENV=dev"]}
77+
78+
refute root |> Path.join("bin/start") |> File.exists?()
79+
refute root |> Path.join("bin/start.bat") |> File.exists?()
80+
refute root |> Path.join("releases/0.1.0/elixir") |> File.exists?()
81+
refute root |> Path.join("releases/0.1.0/elixir.bat") |> File.exists?()
82+
refute root |> Path.join("releases/0.1.0/iex") |> File.exists?()
83+
refute root |> Path.join("releases/0.1.0/iex.bat") |> File.exists?()
84+
end)
85+
end)
86+
end
87+
88+
test "default overlays" do
89+
in_fixture("release_test", fn ->
90+
Mix.Project.in_project(:release_test, ".", fn _ ->
91+
File.mkdir_p!("rel/overlays/empty/directory")
92+
File.write!("rel/overlays/hello", "world")
93+
94+
root = Path.absname("_build/dev/rel/release_test")
95+
Mix.Task.run("release")
96+
97+
assert root |> Path.join("empty/directory") |> File.dir?()
98+
assert root |> Path.join("hello") |> File.read!() == "world"
99+
end)
100+
end)
101+
end
102+
103+
test "custom overlays" do
104+
in_fixture("release_test", fn ->
105+
config = [releases: [release_test: [overlays: "rel/another"]]]
106+
107+
Mix.Project.in_project(:release_test, ".", config, fn _ ->
108+
assert_raise Mix.Error, ~r"a string pointing to an existing directory", fn ->
109+
Mix.Task.run("release", ["--overwrite"])
110+
end
111+
112+
File.mkdir_p!("rel/another/empty/directory")
113+
File.write!("rel/another/hello", "world")
114+
115+
root = Path.absname("_build/dev/rel/release_test")
116+
Mix.Task.rerun("release", ["--overwrite"])
117+
118+
assert root |> Path.join("empty/directory") |> File.dir?()
119+
assert root |> Path.join("hello") |> File.read!() == "world"
120+
end)
121+
end)
122+
end
123+
end
124+
125+
describe "errors" do
126+
test "requires a matching name" do
127+
in_fixture("release_test", fn ->
128+
Mix.Project.in_project(:release_test, ".", fn _ ->
129+
assert_raise Mix.Error, ~r"Unknown release :unknown", fn ->
130+
Mix.Task.run("release", ["unknown"])
131+
end
132+
end)
133+
end)
134+
end
135+
end
136+
137+
describe "tar" do
138+
test "with ERTS" do
40139
in_fixture("release_test", fn ->
41140
config = [releases: [demo: [steps: [:assemble, :tar]]]]
42141

@@ -51,6 +150,10 @@ defmodule Mix.Tasks.ReleaseTest do
51150
File.mkdir_p!(ignored_release_path)
52151
File.touch(Path.join(ignored_release_path, "ignored"))
53152

153+
# Overlays
154+
File.mkdir_p!("rel/overlays/empty/directory")
155+
File.write!("rel/overlays/hello", "world")
156+
54157
Mix.Task.run("release")
55158
tar_path = Path.expand(Path.join([root, "..", "..", "demo-0.1.0.tar.gz"]))
56159
message = "* building #{tar_path}"
@@ -67,6 +170,8 @@ defmodule Mix.Tasks.ReleaseTest do
67170
assert "releases/0.1.0/vm.args" in files
68171
assert "releases/COOKIE" in files
69172
assert "releases/start_erl.data" in files
173+
assert "hello" in files
174+
assert "empty/directory" in files
70175
assert Enum.any?(files, &(&1 =~ "erts"))
71176
assert Enum.any?(files, &(&1 =~ "stdlib"))
72177

@@ -81,7 +186,7 @@ defmodule Mix.Tasks.ReleaseTest do
81186
end)
82187
end
83188

84-
test "tar without ERTS" do
189+
test "without ERTS" do
85190
in_fixture("release_test", fn ->
86191
config = [releases: [demo: [include_erts: false, steps: [:assemble, :tar]]]]
87192

@@ -103,55 +208,6 @@ defmodule Mix.Tasks.ReleaseTest do
103208
end)
104209
end)
105210
end
106-
107-
test "steps" do
108-
in_fixture("release_test", fn ->
109-
last_step = fn release ->
110-
send(self(), {:last_step, release})
111-
release
112-
end
113-
114-
first_step = fn release ->
115-
send(self(), {:first_step, release})
116-
update_in(release.steps, &(&1 ++ [last_step]))
117-
end
118-
119-
config = [releases: [demo: [steps: [first_step, :assemble]]]]
120-
121-
Mix.Project.in_project(:release_test, ".", config, fn _ ->
122-
Mix.Task.run("release")
123-
assert_received {:mix_shell, :info, ["* assembling demo-0.1.0 on MIX_ENV=dev"]}
124-
125-
# Discard info messages from inbox for upcoming assertions
126-
Mix.shell().flush(& &1)
127-
128-
{:messages,
129-
[
130-
{:first_step, %Mix.Release{steps: [:assemble]}},
131-
{:last_step, %Mix.Release{steps: []}}
132-
]} = Process.info(self(), :messages)
133-
end)
134-
end)
135-
end
136-
137-
test "include_executables_for" do
138-
in_fixture("release_test", fn ->
139-
config = [releases: [release_test: [include_executables_for: []]]]
140-
141-
Mix.Project.in_project(:release_test, ".", config, fn _ ->
142-
root = Path.absname("_build/dev/rel/release_test")
143-
Mix.Task.run("release")
144-
assert_received {:mix_shell, :info, ["* assembling release_test-0.1.0 on MIX_ENV=dev"]}
145-
146-
refute root |> Path.join("bin/start") |> File.exists?()
147-
refute root |> Path.join("bin/start.bat") |> File.exists?()
148-
refute root |> Path.join("releases/0.1.0/elixir") |> File.exists?()
149-
refute root |> Path.join("releases/0.1.0/elixir.bat") |> File.exists?()
150-
refute root |> Path.join("releases/0.1.0/iex") |> File.exists?()
151-
refute root |> Path.join("releases/0.1.0/iex.bat") |> File.exists?()
152-
end)
153-
end)
154-
end
155211
end
156212

157213
test "assembles a bootable release with ERTS" do
@@ -555,16 +611,6 @@ defmodule Mix.Tasks.ReleaseTest do
555611
end)
556612
end
557613

558-
test "requires a matching name" do
559-
in_fixture("release_test", fn ->
560-
Mix.Project.in_project(:release_test, ".", fn _ ->
561-
assert_raise Mix.Error, ~r"Unknown release :unknown", fn ->
562-
Mix.Task.run("release", ["unknown"])
563-
end
564-
end)
565-
end)
566-
end
567-
568614
defp open_port(command, args, env \\ []) do
569615
Port.open({:spawn_executable, to_charlist(command)}, [:hide, args: args, env: env])
570616
end

0 commit comments

Comments
 (0)