This repository has been archived by the owner on Apr 7, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 110
/
release.ex
488 lines (451 loc) · 17.4 KB
/
release.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
defmodule Mix.Tasks.Release do
@moduledoc """
Build a release for the current mix application.
## Examples
# Build a release using defaults
mix release
# Pass args to erlexec when running the release
mix release --erl="-env TZ UTC"
# Enable dev mode. Make changes, compile using MIX_ENV=prod
# and execute your release again to pick up the changes
mix release --dev
# Set the verbosity level
mix release --verbosity=[silent|quiet|normal|verbose]
# Do not ask for confirmation to skip missing applications warning
mix release --no-confirm-missing
You may pass any number of arguments as needed. Make sure you pass arguments
using `--key=value`, not `--key value`, as the args may be interpreted incorrectly
otherwise.
"""
@shortdoc "Build a release for the current mix application"
use Mix.Task
import ExUnit.CaptureIO
import ReleaseManager.Utils
alias ReleaseManager.Utils
alias ReleaseManager.Utils.Logger
alias ReleaseManager.Config
alias ReleaseManager.Deps
@_RELXCONF "relx.config"
@_BOOT_FILE "boot"
@_NODETOOL "nodetool"
@_SYSCONFIG "sys.config"
@_VMARGS "vm.args"
@_RELEASE_DEF "release_definition.txt"
@_RELEASES "{{{RELEASES}}}"
@_NAME "{{{PROJECT_NAME}}}"
@_VERSION "{{{PROJECT_VERSION}}}"
@_ERTS_VSN "{{{ERTS_VERSION}}}"
@_ERL_OPTS "{{{ERL_OPTS}}}"
@_LIB_DIRS "{{{LIB_DIRS}}}"
def run(args) do
Mix.Project.compile(args)
if Mix.Project.umbrella? do
config = [umbrella?: true]
for %Mix.Dep{app: app, opts: opts} <- Mix.Dep.Umbrella.loaded do
Mix.Project.in_project(app, opts[:path], config, fn _ -> do_run(args) end)
end
else
do_run(args)
end
end
defp do_run(args) do
# Start with a clean slate
Mix.Tasks.Release.Clean.do_cleanup(:build)
# Collect release configuration
config = parse_args(args)
Logger.notice "Building release with MIX_ENV=#{config.env}."
# Begin release pipeline
config
|> check_applications(args)
|> generate_relx_config
|> generate_sys_config
|> generate_vm_args
|> generate_boot_script
|> execute_before_hooks
|> do_release
|> generate_nodetool
|> generate_install_escript
|> execute_after_hooks
|> update_release_package
|> execute_package_hooks
Logger.info "The release for #{config.name}-#{config.version} is ready!"
Logger.info "You can boot a console running your release with `$ rel/#{config.name}/bin/#{config.name} console`"
end
defp check_applications(%Config{} = config, args) do
case Deps.print_missing_applications(ignore: [:exrm]) do
"" -> config
output ->
IO.puts IO.ANSI.yellow
IO.puts "You have dependencies (direct/transitive) which are not in :applications!"
IO.puts "The following apps should be added to :applications in mix.exs:\n#{output}#{IO.ANSI.reset}\n"
case "--no-confirm-missing" in args do
true ->
config
false ->
msg = IO.ANSI.yellow <> "Continue anyway? Your release may not work as expected if these dependencies are required!"
answer = IO.gets(msg <> " [Yn]: ") |> String.rstrip(?\n)
IO.puts IO.ANSI.reset
case answer =~ ~r/^(Y(es)?)?$/i do
true -> config
false -> abort!()
end
end
end
end
defp generate_relx_config(%Config{name: name, version: version} = config) do
Logger.debug "Generating relx configuration..."
# Get paths
rel_def = rel_file_source_path @_RELEASE_DEF
source = rel_source_path @_RELXCONF
# Get relx.config template contents
relx_config = source |> File.read!
# Get release definition template contents
tmpl = rel_def |> File.read!
# Generate release configuration for historical releases
releases = get_releases(name)
|> Enum.map(fn {rname, rver} -> tmpl |> replace_release_info(rname, rver) end)
|> Enum.join
# Set upgrade flag if this is an upgrade release
config = case releases do
"" -> config
_ -> %{config | :upgrade? => true}
end
elixir_paths = get_elixir_lib_paths() |> Enum.map(&String.to_char_list/1)
lib_dirs = [ '#{Path.join(Mix.Project.build_path, "lib")}', '#{Mix.Project.deps_path}' | elixir_paths ]
# Build release configuration
relx_config = relx_config
|> String.replace(@_RELEASES, releases)
|> String.replace(@_LIB_DIRS, :io_lib.fwrite('~p.\n\n', [{:lib_dirs, lib_dirs}]) |> List.to_string)
# Replace placeholders for current release
relx_config = relx_config |> replace_release_info(name, version)
# Read config as Erlang terms
relx_config = Utils.string_to_terms(relx_config)
# Merge user provided relx.config
user_config_path = rel_dest_path @_RELXCONF
merged = case user_config_path |> File.exists? do
true ->
Logger.debug "Merging custom relx configuration from #{user_config_path |> Path.relative_to_cwd}..."
case Utils.read_terms(user_config_path) do
[] -> relx_config
[{_, _}|_] = user_config -> Utils.merge(relx_config, user_config)
[user_config] when is_list(user_config) -> Utils.merge(relx_config, user_config)
[user_config] -> Utils.merge(relx_config, [user_config])
end
_ ->
relx_config
end
# Save relx config for use later
%{config | :relx_config => merged}
end
defp generate_sys_config(%Config{env: env} = config) do
default_sysconfig = rel_file_source_path @_SYSCONFIG
user_sysconfig = rel_dest_path @_SYSCONFIG
dest = rel_file_dest_path @_SYSCONFIG
Logger.debug "Generating sys.config..."
# Read in current project config
project_conf = load_config(env)
# Merge project config with either the user-provided config, or the default sys.config we provide.
# If a sys.config is provided by the user, it will take precedence over project config. If the
# default sys.config is used, the project config will take precedence instead.
merged = case user_sysconfig |> File.exists? do
true ->
Logger.debug "Merging custom sys.config from #{user_sysconfig |> Path.relative_to_cwd}..."
# User-provided
case user_sysconfig |> Utils.read_terms do
[] -> project_conf
[user_conf] when is_list(user_conf) -> Mix.Config.merge(project_conf, user_conf)
[user_conf] -> Mix.Config.merge(project_conf, [user_conf])
end
_ ->
# Default
[default_conf] = default_sysconfig |> Utils.read_terms
Mix.Config.merge(default_conf, project_conf)
end
# Ensure parent directory exists prior to writing
File.mkdir_p!(dest |> Path.dirname)
# Write the config to disk
dest |> Utils.write_term(merged)
# tighten permissions on sys.config to owner read/write
dest |> File.chmod(0o0600)
# Continue..
config
end
defp generate_vm_args(%Config{version: version, relx_config: relx_config} = config) do
Logger.debug "Generating vm.args..."
vmargs_path = rel_dest_path(@_VMARGS)
vmargs_path = case File.exists?(vmargs_path) do
false ->
src_path = rel_file_source_path(@_VMARGS)
dest_path = rel_file_dest_path(@_VMARGS)
contents = File.read!(src_path) |> String.replace(@_NAME, config.name)
File.write!(dest_path, contents)
dest_path
true ->
vmargs_path
end
overlays = [overlay: [
{:copy, String.to_char_list(vmargs_path), 'releases/#{version}/vm.args'}
]]
# Update configuration to add new overlay for vm.args
updated = Utils.merge(relx_config, overlays)
%{config | :relx_config => updated}
end
defp generate_boot_script(%Config{name: name, version: version, erl: erl_opts} = config) do
erts = extract_erts_version(config)
boot = rel_file_source_path @_BOOT_FILE
winboot = rel_file_source_path "#{@_BOOT_FILE}.bat"
dest = rel_file_dest_path @_BOOT_FILE
windest = rel_file_dest_path "#{@_BOOT_FILE}.bat"
shim = rel_file_source_path "boot_shim"
winshim = rel_file_source_path "boot_shim.bat"
shim_dest = rel_file_dest_path "boot_shim"
winshim_dest = rel_file_dest_path "boot_shim.bat"
Logger.debug "Generating boot script..."
[{boot, dest}, {winboot, windest}, {shim, shim_dest}, {winshim, winshim_dest}]
|> Enum.each(fn {infile, outfile} ->
contents = File.read!(infile)
|> String.replace(@_NAME, name)
|> String.replace(@_VERSION, version)
|> String.replace(@_ERTS_VSN, erts)
|> String.replace(@_ERL_OPTS, erl_opts)
File.write!(outfile, contents)
# Make executable
outfile |> chmod(0o700)
end)
# Continue..
config
end
defp execute_before_hooks(%Config{} = config) do
# Just in case there are plugins which expect relx.config to already
# be persisted, we'll persist it before any are run, and again after each plugin runs
Utils.write_terms(relx_config_path(), config.relx_config)
plugins = ReleaseManager.Plugin.load_all
Enum.reduce plugins, config, fn plugin, conf ->
try do
# Handle the case where a child plugin does not return the configuration
config = case plugin.before_release(conf) do
%Config{} = result -> result
_ -> conf
end
Utils.write_terms(relx_config_path(), config.relx_config)
config
rescue
exception ->
stacktrace = System.stacktrace
Logger.error "Failed to execute before_release hook for #{plugin}!"
reraise exception, stacktrace
end
end
end
defp execute_after_hooks(%Config{} = config) do
plugins = ReleaseManager.Plugin.load_all
Enum.reduce plugins, config, fn plugin, conf ->
try do
# Handle the case where a child plugin does not return the configuration
case plugin.after_release(conf) do
%Config{} = result -> result
_ -> conf
end
rescue
exception ->
stacktrace = System.stacktrace
Logger.error "Failed to execute after_release hook for #{plugin}!"
reraise exception, stacktrace
end
end
end
defp execute_package_hooks(%Config{} = config) do
plugins = ReleaseManager.Plugin.load_all
Enum.reduce plugins, config, fn plugin, conf ->
try do
# Handle the case where a child plugin does not return the configuration
case plugin.after_package(conf) do
%Config{} = result -> result
_ -> conf
end
rescue
exception ->
stacktrace = System.stacktrace
Logger.error "Failed to execute after_package hook for #{plugin}!"
reraise exception, stacktrace
end
end
end
defp do_release(%Config{name: name, version: version, verbosity: verbosity, upgrade?: upgrade?, dev: dev_mode?, env: env} = config) do
Logger.debug "Generating release..."
# Persist relx.config one last time in case it was updated by a plugin
Utils.write_terms(relx_config_path(), config.relx_config)
# If this is an upgrade release, generate an appup
if upgrade? do
# Change mix env for appup generation
with_env env, fn ->
# Generate appup
app = name |> String.to_atom
v1 = get_last_release(name)
v1_path = rel_dest_path [name, "lib", "#{name}-#{v1}"]
v2_path = Mix.Project.compile_path |> String.replace("/ebin", "")
own_path = rel_dest_path "#{name}.appup"
# Look for user's own .appup file before generating one
case own_path |> File.exists? do
true ->
# Copy it to ebin
case File.cp(own_path, Path.join([v2_path, "/ebin", "#{name}.appup"])) do
:ok ->
Logger.info "Using custom .appup located in rel/#{name}.appup"
{:error, reason} ->
Logger.error "Unable to copy custom .appup file: #{reason}"
abort!()
end
_ ->
# No custom .appup found, proceed with autogeneration
case ReleaseManager.Appups.make(app, v1, version, v1_path, v2_path) do
{:ok, _} ->
Logger.info "Generated .appup for #{name} #{v1} -> #{version}"
{:error, reason} ->
Logger.error "Appup generation failed with #{reason}"
abort!()
end
end
end
end
# Do release
try do
logs = capture_io(:stdio, fn ->
result = relx(name, version, :verbose, upgrade?, dev_mode?)
send(self(), result)
end)
receive do
:ok ->
if verbosity == :verbose do
IO.puts(logs)
end
# Clean up template files
Mix.Tasks.Release.Clean.do_cleanup(:relfiles)
# Continue..
config
{:error, message} ->
IO.puts(logs)
Logger.error "ERROR: #{inspect message}"
abort!()
end
catch
err ->
Logger.error "#{IO.inspect err}"
Logger.error "Failed to build release package! Try running with `--verbosity=verbose` to see debugging info!"
abort!()
end
end
defp generate_nodetool(%Config{name: name} = config) do
Logger.debug "Generating nodetool..."
nodetool = rel_file_source_path @_NODETOOL
dest = rel_dest_path [name, "bin", @_NODETOOL]
# Copy
File.cp! nodetool, dest
# Make executable
dest |> chmod(0o700)
# Continue..
config
end
defp generate_install_escript(%Config{name: name} = config) do
escript = rel_file_source_path "install_upgrade.escript"
dest = rel_dest_path [name, "bin", "install_upgrade.escript"]
File.cp! escript, dest
config
end
defp update_release_package(%Config{dev: true} = config), do: config
defp update_release_package(%Config{name: name, version: version, relx_config: relx_config} = config) do
Logger.debug "Packaging release..."
# Delete original release package
tarball = rel_dest_path [name, "#{name}-#{version}.tar.gz"]
File.rm! tarball
# Make sure we have a start.boot file for upgrades/downgrades
source_boot = rel_dest_path([name, "releases", version, "#{name}.boot"])
dest_boot = rel_dest_path([name, "releases", version, "start.boot"])
File.cp! source_boot, dest_boot
# Get include_erts value from relx_config
include_erts = Keyword.get(relx_config, :include_erts, true)
erts = "erts-#{extract_erts_version(config)}"
extras = case include_erts do
false -> []
_ -> [{'#{erts}', '#{rel_dest_path([name, erts])}'}]
end
# Re-package release with modifications
file_list = File.ls!(rel_dest_path(name))
|> Enum.reject(fn n -> n in [erts, "tmp"] end)
|> Enum.map(fn
"releases" -> [Path.join("releases", "RELEASES"),
Path.join("releases", "start_erl.data") |
File.ls!(rel_dest_path([name, "releases", version]))
|> Enum.reject(&(String.ends_with?(&1, ".tar.gz")))
|> Enum.map(fn n -> Path.join(["releases", version, n]) end)]
"lib" -> File.ls!(rel_dest_path([name, "lib"]))
|> Enum.reject(fn n -> String.starts_with?(n, "#{name}-") && !String.ends_with?(n, "-#{version}") end)
|> Enum.map(fn n -> Path.join("lib", n) end)
n -> [n]
end)
|> List.flatten
|> Enum.map(&({'#{&1}', '#{rel_dest_path([name, &1])}'}))
|> Enum.concat(extras)
# Create archive
release_tarball = rel_dest_path([name, "releases", version, "#{name}.tar.gz"])
:ok = :erl_tar.create(
'#{tarball}',
file_list,
[:compressed]
)
# Clean up
File.cp! tarball, release_tarball
File.rm_rf! tarball
# Continue..
%{config | package: release_tarball}
end
defp parse_args(argv) do
{args, _, _} = OptionParser.parse(argv)
defaults = %Config{
name: Mix.Project.config |> Keyword.get(:app) |> Atom.to_string,
version: Mix.Project.config |> Keyword.get(:version),
env: Mix.env
}
Enum.reduce args, defaults, fn arg, config ->
case arg do
{:verbosity, verbosity} ->
verbosity = String.to_atom(verbosity)
Logger.configure(verbosity)
%{config | :verbosity => verbosity}
{key, value} ->
Map.put(config, key, value)
end
end
end
defp replace_release_info(template, name, version) do
template
|> String.replace(@_NAME, name)
|> String.replace(@_VERSION, version)
end
defp extract_erts_version(%Config{relx_config: relx_config}) do
include_erts = Keyword.get(relx_config, :include_erts, true)
case include_erts do
true -> :erlang.system_info(:version) |> IO.iodata_to_binary
false -> ""
path ->
case File.ls("#{path}") do
{:error, _} -> ""
{:ok, entries} ->
erts = entries |> Enum.find(fn
<<"erts-", _version::binary>> -> true
_ -> false
end)
case erts do
<<"erts-", version::binary>> -> version
_ -> ""
end
end
end
end
def relx_config_path do
path = rel_file_dest_path @_RELXCONF
# Ensure destination base path exists
path |> Path.dirname |> File.mkdir_p!
path
end
end