-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathgenerate_release.ex
291 lines (240 loc) · 10.3 KB
/
generate_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
# Copyright(c) 2015-2024 ACCESS CO., LTD. All rights reserved.
defmodule Mix.Tasks.AntikytheraCore.GenerateRelease do
@shortdoc "Generates a new release tarball for antikythera instance using `mix release`"
@moduledoc """
#{@shortdoc}.
Notes on the implementation details of this task:
- Release generation is basically done under `rel_erlang-*/`.
During tests (i.e. when `ANTIKYTHERA_COMPILE_ENV=local`), files are generated under `rel_local_erlang-*/`.
- Erlang/OTP major version number is included in the path in order to distinguish artifacts generated by different OTP releases.
No binary compatibility is maintained by major version release of Erlang/OTP due to precompilation of Elixir's regex sigils.
For more details see [documentation for Regex module](https://hexdocs.pm/elixir/Regex.html#module-precompilation).
- Generated releases are placed under `rel(_local)_erlang-*/<antikythera_instance>/releases/`.
- If no previous releases found, this task generates a new release from scratch (i.e. without relup).
If any previous releases exist, relup file to upgrade from the latest existing release to the current release is also generated.
- Making a new release tarball consists of the following steps:
- Preparation
- Template files (`rel/*.eex`) used by `mix release` to create `env.sh` and `vm.args`.
- Configuration for `mix release` provided by `#{__MODULE__}.config_for_mix_release/0`.
- Release generation
- Ensure that source files are compiled and `<antikythera_instance>.app` is up-to-date.
- If a previous release is found, generate `*.appup` and `relup` files.
- `*.appup` files are generated before assembling.
- `relup` file is generated after assembling.
- Generate a new release by `:assemble` step of `mix release`.
- Generate `RELEASES` file, which is required by `:release_handler`.
- Cleanup
- Make a tarball by `:tar` step of `mix release`, then move it to the version directory.
"""
use Mix.Task
prefix = if Antikythera.Env.compile_env() == :local, do: "rel_local", else: "rel"
@release_output_dir_basename prefix <> "_erlang-#{System.otp_release()}"
@antikythera_repo_rel_templates_path Path.expand(Path.join([__DIR__, "..", "..", "rel"]))
@impl Mix.Task
def run(_args) do
Mix.Project.get!()
config = Mix.Project.config()
instance_app = config[:app]
if instance_app == :antikythera do
Mix.raise("Application name of an antikythera instance must not be `:antikythera`")
end
if config[:releases][instance_app] == nil do
Mix.raise("""
Configuration for `mix release #{instance_app}` is required.
See `#{__MODULE__}.config_for_mix_release/0` for more details.
""")
end
Mix.Task.run("compile")
# <antikythera_instance>.app might be stale in some situations
# (e.g. when antikythera instance's version is updated by an empty commit).
if read_app_version_from_app_file(instance_app) != config[:version] do
Mix.Task.rerun("compile.app", ["--force"])
end
release_name_str = Atom.to_string(instance_app)
:ok = Mix.Task.run("release", [release_name_str])
end
@doc """
Generates a release configuration required by `mix #{Mix.Task.task_name(__MODULE__)}`.
A release name for the configuration must be identical to the antikythera instance name.
The following is an example configuration for `antikythera_instance_example`:
```
def project do
[
releases: [
antikythera_instance_example: &#{__MODULE__}.config_for_mix_release/0,
...
]
]
end
```
"""
def config_for_mix_release() do
# Release name to generate is identical to the instance app name.
release_name_str = Atom.to_string(instance_app())
[
path: Path.join(@release_output_dir_basename, release_name_str),
include_executables_for: [:unix],
rel_templates_path: @antikythera_repo_rel_templates_path,
steps: [&before_assemble/1, :assemble, &after_assemble/1, :tar, &move_tarball/1],
# `AntikytheraCore.Release.Appup` detects changes of modules including their metadata.
# To include module's metadata in a BEAM file, we set `strip_beams: false` here.
strip_beams: false,
# Elixir checks the release existence by looking for only a directory for its version,
# but we should check the release existence by looking for its release package (tarball).
# To skip Elixir's check and proceed to our check, we set `overwrite: true` here.
overwrite: true
]
end
defp instance_app(), do: Mix.Project.config()[:app]
defp before_assemble(%Mix.Release{version: version} = release) do
existing_versions = get_existing_release_versions(release)
cond do
existing_versions == [] ->
Mix.shell().info("Generating release #{version} without upgrade ...")
release
version in existing_versions ->
Mix.shell().info("Version '#{version}' already exists.")
%Mix.Release{release | steps: []}
true ->
# Only supporting upgrade from the latest existing version.
latest_existing = Enum.max(existing_versions)
if latest_existing >= version do
Mix.raise(
"Latest existing version (#{latest_existing}) " <>
"must precede the current version (#{version})!"
)
end
Mix.shell().info(
"Generating release #{version} with upgrade instruction from #{latest_existing} ..."
)
generate_appup_files(release, latest_existing)
put_latest_existing_version(release, latest_existing)
end
end
defp get_existing_release_versions(%Mix.Release{} = release) do
case File.ls(releases_dir(release)) do
{:error, _} ->
[]
{:ok, release_version_dirs_and_other_files} ->
Enum.filter(release_version_dirs_and_other_files, &version_with_tarball?(&1, release))
end
end
defp version_with_tarball?(version, %Mix.Release{} = release) do
with true <- Antikythera.VersionStr.valid?(version) do
release
|> tarball_path(version)
|> File.exists?()
end
end
defp generate_appup_files(%Mix.Release{} = release, prev_release_version) do
prev_app_versions = read_app_versions_from_rel_file(release, prev_release_version)
# We don't support upgrading Erlang/Elixir's core applications.
upgradable_apps = [instance_app() | Mix.Project.deps_apps()]
Enum.each(upgradable_apps, &generate_appup_if_needed(&1, prev_app_versions[&1], release))
end
defp generate_appup_if_needed(_app, nil, _release), do: :ok
defp generate_appup_if_needed(app, prev_version, release) do
case read_app_version_from_app_file(app) do
^prev_version ->
:ok
version ->
dir = Application.app_dir(app)
prev_dir = lib_dir_for_app(release, app, prev_version)
:ok = AntikytheraCore.Release.Appup.make(app, prev_version, version, prev_dir, dir)
Mix.shell().info("Generated #{app}.appup.")
end
end
defp after_assemble(%Mix.Release{} = release) do
create_RELEASES(release)
case get_latest_existing_version(release) do
nil ->
release
prev_version ->
make_relup(release, prev_version)
release
end
end
# credo:disable-for-next-line Credo.Check.Readability.FunctionNames
defp create_RELEASES(%Mix.Release{version: version} = release) do
releases_dir = String.to_charlist(releases_dir(release))
rel_file = rel_file_path(release, version)
:ok = :release_handler.create_RELEASES('.', releases_dir, rel_file, [])
Mix.shell().info("Generated RELEASES.")
end
defp make_relup(
%Mix.Release{version: version, version_path: version_path} = release,
prev_version
) do
current = make_name_for_relup(release, version)
up_from = make_name_for_relup(release, prev_version)
code_paths =
Enum.map(
read_app_versions_from_rel_file(release, version) ++
read_app_versions_from_rel_file(release, prev_version),
fn {app, app_version} -> code_path_for_app(release, app, app_version) end
)
outdir = String.to_charlist(version_path)
:ok = :systools.make_relup(current, [up_from], [up_from], path: code_paths, outdir: outdir)
Mix.shell().info("Generated relup from #{prev_version} to #{version}.")
end
defp move_tarball(%Mix.Release{name: name, path: path, version: version} = release) do
src = Path.join(path, "#{name}-#{version}.tar.gz")
dst = tarball_path(release, version)
File.copy!(src, dst)
File.rm!(src)
release
end
defp read_app_version_from_app_file(app) do
app
|> Application.app_dir()
|> AntikytheraCore.Version.read_from_app_file(app)
end
defp releases_dir(%Mix.Release{path: path}), do: Path.join(path, "releases")
defp tarball_path(%Mix.Release{name: name} = release, version) do
release
|> releases_dir()
|> Path.join(version)
|> Path.join("#{name}.tar.gz")
end
defp rel_file_path(%Mix.Release{name: name} = release, version) do
release
|> releases_dir()
|> Path.join(version)
|> Path.join("#{name}.rel")
|> String.to_charlist()
end
defp read_app_versions_from_rel_file(%Mix.Release{name: name} = release, version) do
release_name_chars = Atom.to_charlist(name)
{:ok, [{:release, {^release_name_chars, _}, {:erts, _}, apps}]} =
release
|> rel_file_path(version)
|> :file.consult()
Keyword.new(apps, fn
{app, app_version} -> {app, List.to_string(app_version)}
{app, app_version, _} -> {app, List.to_string(app_version)}
end)
end
defp make_name_for_relup(%Mix.Release{} = release, version) do
release
|> rel_file_path(version)
|> :filename.rootname('.rel')
end
defp lib_dir_for_app(%Mix.Release{path: path}, app, app_version) do
Path.join([path, "lib", "#{app}-#{app_version}"])
end
defp code_path_for_app(%Mix.Release{} = release, app, app_version) do
release
|> lib_dir_for_app(app, app_version)
|> Path.join("ebin")
|> String.to_charlist()
end
defp put_latest_existing_version(%Mix.Release{options: options} = release, latest_existing) do
%Mix.Release{
release
| options: Keyword.put(options, :latest_existing, latest_existing)
}
end
defp get_latest_existing_version(%Mix.Release{options: options}) do
Keyword.get(options, :latest_existing)
end
end