Skip to content

Commit 05fd5a5

Browse files
committedJul 15, 2016
Version 0.1.0
0 parents  commit 05fd5a5

34 files changed

+2776
-0
lines changed
 

‎.gitignore

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps
9+
10+
# Where 3rd-party dependencies like ExDoc output generated docs.
11+
/doc
12+
13+
# If the VM crashes, it generates a dump, let's ignore it too.
14+
erl_crash.dump
15+
16+
# Also ignore archive artifacts (built via "mix archive.build").
17+
*.ez

‎LICENSE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2016 Paul Schoenfelder
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

‎README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Bundler
2+
3+
So called because building releases involves "bundling" up the application and it's resources into
4+
a single package for deployment.
5+
6+
This is a implementation of release building functionality for the Elixir standard library/tooling,
7+
as a pluggable dependency. I'm using this to prototype the native implementation of this functionality
8+
prior to merging into Elixir proper.
9+
10+
## Installation
11+
12+
```elixir
13+
defp deps do
14+
[{:bundler, "~> 0.1"}]
15+
end
16+
```
17+
18+
Just add as a mix dependency and use `mix release`. This is a replacement for exrm, but is in beta at this time.
19+
20+
If you are new to releases, please review the [documentation](https://hexdocs.pm/bundler).
21+
22+
## TODO
23+
24+
- Upgrades/downgrades
25+
- Read-only filesystems
26+
- CLI tooling
27+
- Documentation
28+
- Code cleanup
29+
30+
## License
31+
32+
MIT. See the `LICENSE.md` in this repository for more details.

‎docs/Common Issues.md

Whitespace-only changes.

‎docs/Configuration.md

Whitespace-only changes.

‎docs/Getting Started.md

Whitespace-only changes.

‎docs/Upgrades and Downgrades.md

Whitespace-only changes.

‎docs/Walkthrough.md

Whitespace-only changes.

‎lib/bundler/lib/logger.ex

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule Bundler.Utils.Logger do
2+
3+
def configure(verbosity) when is_atom(verbosity) do
4+
Application.put_env(:bundler, :verbosity, verbosity)
5+
end
6+
7+
@doc "Print an informational message in cyan"
8+
def debug(message), do: log(:debug, "#{IO.ANSI.cyan}==> #{message}#{IO.ANSI.reset}")
9+
@doc "Print an informational message in bright cyan"
10+
def info(message), do: log(:info, "#{IO.ANSI.bright}#{IO.ANSI.cyan}==> #{message}#{IO.ANSI.reset}")
11+
@doc "Print a success message in bright green"
12+
def success(message), do: log(:warn, "#{IO.ANSI.bright}#{IO.ANSI.green}==> #{message}#{IO.ANSI.reset}")
13+
@doc "Print a warning message in yellow"
14+
def warn(message), do: log(:warn, "#{IO.ANSI.yellow}==> #{message}#{IO.ANSI.reset}")
15+
@doc "Print a notice in yellow"
16+
def notice(message), do: log(:notice, "#{IO.ANSI.yellow}#{message}#{IO.ANSI.reset}")
17+
@doc "Print an error message in red"
18+
def error(message), do: log(:error, "#{IO.ANSI.red}==> #{message}#{IO.ANSI.reset}")
19+
20+
defp log(level, message),
21+
do: log(level, Application.get_env(:bundler, :verbosity, :normal), message)
22+
23+
defp log(_, :verbose, message), do: IO.puts message
24+
defp log(:error, :silent, message), do: IO.puts message
25+
defp log(_level, :silent, _message), do: :ok
26+
defp log(:debug, :quiet, _message), do: :ok
27+
defp log(:debug, :normal, _message), do: :ok
28+
defp log(:debug, _verbosity, message), do: IO.puts message
29+
defp log(:info, :quiet, _message), do: :ok
30+
defp log(:info, _verbosity, message), do: IO.puts message
31+
defp log(_level, _verbosity, message), do: IO.puts message
32+
33+
end

‎lib/bundler/tasks/clean.ex

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
defmodule Mix.Tasks.Release.Clean do
2+
@moduledoc """
3+
Cleans release artifacts from the current project.
4+
5+
## Examples
6+
7+
# Cleans files associated with the latest release
8+
mix release.clean
9+
10+
# Remove all release files
11+
mix release.clean --implode
12+
13+
# Remove all release files, and do it without confirmation
14+
mix release.clean --implode --no-confirm
15+
16+
"""
17+
@shortdoc "Clean up any release-related files"
18+
use Mix.Task
19+
alias Bundler.Utils.Logger
20+
21+
def run(args) do
22+
Logger.configure(:debug)
23+
24+
# make sure loadpaths are updated
25+
Mix.Task.run("loadpaths", [])
26+
27+
opts = parse_args(args)
28+
29+
implode? = Keyword.get(opts, :implode, false)
30+
no_confirm? = Keyword.get(opts, :no_confirm, false)
31+
cond do
32+
implode? && no_confirm? ->
33+
clean_all!
34+
implode? && confirm_implode? ->
35+
clean_all!
36+
:else ->
37+
clean!
38+
end
39+
end
40+
41+
defp clean_all! do
42+
Logger.debug "Cleaning all releases.."
43+
unless File.exists?("rel") do
44+
Logger.warn "No rel directory found! Nothing to do."
45+
exit(:normal)
46+
end
47+
File.rm_rf!("rel")
48+
Logger.success "Clean successful!"
49+
end
50+
51+
defp clean! do
52+
# load release configuration
53+
Logger.debug "Cleaning last release.."
54+
55+
unless File.exists?("rel/config.exs") do
56+
Logger.warn "No config file found! Nothing to do."
57+
exit(:normal)
58+
end
59+
60+
config = Mix.Releases.Config.read!("rel/config.exs")
61+
releases = config.releases
62+
# build release
63+
paths = Path.wildcard(Path.join("rel", "*"))
64+
for release <- releases, Path.join("rel", "#{release.name}") in paths do
65+
Logger.notice " Removing release #{release.name}:#{release.version}"
66+
clean_release(release, Path.join("rel", "#{release.name}"))
67+
end
68+
Logger.success "Clean successful!"
69+
end
70+
71+
defp clean_release(release, path) do
72+
# Remove erts
73+
erts_paths = Path.wildcard(Path.join(path, "erts-*"))
74+
for erts <- erts_paths do
75+
File.rm_rf!(erts)
76+
end
77+
# Remove lib
78+
File.rm_rf!(Path.join(path, "lib"))
79+
# Remove releases/start_erl.data
80+
File.rm(Path.join([path, "releases", "start_erl.data"]))
81+
# Remove current release version
82+
File.rm_rf!(Path.join([path, "releases", "#{release.version}"]))
83+
end
84+
85+
defp parse_args(argv) do
86+
{overrides, _} = OptionParser.parse!(argv, [implode: :boolean, no_confirm: :boolean])
87+
Keyword.merge([implode: false, no_confirm: false], overrides)
88+
end
89+
90+
defp confirm_implode? do
91+
IO.puts IO.ANSI.yellow
92+
msg = """
93+
THIS WILL REMOVE ALL RELEASES AND RELATED CONFIGURATION!
94+
Are you absolutely sure you want to proceed?
95+
"""
96+
answer = IO.gets(msg <> " [Yn]: ") |> String.rstrip(?\n)
97+
IO.puts IO.ANSI.reset
98+
answer =~ ~r/^(Y(es)?)?$/i
99+
end
100+
101+
end

‎lib/bundler/tasks/init.ex

+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
defmodule Mix.Tasks.Release.Init do
2+
@moduledoc """
3+
Prepares a new project for use with releases.
4+
This simply creates a `rel` directory in the project root,
5+
and creates a basic initial configuration file in `rel/config.exs`.
6+
7+
After running this, you can build a release right away with `mix release`,
8+
but it is recommended you review the config file to understand it's contents.
9+
10+
## Examples
11+
12+
# Initialize releases, with a fully commented config file
13+
mix release.init
14+
15+
# Initialize releases, but with no comments in the config file
16+
mix release.init --no-doc
17+
18+
# For umbrella projects, generate a config where each app
19+
# in the umbrella is it's own release, rather than all
20+
# apps under a single release
21+
mix release.init --release-per-app
22+
23+
# Name the release, by default the current application name
24+
# will be used, or in the case of umbrella projects, the name
25+
# of the directory in which the umbrella project resides, with
26+
# invalid characters replaced or stripped out.
27+
mix release.init --name foobar
28+
29+
"""
30+
@shortdoc "initialize a new release configuration"
31+
use Mix.Task
32+
alias Mix.Releases.Utils
33+
alias Bundler.Utils.Logger
34+
35+
def run(args) do
36+
Logger.configure(:debug)
37+
38+
# Generate template bindings based on type of project and task opts
39+
opts = parse_args(args)
40+
bindings = case Mix.Project.umbrella? do
41+
true -> get_umbrella_bindings(opts)
42+
false -> get_standard_bindings(opts)
43+
end
44+
# Create /rel
45+
File.mkdir_p!("rel")
46+
# Generate config.exs
47+
config = Utils.template(:example_config, bindings)
48+
# Save config.exs to /rel
49+
File.write!(Path.join("rel", "config.exs"), config)
50+
51+
IO.puts(
52+
IO.ANSI.cyan <>
53+
"\nAn example config file has been placed in rel/config.exs, review it,\n" <>
54+
"make edits as needed/desired, and then run `mix release` to build the release" <>
55+
IO.ANSI.reset
56+
)
57+
end
58+
59+
@defaults [no_doc: false,
60+
release_per_app: false]
61+
defp parse_args(argv) do
62+
{overrides, _} = OptionParser.parse!(argv,
63+
strict: [no_doc: :boolean,
64+
release_per_app: :boolean])
65+
Keyword.merge(@defaults, overrides)
66+
end
67+
68+
defp get_umbrella_bindings(opts) do
69+
apps_path = Keyword.get(Mix.Project.config, :apps_path)
70+
apps_paths = File.ls!(apps_path)
71+
apps = apps_paths
72+
|> Enum.map(&Path.join(apps_path, &1))
73+
|> Enum.map(fn app_path ->
74+
Mix.Project.in_project(String.to_atom(Path.basename(app_path)), app_path, fn mixfile ->
75+
{Keyword.get(mixfile.project, :app), :permanent}
76+
end)
77+
end)
78+
no_doc? = Keyword.get(opts, :no_doc, false)
79+
release_per_app? = Keyword.get(opts, :release_per_app, false)
80+
if release_per_app? do
81+
[no_docs: no_doc?,
82+
releases: Enum.map(apps, fn {app, start_type} ->
83+
[release_name: app, release_applications: [{app, start_type}]]
84+
end)]
85+
else
86+
release_name = String.replace(Path.basename(File.cwd!), "-", "_")
87+
[no_docs: no_doc?,
88+
releases: [
89+
[release_name: String.to_atom(release_name),
90+
release_applications: apps]]]
91+
end
92+
end
93+
94+
defp get_standard_bindings(opts) do
95+
app = Keyword.get(Mix.Project.config, :app)
96+
no_doc? = Keyword.get(opts, :no_doc, false)
97+
[no_docs: no_doc?,
98+
releases: [
99+
[release_name: app,
100+
release_applications: [{app, :permanent}]]]]
101+
end
102+
end

‎lib/bundler/tasks/release.ex

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
defmodule Mix.Tasks.Release do
2+
@moduledoc """
3+
Build a release for the current mix application.
4+
5+
## Examples
6+
7+
# Build a release using defaults
8+
mix release
9+
10+
# Pass args to erlexec when running the release
11+
mix release --erl="-env TZ UTC"
12+
13+
# Enable dev mode. Make changes, compile using MIX_ENV=prod
14+
# and execute your release again to pick up the changes
15+
mix release --dev
16+
17+
# Mute logging output
18+
mix release --silent
19+
20+
# Quiet logging output
21+
mix release --quiet
22+
23+
# Verbose logging output
24+
mix release --verbose
25+
26+
# Do not package release, just assemble it
27+
mix release --no-tar
28+
29+
"""
30+
@shortdoc "Build a release for the current mix application"
31+
use Mix.Task
32+
alias Mix.Releases.Config
33+
alias Bundler.Utils.Logger
34+
35+
def run(args) do
36+
# Parse options
37+
opts = parse_args(args)
38+
verbosity = Keyword.get(opts, :verbosity)
39+
Logger.configure(verbosity)
40+
41+
# make sure loadpaths are updated
42+
Mix.Task.run("loadpaths", [])
43+
44+
# load release configuration
45+
Logger.debug "Loading configuration.."
46+
config = Mix.Releases.Config.read!("rel/config.exs")
47+
48+
# Apply override options
49+
config = case Keyword.get(opts, :dev_mode) do
50+
nil -> config
51+
m -> %{config | :dev_mode => m}
52+
end
53+
config = case Keyword.get(opts, :erl_opts) do
54+
nil -> config
55+
o -> %{config | :erl_opts => o}
56+
end
57+
no_tar? = Keyword.get(opts, :no_tar)
58+
59+
# build release
60+
Logger.info "Assembling release.."
61+
case {Mix.Releases.Assembler.assemble(config), no_tar?} do
62+
{{:ok, %Config{:selected_release => release}}, true} ->
63+
print_success(release.name)
64+
{{:ok, %Config{:selected_release => release} = config}, false} ->
65+
Logger.info "Packaging release.."
66+
case Mix.Releases.Archiver.archive(config) do
67+
:ok ->
68+
print_success(release.name)
69+
other ->
70+
Logger.error "Problem generating release tarball:\n " <>
71+
"#{inspect other}"
72+
end
73+
{{:error, reason},_} ->
74+
Logger.error "Failed to build release:\n " <>
75+
"#{inspect reason}"
76+
end
77+
end
78+
79+
defp print_success(app) do
80+
Logger.success "Release successfully built!\n " <>
81+
"You can run it in one of the following ways:\n " <>
82+
"Interactive: rel/#{app}/bin/#{app} console\n " <>
83+
"Foreground: rel/#{app}/bin/#{app} foreground\n " <>
84+
"Daemon: rel/#{app}/bin/#{app} start"
85+
end
86+
87+
defp parse_args(argv) do
88+
switches = [silent: :boolean, quiet: :boolean, verbose: :boolean,
89+
dev: :boolean, erl: :string, no_tar: :boolean]
90+
{overrides, _} = OptionParser.parse!(argv, switches)
91+
verbosity = :normal
92+
verbosity = cond do
93+
Keyword.get(overrides, :verbose, false) -> :verbose
94+
Keyword.get(overrides, :quiet, false) -> :quiet
95+
Keyword.get(overrides, :silent, false) -> :silent
96+
:else -> verbosity
97+
end
98+
[verbosity: verbosity,
99+
dev_mode: Keyword.get(overrides, :dev),
100+
erl_opts: Keyword.get(overrides, :erl),
101+
no_tar: Keyword.get(overrides, :no_tar, false)]
102+
end
103+
end

‎lib/mix/lib/releases/appups.ex

+229
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
defmodule Mix.Releases.Appup do
2+
@moduledoc """
3+
This module is responsible for generating appups between two releases.
4+
"""
5+
6+
@type app :: atom
7+
@type version_str :: String.t
8+
@type path_str :: String.t
9+
10+
@type appup_ver :: char_list
11+
@type instruction :: {:add_module, module} |
12+
{:delete_module, module} |
13+
{:update, module, :supervisor | {:advanced, [term]}} |
14+
{:load_module, module}
15+
@type upgrade_instructions :: [{appup_ver, instruction}]
16+
@type downgrade_instructions :: [{appup_ver, instruction}]
17+
@type appup :: {appup_ver, upgrade_instructions, downgrade_instructions}
18+
19+
20+
@doc """
21+
Generate a .appup for the given application, start version, and upgrade version.
22+
23+
## Parameters
24+
25+
- `application`: the application name as an atom
26+
- `v1`: the previous version, such as "0.0.1"
27+
- `v2`: the new version, such as "0.0.2"
28+
- `v1_path`: the path to the v1 artifacts (rel/<app>/lib/<app>-0.0.1)
29+
- `v2_path`: the path to the v2 artifacts (_build/prod/lib/<app>)
30+
31+
"""
32+
@spec make(app, version_str, version_str, path_str, path_str) :: {:ok, appup} | {:error, term}
33+
def make(application, v1, v2, v1_path, v2_path) do
34+
v1_dotapp =
35+
v1_path
36+
|> Path.join("/ebin/")
37+
|> Path.join(Atom.to_string(application) <> ".app")
38+
|> String.to_char_list
39+
v2_dotapp =
40+
v2_path
41+
|> Path.join("/ebin/")
42+
|> Path.join(Atom.to_string(application) <> ".app")
43+
|> String.to_char_list
44+
45+
case :file.consult(v1_dotapp) do
46+
{:ok, [{:application, ^application, v1_props}]} ->
47+
consulted_v1_vsn = vsn(v1_props)
48+
case consulted_v1_vsn === v1 do
49+
true ->
50+
case :file.consult(v2_dotapp) do
51+
{:ok, [{:application, ^application, v2_props}]} ->
52+
consulted_v2_vsn = vsn(v2_props)
53+
case consulted_v2_vsn === v2 do
54+
true ->
55+
appup = make_appup(v1, v1_path, v1_props, v2, v2_path, v2_props)
56+
{:ok, appup}
57+
false ->
58+
{:error, {:mismatched_versions, :newer, expected: v2, got: consulted_v2_vsn}}
59+
end
60+
_ ->
61+
{:error, {:invalid_dotapp, v2_dotapp}}
62+
end
63+
false ->
64+
{:error, {:mismatched_versions, :older, expected: v1, got: consulted_v1_vsn}}
65+
end
66+
_ ->
67+
{:error, {:invalid_dotapp, v1_dotapp}}
68+
end
69+
end
70+
71+
defp make_appup(v1, v1_path, _v1_props, v2, v2_path, _v2_props) do
72+
v1 = String.to_char_list(v1)
73+
v2 = String.to_char_list(v2)
74+
v1_path = String.to_char_list(Path.join(v1_path, "ebin"))
75+
v2_path = String.to_char_list(Path.join(v2_path, "ebin"))
76+
77+
{deleted, added, changed} = :beam_lib.cmp_dirs(v1_path, v2_path)
78+
79+
{v2, # New version
80+
[{v1, # Upgrade instructions from version v1
81+
generate_instructions(:added, added) ++
82+
generate_instructions(:changed, changed) ++
83+
generate_instructions(:deleted, deleted)}],
84+
[{v1, # Downgrade instructions to version v1
85+
generate_instructions(:deleted, deleted) ++
86+
generate_instructions(:changed, changed) ++
87+
generate_instructions(:added, deleted)}]}
88+
end
89+
90+
# For modules which have changed, we must make sure
91+
# that they are loaded/updated in such an order that
92+
# modules they depend upon are loaded/updated first,
93+
# where possible (due to cyclic dependencies, this is
94+
# not always feasible). After generating the instructions,
95+
# we perform a best-effort topological sort of the modules
96+
# involved, such that an optimal ordering of the instructions
97+
# is generated
98+
defp generate_instructions(:changed, files) do
99+
files
100+
|> Enum.map(&generate_instruction(:changed, &1))
101+
|> topological_sort
102+
end
103+
defp generate_instructions(type, files) do
104+
Enum.map(files, &generate_instruction(type, &1))
105+
end
106+
107+
defp generate_instruction(:added, file), do: {:add_module, module_name(file)}
108+
defp generate_instruction(:deleted, file), do: {:delete_module, module_name(file)}
109+
defp generate_instruction(:changed, {v1_file, v2_file}) do
110+
module_name = module_name(v1_file)
111+
attributes = beam_attributes(v1_file)
112+
exports = beam_exports(v1_file)
113+
imports = beam_imports(v2_file)
114+
is_supervisor = is_supervisor?(attributes)
115+
is_special_proc = is_special_process?(exports)
116+
depends_on = imports
117+
|> Enum.map(fn {m,_f,_a} -> m end)
118+
|> Enum.uniq
119+
generate_instruction_advanced(module_name, is_supervisor, is_special_proc, depends_on)
120+
end
121+
122+
defp beam_attributes(file) do
123+
{:ok, {_, [attributes: attributes]}} = :beam_lib.chunks(file, [:attributes])
124+
attributes
125+
end
126+
127+
defp beam_imports(file) do
128+
{:ok, {_, [imports: imports]}} = :beam_lib.chunks(file, [:imports])
129+
imports
130+
end
131+
132+
defp beam_exports(file) do
133+
{:ok, {_, [exports: exports]}} = :beam_lib.chunks(file, [:exports])
134+
exports
135+
end
136+
137+
defp is_special_process?(exports) do
138+
Keyword.get(exports, :system_code_change) == 4 ||
139+
Keyword.get(exports, :code_change) == 3
140+
end
141+
142+
defp is_supervisor?(attributes) do
143+
behaviours = Keyword.get(attributes, :behavior, []) ++
144+
Keyword.get(attributes, :behaviour, [])
145+
(:supervisor in behaviours) || (Supervisor in behaviours)
146+
end
147+
148+
# supervisor
149+
defp generate_instruction_advanced(m, true, _is_special, []), do: {:update, m, :supervisor}
150+
defp generate_instruction_advanced(m, true, _is_special, dep_mods), do: {:update, m, :supervisor, dep_mods}
151+
# special process (i.e. exports code_change/3 or system_code_change/4)
152+
defp generate_instruction_advanced(m, _is_sup, true, []), do: {:update, m, {:advanced, []}}
153+
defp generate_instruction_advanced(m, _is_sup, true, dep_mods), do: {:update, m, {:advanced, []}, dep_mods}
154+
# non-special process (i.e. neither code_change/3 nor system_code_change/4 are exported)
155+
defp generate_instruction_advanced(m, _is_sup, false, []), do: {:load_module, m}
156+
defp generate_instruction_advanced(m, _is_sup, false, dep_mods), do: {:load_module, m, dep_mods}
157+
158+
# This "topological" sort is not truly topological, since module dependencies
159+
# are represented as a directed, cyclic graph, and it is not actually
160+
# possible to sort such a graph due to the cycles which occur. However, one
161+
# can "break" loops, until one reaches a point that the graph becomes acyclic,
162+
# and those topologically sortable. That's effectively what happens here:
163+
# we perform the sort, breaking loops where they exist by attempting to
164+
# weight each of the two dependencies based on the number of outgoing dependencies
165+
# they have, where the fewer number of outgoing dependencies always comes first.
166+
# I have experimented with various different approaches, including algorithms for
167+
# feedback arc sets, and none appeared to work as well as the one below. I'm definitely
168+
# open to better algorithms, because I don't particularly like this one.
169+
defp topological_sort(instructions) do
170+
mods = Enum.map(instructions, fn i-> elem(i, 1) end)
171+
instructions
172+
|> Enum.sort(&do_sort_instructions(mods, &1, &2))
173+
|> Enum.map(fn
174+
{:update, _, _} = i -> i
175+
{:load_module, _} = i -> i
176+
{:update, m, type, deps} ->
177+
{:update, m, type, Enum.filter(deps, fn ^m -> false; d -> d in mods end)}
178+
{:load_module, m, deps} ->
179+
{:load_module, m, Enum.filter(deps, fn ^m -> false; d -> d in mods end)}
180+
end)
181+
end
182+
defp do_sort_instructions(_, {:update, a, _}, {:update, b, _}), do: a > b
183+
defp do_sort_instructions(_, {:update, _, _}, {:update, _, _, _}), do: true
184+
defp do_sort_instructions(_, {:update, _, _}, {:load_module, _, _}), do: false
185+
defp do_sort_instructions(_, {:load_module, a}, {:load_module, b}), do: a > b
186+
defp do_sort_instructions(_, {:load_module, _}, {:update, _, _, _}), do: true
187+
defp do_sort_instructions(_, {:load_module, _}, {:load_module, _, _}), do: true
188+
defp do_sort_instructions(mods, a, b) do
189+
am = elem(a, 1)
190+
bm = elem(b, 1)
191+
ad = extract_deps(a)
192+
bd = extract_deps(b)
193+
do_sort_instructions(mods, am, bm, ad, bd)
194+
end
195+
defp do_sort_instructions(mods, am, bm, ad, bd) do
196+
ad = Enum.filter(ad, fn ^am -> false; d -> d in mods end)
197+
bd = Enum.filter(bd, fn ^bm -> false; d -> d in mods end)
198+
lad = length(ad)
199+
lbd = length(bd)
200+
cond do
201+
lad == 0 and lbd != 0 -> true
202+
lad != 0 and lbd == 0 -> false
203+
# If a depends on b and b doesn't depend on a
204+
# Then b comes first, and vice versa
205+
am in bd and not bm in ad -> true
206+
not am in bd and bm in ad -> false
207+
# If either they don't depend on each other,
208+
# or they both depend on each other, then the
209+
# module with the least outgoing dependencies
210+
# comes first. Otherwise we treat them as equal
211+
lad > lbd -> false
212+
lbd > lad -> true
213+
:else -> true
214+
end
215+
end
216+
217+
defp extract_deps({:update, _, _, deps}), do: deps
218+
defp extract_deps({:load_module, _, deps}), do: deps
219+
220+
defp module_name(file) do
221+
Keyword.fetch!(:beam_lib.info(file), :module)
222+
end
223+
224+
defp vsn(props) do
225+
{:value, {:vsn, vsn}} = :lists.keysearch(:vsn, 1, props)
226+
List.to_string(vsn)
227+
end
228+
229+
end

‎lib/mix/lib/releases/archiver.ex

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
defmodule Mix.Releases.Archiver do
2+
@moduledoc """
3+
This module is responsible for packaging a release into a tarball.
4+
"""
5+
alias Mix.Releases.{Config, Overlays, Utils}
6+
7+
def archive(%Config{} = config) do
8+
release = config.selected_release
9+
name = "#{release.name}"
10+
output_dir = Path.relative_to_cwd(Path.join("rel", "#{release.name}"))
11+
12+
case make_tar(config, release, name, output_dir) do
13+
{:error, _} = err ->
14+
err
15+
:ok ->
16+
case apply_overlays(config, release, name, output_dir) do
17+
{:ok, overlays} ->
18+
update_tar(config, release, name, output_dir, overlays)
19+
{:error, _} = err ->
20+
err
21+
end
22+
end
23+
end
24+
25+
defp make_tar(config, release, name, output_dir) do
26+
opts = [
27+
{:path, ['#{Path.join([output_dir, "lib", "*", "ebin"])}']},
28+
{:dirs, [:include | case config.include_src do
29+
true -> [:src, :c_src]
30+
false -> []
31+
end]},
32+
{:outdir, '#{Path.join([output_dir, "releases", release.version])}'} |
33+
case config.include_erts do
34+
true ->
35+
path = Path.expand("#{:code.root_dir()}")
36+
[{:erts, '#{path}'}]
37+
false ->
38+
[]
39+
path ->
40+
path = Path.expand(path)
41+
[{:erts, '#{path}'}]
42+
end
43+
]
44+
rel_path = '#{Path.join([output_dir, "releases", release.version, name])}'
45+
case :systools.make_tar(rel_path, opts) do
46+
:ok ->
47+
:ok
48+
{:ok, mod, warnings} ->
49+
{:error, {:tar_generation_warn, mod, warnings}}
50+
:error ->
51+
{:error, {:tar_generation_error, :unknown}}
52+
{:error, mod, errors} ->
53+
{:error, {:tar_generation_error, mod, errors}}
54+
end
55+
end
56+
57+
defp update_tar(config, release, name, output_dir, overlays) do
58+
tarfile = '#{Path.join([output_dir, "releases", release.version, name <> ".tar.gz"])}'
59+
tmpdir = Utils.insecure_mkdtemp!
60+
:erl_tar.extract(tarfile, [{:cwd, '#{tmpdir}'}, :compressed])
61+
:ok = :erl_tar.create(tarfile, [
62+
{'releases', '#{Path.join(tmpdir, "releases")}'},
63+
{'#{Path.join("releases", "start_erl.data")}',
64+
'#{Path.join([output_dir, "releases", "start_erl.data"])}'},
65+
{'#{Path.join("releases", "RELEASES")}',
66+
'#{Path.join([output_dir, "releases", "RELEASES"])}'},
67+
{'#{Path.join(["releases", release.version, "vm.args"])}',
68+
'#{Path.join([output_dir, "releases", release.version, "vm.args"])}'},
69+
{'#{Path.join(["releases", release.version, "sys.config"])}',
70+
'#{Path.join([output_dir, "releases", release.version, "sys.config"])}'},
71+
{'#{Path.join(["releases", release.version, name <> ".sh"])}',
72+
'#{Path.join([output_dir, "releases", release.version, name <> ".sh"])}'},
73+
{'bin', '#{Path.join(output_dir, "bin")}'} |
74+
case config.include_erts do
75+
false ->
76+
case config.include_system_libs do
77+
false ->
78+
libs = Path.wildcard(Path.join([tmpdir, "lib", "*"]))
79+
system_libs = Path.wildcard(Path.join("#{:code.lib_dir}", "*"))
80+
for libdir <- :lists.subtract(libs, system_libs),
81+
do: {'#{Path.join("lib", libdir)}', '#{Path.join([tmpdir, "lib", libdir])}'}
82+
true ->
83+
[{'lib', '#{Path.join(tmpdir, "lib")}'}]
84+
end
85+
true ->
86+
erts_vsn = Utils.erts_version()
87+
[{'lib', '#{Path.join(tmpdir, "lib")}'},
88+
{'erts-#{erts_vsn}', '#{Path.join(output_dir, "erts-" <> erts_vsn)}'}]
89+
end
90+
] ++ overlays, [:dereference, :compressed])
91+
File.rm_rf!(tmpdir)
92+
:ok
93+
end
94+
95+
defp apply_overlays(config, release, _name, output_dir) do
96+
overlay_vars = config.overlay_vars ++ generate_overlay_vars(config, release)
97+
hook_overlays = [
98+
{:mkdir, "releases/<%= release_version %>/hooks"},
99+
{:copy, config.pre_start_hook, "releases/<%= release_version %>/hooks/pre_start"},
100+
{:copy, config.post_start_hook, "releases/<%= release_version %>/hooks/post_start"},
101+
{:copy, config.pre_stop_hook, "releases/<%= release_version %>/hooks/pre_stop"},
102+
{:copy, config.post_stop_hook, "releases/<%= release_version %>/hooks/post_stop"}
103+
] |> Enum.filter(fn {:copy, nil, _} -> false; _ -> true end)
104+
overlays = hook_overlays ++ config.overlays
105+
case Overlays.apply(release, output_dir, overlays, overlay_vars) do
106+
{:ok, paths} ->
107+
{:ok, Enum.map(paths, fn path ->
108+
{'#{path}', '#{Path.join([output_dir, path])}'}
109+
end)}
110+
{:error, _} = err ->
111+
err
112+
end
113+
end
114+
115+
defp generate_overlay_vars(_config, release) do
116+
[erts_vsn: Utils.erts_version(),
117+
release_name: release.name,
118+
release_version: release.version]
119+
end
120+
end

‎lib/mix/lib/releases/assembler.ex

+405
Large diffs are not rendered by default.

‎lib/mix/lib/releases/config.ex

+320
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
defmodule Mix.Releases.Config do
2+
@moduledoc false
3+
4+
defmodule LoadError do
5+
defexception [:file, :error]
6+
7+
def message(%LoadError{file: file, error: error}) do
8+
"could not load release config #{Path.relative_to_cwd(file)}\n " <>
9+
"#{Exception.format_banner(:error, error)}"
10+
end
11+
end
12+
13+
defmodule ReleaseDefinition do
14+
defstruct name: "",
15+
version: "0.0.1",
16+
applications: [
17+
:iex, # included so the elixir shell works
18+
:sasl # required for upgrades
19+
# can also use `app_name: type`, as in `some_dep: load`,
20+
# to only load the application, not start it
21+
]
22+
23+
def new(name, version, apps \\ []) do
24+
definition = %__MODULE__{name: name, version: version}
25+
%{definition | :applications => definition.applications ++ apps}
26+
end
27+
end
28+
29+
defstruct dev_mode: false,
30+
paths: [], # additional code paths to search
31+
vm_args: nil, # path to a custom vm.args
32+
sys_config: nil, # path to a custom sys.config
33+
include_erts: true, # false | path: "path/to/erts"
34+
include_src: false, # true
35+
include_system_libs: true, # false | path: "path/to/libs"
36+
strip_debug_info?: true, # false
37+
selected_release: :default, # the release being built
38+
upgrade_from: :default, # the release to upgrade from (if applicable)
39+
erl_opts: [],
40+
releases: [], # the releases to select from
41+
overrides: [
42+
# During development its often the case that you want to substitute the app
43+
# that you are working on for a 'production' version of an app. You can
44+
# explicitly tell Mix to override all versions of an app that you specify
45+
# with an app in an arbitrary directory. Mix will then symlink that app
46+
# into the release in place of the specified app. be aware though that Mix
47+
# will check your app for consistancy so it should be a normal OTP app and
48+
# already be built.
49+
],
50+
overlay_vars: [
51+
# key: value
52+
],
53+
overlays: [
54+
# copy: {from_path, to_path}
55+
# link: {from_path, to_path}
56+
# mkdir: path
57+
# template: {template_path, output_path}
58+
],
59+
pre_start_hook: nil,
60+
post_start_hook: nil,
61+
pre_stop_hook: nil,
62+
post_stop_hook: nil
63+
64+
defmacro __using__(_) do
65+
quote do
66+
import Mix.Releases.Config, only: [
67+
release: 2, release: 3, override: 2, overlay: 2, config: 1,
68+
version: 1
69+
]
70+
{:ok, agent} = Mix.Config.Agent.start_link
71+
var!(config_agent, Mix.Releases.Config) = agent
72+
end
73+
end
74+
75+
defmacro config(opts) when is_list(opts) do
76+
quote do
77+
Mix.Config.Agent.merge var!(config_agent, Mix.Releases.Config),
78+
[{:settings, unquote(opts)}]
79+
end
80+
end
81+
82+
defmacro release(name, version, applications \\ []) do
83+
quote do
84+
Mix.Config.Agent.merge var!(config_agent, Mix.Releases.Config),
85+
[{:releases, [{unquote(name), [{unquote(version), unquote(applications)}]}]}]
86+
end
87+
end
88+
89+
defmacro override(app, path) do
90+
quote do
91+
Mix.Config.Agent.merge var!(config_agent, Mix.Releases.Config),
92+
[{:overrides, [{unquote(app), unquote(path)}]}]
93+
end
94+
end
95+
96+
defmacro overlay(type, opts) do
97+
quote do
98+
Mix.Config.Agent.merge var!(config_agent, Mix.Releases.Config),
99+
[{:overlays, [{unquote(type), unquote(opts)}]}]
100+
end
101+
end
102+
103+
defmacro version(app) do
104+
quote do
105+
Application.load(unquote(app))
106+
case Application.spec(unquote(app)) do
107+
nil -> raise ArgumentError, "could not load app #{unquote(app)}"
108+
spec -> Keyword.get(spec, :vsn)
109+
end
110+
end
111+
end
112+
113+
@doc """
114+
Reads and validates a configuration file.
115+
`file` is the path to the configuration file to be read. If that file doesn't
116+
exist or if there's an error loading it, a `Mix.Releases.Config.LoadError` exception
117+
will be raised.
118+
"""
119+
def read!(file) do
120+
try do
121+
{config, binding} = Code.eval_file(file)
122+
123+
config = case List.keyfind(binding, {:config_agent, Mix.Releases.Config}, 0) do
124+
{_, agent} -> get_config_and_stop_agent(agent)
125+
nil -> config
126+
end
127+
128+
config = to_struct(config)
129+
validate!(config)
130+
config
131+
rescue
132+
e in [LoadError] -> reraise(e, System.stacktrace)
133+
e -> reraise(LoadError, [file: file, error: e], System.stacktrace)
134+
end
135+
end
136+
137+
def validate!(%__MODULE__{:releases => []}) do
138+
raise ArgumentError,
139+
"expected release config to have at least one release defined"
140+
end
141+
def validate!(%__MODULE__{} = config) do
142+
for override <- config.overrides do
143+
case override do
144+
{app, path} when is_atom(app) and is_binary(path) ->
145+
:ok
146+
value ->
147+
raise ArgumentError,
148+
"expected override to be an app name and path, but got: #{inspect value}"
149+
end
150+
end
151+
for overlay <- config.overlays do
152+
case overlay do
153+
{op, opts} when is_atom(op) and is_list(opts) ->
154+
:ok
155+
value ->
156+
raise ArgumentError,
157+
"expected overlay to be an overlay type and options, but got: #{inspect value}"
158+
end
159+
end
160+
cond do
161+
is_list(config.overlay_vars) && length(config.overlay_vars) > 0 && Keyword.keyword?(config.overlay_vars) ->
162+
:ok
163+
is_list(config.overlay_vars) && length(config.overlay_vars) == 0 ->
164+
:ok
165+
:else ->
166+
raise ArgumentError,
167+
"expected overlay_vars to be a keyword list, but got: #{inspect config.overlay_vars}"
168+
end
169+
paths_valid? = Enum.all?(config.paths, &is_binary/1)
170+
cond do
171+
not is_boolean(config.dev_mode) ->
172+
raise ArgumentError,
173+
"expected :dev_mode to be a boolean, but got: #{inspect config.dev_mode}"
174+
not paths_valid? ->
175+
raise ArgumentError,
176+
"expected :paths to be a list of strings, but got: #{inspect config.paths}"
177+
not (is_nil(config.vm_args) or is_binary(config.vm_args)) ->
178+
raise ArgumentError,
179+
"expected :vm_args to be nil or a path string, but got: #{inspect config.vm_args}"
180+
not (is_nil(config.sys_config) or is_binary(config.sys_config)) ->
181+
raise ArgumentError,
182+
"expected :sys_config to be nil or a path string, but got: #{inspect config.sys_config}"
183+
not (is_boolean(config.include_erts) or is_binary(config.include_erts)) ->
184+
raise ArgumentError,
185+
"expected :include_erts to be boolean or a path string, but got: #{inspect config.include_erts}"
186+
not (is_boolean(config.include_src) or is_binary(config.include_src)) ->
187+
raise ArgumentError,
188+
"expected :include_src to be boolean, but got: #{inspect config.include_src}"
189+
not (is_boolean(config.include_system_libs) or is_binary(config.include_system_libs)) ->
190+
raise ArgumentError,
191+
"expected :include_system_libs to be boolean or a path string, but got: #{inspect config.include_system_libs}"
192+
not is_list(config.erl_opts) ->
193+
raise ArgumentError,
194+
"expected :erl_opts to be a list, but got: #{inspect config.erl_opts}"
195+
not is_boolean(config.strip_debug_info?) ->
196+
raise ArgumentError,
197+
"expected :strip_debug_info? to be a boolean, but got: #{inspect config.strip_debug_info?}"
198+
not (is_nil(config.pre_start_hook) or is_binary(config.pre_start_hook)) ->
199+
raise ArgumentError,
200+
"expected :pre_start_hook to be nil or a path string, but got: #{inspect config.pre_start_hook}"
201+
not (is_nil(config.post_start_hook) or is_binary(config.post_start_hook)) ->
202+
raise ArgumentError,
203+
"expected :post_start_hook to be nil or a path string, but got: #{inspect config.post_start_hook}"
204+
not (is_nil(config.pre_stop_hook) or is_binary(config.pre_stop_hook)) ->
205+
raise ArgumentError,
206+
"expected :pre_stop_hook to be nil or a path string, but got: #{inspect config.pre_stop_hook}"
207+
not (is_nil(config.post_stop_hook) or is_binary(config.post_stop_hook)) ->
208+
raise ArgumentError,
209+
"expected :post_stop_hook to be nil or a path string, but got: #{inspect config.post_stop_hook}"
210+
:else ->
211+
true
212+
end
213+
end
214+
def validate!(config) do
215+
raise ArgumentError,
216+
"expected release config to be a struct, instead got: #{inspect config}"
217+
end
218+
219+
defp get_config_and_stop_agent(agent) do
220+
config = Mix.Config.Agent.get(agent)
221+
Mix.Config.Agent.stop(agent)
222+
config
223+
end
224+
225+
defp to_struct(config) when is_list(config) do
226+
case Keyword.keyword?(config) do
227+
false -> to_struct(:default)
228+
true ->
229+
%__MODULE__{}
230+
|> to_struct(:settings, Keyword.get(config, :settings, []))
231+
|> to_struct(:releases, Keyword.get(config, :releases, []))
232+
|> to_struct(:overrides, Keyword.get(config, :overrides, []))
233+
|> to_struct(:overlays, Keyword.get(config, :overlays, []))
234+
end
235+
end
236+
# If no config is given, generate a default release definition for the current project.
237+
# If the current project is an umbrella, generate a release which contains all applications
238+
# in the umbrella.
239+
defp to_struct(_) do
240+
current_project = Mix.Project.config
241+
case Mix.Project.umbrella?(current_project) do
242+
true ->
243+
apps_path = Keyword.fetch!(current_project, :apps_path)
244+
apps = get_umbrella_apps(apps_path)
245+
app = convert_to_name(Mix.Project.get!)
246+
version = "0.1.0"
247+
%__MODULE__{
248+
releases: [ReleaseDefinition.new(app, version, apps)]
249+
}
250+
false ->
251+
app = Keyword.fetch!(current_project, :app)
252+
version = Keyword.fetch!(current_project, :version)
253+
%__MODULE__{
254+
releases: [ReleaseDefinition.new(app, version, [app])]
255+
}
256+
end
257+
end
258+
259+
defp to_struct(config, :settings, []), do: config
260+
defp to_struct(config, :settings, s) do
261+
%__MODULE__{
262+
config |
263+
:dev_mode => Keyword.get(s, :dev_mode, config.dev_mode),
264+
:paths => Keyword.get(s, :paths, config.paths),
265+
:vm_args => Keyword.get(s, :vm_args, config.vm_args),
266+
:sys_config => Keyword.get(s, :sys_config, config.sys_config),
267+
:include_erts => Keyword.get(s, :include_erts, config.include_erts),
268+
:include_src => Keyword.get(s, :include_erts, config.include_src),
269+
:erl_opts => Keyword.get(s, :erl_opts, config.erl_opts),
270+
:include_system_libs => Keyword.get(s, :include_system_libs, config.include_system_libs),
271+
:strip_debug_info? => Keyword.get(s, :strip_debug_info?, config.strip_debug_info?),
272+
:overlay_vars => Keyword.get(s, :overlay_vars, config.overlay_vars),
273+
:pre_start_hook => Keyword.get(s, :pre_start_hook, config.pre_start_hook),
274+
:post_start_hook => Keyword.get(s, :post_start_hook, config.post_start_hook),
275+
:pre_stop_hook => Keyword.get(s, :pre_stop_hook, config.pre_stop_hook),
276+
:post_stop_hook => Keyword.get(s, :post_stop_hook, config.post_stop_hook)
277+
}
278+
end
279+
defp to_struct(config, :releases, []), do: config
280+
defp to_struct(config, :releases, r) do
281+
releases = Enum.flat_map(r, fn
282+
{app, [{version, []}]}->
283+
[ReleaseDefinition.new(app, version, [app])]
284+
{app, [{version, apps}]} when is_list(apps) ->
285+
[ReleaseDefinition.new(app, version, Enum.uniq([app|apps]))]
286+
{app, versions} when is_list(versions) ->
287+
Enum.map(versions, fn
288+
{version, []} ->
289+
ReleaseDefinition.new(app, version)
290+
{version, apps} when is_list(apps) ->
291+
ReleaseDefinition.new(app, version, Enum.uniq([app|apps]))
292+
end)
293+
end)
294+
%__MODULE__{config | :releases => releases}
295+
end
296+
defp to_struct(config, :overrides, o) do
297+
%__MODULE__{config | :overrides => o}
298+
end
299+
defp to_struct(config, :overlays, o) do
300+
%__MODULE__{config | :overlays => o}
301+
end
302+
303+
defp convert_to_name(module) when is_atom(module) do
304+
[name_str|_] = Module.split(module)
305+
Regex.split(~r/(?<word>[A-Z][^A-Z]*)/, name_str, on: [:word], include_captures: true, trim: true)
306+
|> Enum.map(&String.downcase/1)
307+
|> Enum.join("_")
308+
|> String.to_atom
309+
end
310+
311+
defp get_umbrella_apps(apps_path) do
312+
Path.wildcard(Path.join(apps_path, "*"))
313+
|> Enum.map(fn path ->
314+
Mix.Project.in_project(:app, path, fn _mixfile ->
315+
Keyword.fetch!(Mix.Project.config, :app)
316+
end)
317+
end)
318+
end
319+
320+
end

‎lib/mix/lib/releases/overlays.ex

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
defmodule Mix.Releases.Overlays do
2+
@moduledoc """
3+
This module is responsible for applying overlays to a release, prior to packaging.
4+
Overlays are templated with EEx, with bindings set to the values configured in `overlay_vars`.
5+
6+
There are some preconfigured overlay variables, namely:
7+
- `erts_vsn`: The version of ERTS used by this release
8+
- `release_name`: The name of the current release
9+
- `release_version`: The version of the current release
10+
11+
For example, given a release named `my_release`, version `0.1.0`:
12+
13+
{:mkdir, "releases/<%= release_version %>/foo"}
14+
15+
The above overlay will create a directory, `rel/my_release/releases/0.1.0/foo`. Overlay input paths are
16+
relative to the project root, but overlay output paths are relative to the root directory for the current
17+
release, which is why the directory is created in `rel/my_release`, and not in the project root.
18+
"""
19+
alias Mix.Releases.Config.ReleaseDefinition
20+
21+
@typep overlay :: {:mkdir, String.t} |
22+
{:copy, String.t, String.t} |
23+
{:link, String.t, String.t} |
24+
{:template, String.t, String.t}
25+
26+
@typep error :: {:error, {:invalid_overlay, term}} |
27+
{:error, {:template_str, String.t}} |
28+
{:error, {:template_file, {non_neg_integer, non_neg_integer, String.t}}} |
29+
{:error, {:overlay_failed, term, overlay}}
30+
31+
@doc """
32+
Applies a list of overlays to the current release.
33+
Returns `{:ok, output_paths}` or `{:error, details}`, where `details` is
34+
one of the following:
35+
36+
- {:invalid_overlay, term} - a malformed overlay object
37+
- {:template_str, desc} - templating an overlay parameter failed
38+
- {:template_file, file, line, desc} - a template overlay failed
39+
- {:overlay_failed, term, overlay} - applying an overlay failed
40+
"""
41+
@spec apply(ReleaseDefinition.t, String.t, list(overlay), Keyword.t) :: {:ok, [String.t]} | error
42+
def apply(_release, _ouput_dir, [], _overlay_vars), do: {:ok, []}
43+
def apply(release, output_dir, overlays, overlay_vars) do
44+
do_apply(release, output_dir, overlays, overlay_vars, [])
45+
end
46+
47+
defp do_apply(_release, _output_dir, _overlays, _vars, {:error, _} = err),
48+
do: err
49+
defp do_apply(_release, _output_dir, [], _vars, acc),
50+
do: {:ok, acc}
51+
defp do_apply(release, output_dir, [overlay|rest], overlay_vars, acc) when is_list(acc) do
52+
case do_overlay(release, output_dir, overlay, overlay_vars) do
53+
{:ok, path} ->
54+
do_apply(release, output_dir, rest, overlay_vars, [path|acc])
55+
{:error, {:invalid_overlay, _}} = err -> err
56+
{:error, {:template_str, _}} = err -> err
57+
{:error, {:template_file, _}} = err -> err
58+
{:error, reason} ->
59+
{:error, {:overlay_failed, reason, overlay}}
60+
end
61+
end
62+
63+
defp do_overlay(_release, output_dir, {:mkdir, path}, vars) when is_binary(path) do
64+
with {:ok, path} <- template_str(path, vars),
65+
expanded <- Path.join(output_dir, path),
66+
:ok <- File.mkdir_p(expanded),
67+
do: {:ok, path}
68+
end
69+
defp do_overlay(_release, output_dir, {:copy, from, to}, vars) when is_binary(from) and is_binary(to) do
70+
with {:ok, from} <- template_str(from, vars),
71+
{:ok, to} <- template_str(to, vars),
72+
expanded_to <- Path.join(output_dir, to),
73+
{:ok, _} <- File.cp_r(from, expanded_to),
74+
do: {:ok, to}
75+
end
76+
defp do_overlay(_release, output_dir, {:link, from, to}, vars) when is_binary(from) and is_binary(to) do
77+
with {:ok, from} <- template_str(from, vars),
78+
{:ok, to} <- template_str(to, vars),
79+
expanded_to <- Path.join(output_dir, to),
80+
:ok <- File.ln_s(from, expanded_to),
81+
do: {:ok, to}
82+
end
83+
defp do_overlay(_release, output_dir, {:template, tmpl_path, to}, vars) when is_binary(tmpl_path) and is_binary(to) do
84+
with true <- File.exists?(tmpl_path),
85+
{:ok, templated} <- template_file(tmpl_path, vars),
86+
expanded_to <- Path.join(output_dir, to),
87+
:ok <- File.mkdir_p(Path.dirname(expanded_to)),
88+
:ok <- File.write(expanded_to, templated),
89+
do: {:ok, to}
90+
end
91+
defp do_overlay(_release, _output_dir, invalid, _), do: {:error, {:invalid_overlay, invalid}}
92+
93+
defp template_str(str, overlay_vars) do
94+
try do
95+
{:ok, EEx.eval_string(str, overlay_vars)}
96+
rescue
97+
err in [CompileError] ->
98+
{:error, {:template_str, err.description}}
99+
end
100+
end
101+
102+
defp template_file(path, overlay_vars) do
103+
try do
104+
{:ok, EEx.eval_file(path, overlay_vars)}
105+
rescue
106+
err in [CompileError] ->
107+
{:error, {:template_file, {err.file, err.line, err.description}}}
108+
end
109+
end
110+
end

‎lib/mix/lib/releases/utils.ex

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
defmodule Mix.Releases.Utils do
2+
@moduledoc false
3+
4+
@doc """
5+
Loads a template from :bundler's `priv/templates` directory based on the provided name.
6+
Any parameters provided are configured as bindings for the template
7+
"""
8+
@spec template(atom | String.t, Keyword.t) :: String.t
9+
def template(name, params \\ []) do
10+
template_path = Path.join(["#{:code.priv_dir(:bundler)}", "templates", "#{name}.eex"])
11+
EEx.eval_file(template_path, params)
12+
end
13+
14+
@doc """
15+
Writes an Elixir/Erlang term to the provided path
16+
"""
17+
def write_term(path, term) do
18+
:file.write_file('#{path}', :io_lib.fwrite('~p.\n', [term]))
19+
end
20+
21+
@doc """
22+
Writes a collection of Elixir/Erlang terms to the provided path
23+
"""
24+
def write_terms(path, terms) when is_list(terms) do
25+
contents = String.duplicate("~p.\n\n", Enum.count(terms))
26+
|> String.to_char_list
27+
|> :io_lib.fwrite(Enum.reverse(terms))
28+
:file.write_file('#{path}', contents, [encoding: :utf8])
29+
end
30+
31+
@doc """
32+
Determines the current ERTS version
33+
"""
34+
@spec erts_version() :: String.t
35+
def erts_version, do: "#{:erlang.system_info(:version)}"
36+
37+
@doc """
38+
Creates a temporary directory with a random name in a canonical
39+
temporary files directory of the current system
40+
(i.e. `/tmp` on *NIX or `./tmp` on Windows)
41+
42+
Returns the path of the temp directory, and raises an error
43+
if it is unable to create the directory.
44+
"""
45+
@spec insecure_mkdtemp!() :: String.t | no_return
46+
def insecure_mkdtemp!() do
47+
unique_num = trunc(:random.uniform() * 1000000000000)
48+
tmpdir_path = case :erlang.system_info(:system_architecture) do
49+
"win32" ->
50+
Path.join(["./tmp", ".tmp_dir#{unique_num}"])
51+
_ ->
52+
Path.join(["/tmp", ".tmp_dir#{unique_num}"])
53+
end
54+
File.mkdir_p!(tmpdir_path)
55+
tmpdir_path
56+
end
57+
58+
end

‎mix.exs

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
defmodule Bundler.Mixfile do
2+
use Mix.Project
3+
4+
def project do
5+
[app: :bundler,
6+
version: "0.1.0",
7+
elixir: "~> 1.3",
8+
build_embedded: Mix.env == :prod,
9+
start_permanent: Mix.env == :prod,
10+
deps: deps,
11+
description: description,
12+
package: package,
13+
docs: docs]
14+
end
15+
16+
def application, do: [applications: []]
17+
18+
defp deps do
19+
[{:ex_doc, github: "elixir-lang/ex_doc", only: [:dev]},
20+
{:earmark, "~> 1.0", only: [:dev]},
21+
{:excoveralls, "~> 0.5", only: [:dev, :test]}]
22+
end
23+
24+
defp description do
25+
"Build releases of your Mix projects with ease!"
26+
end
27+
defp package do
28+
[ files: ["lib", "priv", "mix.exs", "README.md", "LICENSE.md"],
29+
maintainers: ["Paul Schoenfelder"],
30+
licenses: ["MIT"],
31+
links: %{"Github": "https://github.com/bitwalker/bundler",
32+
"Documentation": "https://hexdocs.pm/bundler"}]
33+
end
34+
defp docs do
35+
[main: "getting-started",
36+
extras: [
37+
"docs/Getting Started.md",
38+
"docs/Configuration.md",
39+
"docs/Walkthrough.md",
40+
"docs/Upgrades and Downgrades.md",
41+
"docs/Common Issues.md"
42+
]]
43+
end
44+
end

‎mix.lock

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
%{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []},
2+
"earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []},
3+
"ex_doc": {:git, "https://github.com/elixir-lang/ex_doc.git", "be21e6de9f78a5612d48c50b7ad500d96cb0ccc0", []},
4+
"excoveralls": {:hex, :excoveralls, "0.5.5", "d97b6fc7aa59c5f04f2fa7ec40fc0b7555ceea2a5f7e7c442aad98ddd7f79002", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}, {:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}]},
5+
"exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]},
6+
"hackney": {:hex, :hackney, "1.6.1", "ddd22d42db2b50e6a155439c8811b8f6df61a4395de10509714ad2751c6da817", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]},
7+
"idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []},
8+
"jsx": {:hex, :jsx, "2.6.2", "213721e058da0587a4bce3cc8a00ff6684ced229c8f9223245c6ff2c88fbaa5a", [:mix, :rebar], []},
9+
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []},
10+
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []},
11+
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}}

‎priv/templates/boot.eex

+507
Large diffs are not rendered by default.

‎priv/templates/boot_loader.eex

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/sh
2+
set -e
3+
4+
SCRIPT_DIR="$(cd $(dirname "$0") && pwd -P)"
5+
RELEASE_ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
6+
RELEASES_DIR="$RELEASE_ROOT_DIR/releases"
7+
REL_NAME="<%= rel_name %>"
8+
REL_VSN=$(cat $RELEASES_DIR/start_erl.data | cut -d' ' -f2)
9+
ERTS_VSN=$(cat $RELEASES_DIR/start_erl.data | cut -d' ' -f1)
10+
11+
12+
_cleanup() {
13+
echo "Shutting down node cleanly.."
14+
exec "$RELEASES_DIR/$REL_VSN/$REL_NAME.sh" rpc init stop
15+
}
16+
17+
case "$1" in
18+
foreground)
19+
trap '_cleanup' HUP INT TERM QUIT
20+
21+
"$RELEASES_DIR/$REL_VSN/$REL_NAME.sh" $@ &
22+
__pid=$!
23+
24+
wait $__pid
25+
__exit_status=$?
26+
27+
exit $__exit_status
28+
;;
29+
*)
30+
exec "$RELEASES_DIR/$REL_VSN/$REL_NAME.sh" "$@"
31+
;;
32+
esac

‎priv/templates/erl_script.eex

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/sh
2+
set -e
3+
4+
SCRIPT_DIR=`dirname $0`
5+
ROOTDIR=`cd $SCRIPT_DIR/../../ && pwd`
6+
BINDIR=$ROOTDIR/erts-<%= erts_vsn %>/bin
7+
EMU=beam
8+
PROGNAME=`echo $0 | sed 's/.*\\///'`
9+
export EMU
10+
export ROOTDIR
11+
export BINDIR
12+
export PROGNAME
13+
exec "$BINDIR/erlexec" ${1+"$@"}

‎priv/templates/example_config.eex

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
use Mix.Releases.Config
2+
<%= unless no_docs do %>
3+
# A full list of config options is as follows:
4+
#
5+
# - dev_mode (boolean);
6+
# symlink compiled files into the release,
7+
# so that you can release once, but see changes when you recompile
8+
# the project. This is only for development.
9+
# - paths (list of strings);
10+
# a list of file paths, representing code paths
11+
# to search for compiled BEAMs
12+
# - vm_args (string);
13+
# a path to a custom vm.args file
14+
# - sys_config (string);
15+
# a path to a custom sys.config file
16+
# - include_erts (boolean | string);
17+
# whether to include the system ERTS or not,
18+
# a path to an alternative ERTS can also be provided
19+
# - include_src (boolean);
20+
# should source code be included in the release
21+
# - include_system_libs (boolean | string);
22+
# should system libs be included in the release,
23+
# a path to system libs to be included can also be provided
24+
# - strip_debug_info? (boolean);
25+
# should debugging info be stripped from BEAM files in the release
26+
# - erl_opts (list of strings);
27+
# a list of Erlang VM options to be used
28+
# - overrides (keyword list);
29+
# During development its often the case that you want to substitute the app
30+
# that you are working on for a 'production' version of an app. You can
31+
# explicitly tell Mix to override all versions of an app that you specify
32+
# with an app in an arbitrary directory. Mix will then symlink that app
33+
# into the release in place of the specified app. be aware though that Mix
34+
# will check your app for consistancy so it should be a normal OTP app and
35+
# already be built.
36+
# - overlay_vars (keyword list);
37+
# A keyword list of bindings to use in overlays
38+
# - overlays (special keyword list);
39+
# A list of overlay operations to perform against the release,
40+
# such as copying files, symlinking files, etc.
41+
# copy: {from_path, to_path}
42+
# link: {from_path, to_path}
43+
# mkdir: path
44+
# template: {template_path, output_path}
45+
# - pre_start_hook: nil,
46+
# - post_start_hook: nil,
47+
# - pre_stop_hook: nil,
48+
# - post_stop_hook: nil
49+
<% end %>
50+
config debug?: false,
51+
include_erts: true
52+
<%= unless no_docs do %>
53+
# You may define one or more releases in this file,
54+
# the first one in the file will be built by default,
55+
# but you may set another with the `:selected_release` config
56+
# option, or by passing `--release <name>` when running `mix release`<% end %>
57+
<%= for release <- releases do %>
58+
release :<%= Keyword.get(release, :release_name) %>, version(:<%= Keyword.get(release, :release_name)%>),
59+
[<%= Enum.map(Keyword.get(release, :release_applications), fn {app, start_type} ->
60+
"#{app}: :#{start_type}"
61+
end) |> Enum.join(",\n ") %>]
62+
<% end %>

‎priv/templates/install_upgrade.eex

+143
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/env escript
2+
%%! -noshell -noinput
3+
%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
4+
%% ex: ft=erlang ts=4 sw=4 et
5+
6+
-define(TIMEOUT, 300000).
7+
-define(INFO(Fmt,Args), io:format(Fmt,Args)).
8+
9+
%% Unpack or upgrade to a new tar.gz release
10+
main(["unpack", RelName, NameTypeArg, NodeName, Cookie, VersionArg]) ->
11+
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
12+
WhichReleases = which_releases(TargetNode),
13+
Version = parse_version(VersionArg),
14+
case proplists:get_value(Version, WhichReleases) of
15+
undefined ->
16+
%% not installed, so unpack tarball:
17+
?INFO("Release ~s not found, attempting to unpack releases/~s/~s.tar.gz~n",[Version,Version,RelName]),
18+
ReleasePackage = Version ++ "/" ++ RelName,
19+
case rpc:call(TargetNode, release_handler, unpack_release,
20+
[ReleasePackage], ?TIMEOUT) of
21+
{ok, Vsn} ->
22+
?INFO("Unpacked successfully: ~p~n", [Vsn]);
23+
{error, UnpackReason} ->
24+
print_existing_versions(TargetNode),
25+
?INFO("Unpack failed: ~p~n",[UnpackReason]),
26+
erlang:halt(2)
27+
end;
28+
old ->
29+
%% no need to unpack, has been installed previously
30+
?INFO("Release ~s is marked old, switching to it.~n",[Version]);
31+
unpacked ->
32+
?INFO("Release ~s is already unpacked, now installing.~n",[Version]);
33+
current ->
34+
?INFO("Release ~s is already installed and current. Making permanent.~n",[Version]);
35+
permanent ->
36+
?INFO("Release ~s is already installed, and set permanent.~n",[Version])
37+
end;
38+
main(["install", RelName, NameTypeArg, NodeName, Cookie, VersionArg]) ->
39+
TargetNode = start_distribution(NodeName, NameTypeArg, Cookie),
40+
WhichReleases = which_releases(TargetNode),
41+
Version = parse_version(VersionArg),
42+
case proplists:get_value(Version, WhichReleases) of
43+
undefined ->
44+
%% not installed, so unpack tarball:
45+
?INFO("Release ~s not found, attempting to unpack releases/~s/~s.tar.gz~n",[Version,Version,RelName]),
46+
ReleasePackage = Version ++ "/" ++ RelName,
47+
case rpc:call(TargetNode, release_handler, unpack_release,
48+
[ReleasePackage], ?TIMEOUT) of
49+
{ok, Vsn} ->
50+
?INFO("Unpacked successfully: ~p~n", [Vsn]),
51+
install_and_permafy(TargetNode, RelName, Vsn);
52+
{error, UnpackReason} ->
53+
print_existing_versions(TargetNode),
54+
?INFO("Unpack failed: ~p~n",[UnpackReason]),
55+
erlang:halt(2)
56+
end;
57+
old ->
58+
%% no need to unpack, has been installed previously
59+
?INFO("Release ~s is marked old, switching to it.~n",[Version]),
60+
install_and_permafy(TargetNode, RelName, Version);
61+
unpacked ->
62+
?INFO("Release ~s is already unpacked, now installing.~n",[Version]),
63+
install_and_permafy(TargetNode, RelName, Version);
64+
current -> %% installed and in-use, just needs to be permanent
65+
?INFO("Release ~s is already installed and current. Making permanent.~n",[Version]),
66+
permafy(TargetNode, RelName, Version);
67+
permanent ->
68+
?INFO("Release ~s is already installed, and set permanent.~n",[Version])
69+
end;
70+
main(_) ->
71+
erlang:halt(1).
72+
73+
parse_version(V) when is_list(V) ->
74+
hd(string:tokens(V,"/")).
75+
76+
install_and_permafy(TargetNode, RelName, Vsn) ->
77+
case rpc:call(TargetNode, release_handler, check_install_release, [Vsn], ?TIMEOUT) of
78+
{ok, _OtherVsn, _Desc} ->
79+
ok;
80+
{error, Reason} ->
81+
?INFO("ERROR: release_handler:check_install_release failed: ~p~n",[Reason]),
82+
erlang:halt(3)
83+
end,
84+
case rpc:call(TargetNode, release_handler, install_release, [Vsn], ?TIMEOUT) of
85+
{ok, _, _} ->
86+
?INFO("Installed Release: ~s~n", [Vsn]),
87+
permafy(TargetNode, RelName, Vsn),
88+
ok;
89+
{error, {no_such_release, Vsn}} ->
90+
VerList =
91+
iolist_to_binary(
92+
[io_lib:format("* ~s\t~s~n",[V,S]) || {V,S} <- which_releases(TargetNode)]),
93+
?INFO("Installed versions:~n~s", [VerList]),
94+
?INFO("ERROR: Unable to revert to '~s' - not installed.~n", [Vsn]),
95+
erlang:halt(2)
96+
end.
97+
98+
permafy(TargetNode, RelName, Vsn) ->
99+
ok = rpc:call(TargetNode, release_handler, make_permanent, [Vsn], ?TIMEOUT),
100+
file:copy(filename:join(["bin", RelName++"-"++Vsn]),
101+
filename:join(["bin", RelName])),
102+
?INFO("Made release permanent: ~p~n", [Vsn]),
103+
ok.
104+
105+
which_releases(TargetNode) ->
106+
R = rpc:call(TargetNode, release_handler, which_releases, [], ?TIMEOUT),
107+
[ {V, S} || {_,V,_, S} <- R ].
108+
109+
print_existing_versions(TargetNode) ->
110+
VerList = iolist_to_binary([
111+
io_lib:format("* ~s\t~s~n",[V,S])
112+
|| {V,S} <- which_releases(TargetNode) ]),
113+
?INFO("Installed versions:~n~s", [VerList]).
114+
115+
start_distribution(NodeName, NameTypeArg, Cookie) ->
116+
MyNode = make_script_node(NodeName),
117+
{ok, _Pid} = net_kernel:start([MyNode, get_name_type(NameTypeArg)]),
118+
erlang:set_cookie(node(), list_to_atom(Cookie)),
119+
TargetNode = list_to_atom(NodeName),
120+
case {net_kernel:connect_node(TargetNode),
121+
net_adm:ping(TargetNode)} of
122+
{true, pong} ->
123+
ok;
124+
{_, pang} ->
125+
io:format("Node ~p not responding to pings.\n", [TargetNode]),
126+
erlang:halt(1)
127+
end,
128+
{ok, Cwd} = file:get_cwd(),
129+
ok = rpc:call(TargetNode, file, set_cwd, [Cwd], ?TIMEOUT),
130+
TargetNode.
131+
132+
make_script_node(Node) ->
133+
[Name, Host] = string:tokens(Node, "@"),
134+
list_to_atom(lists:concat([Name, "_upgrader_", os:getpid(), "@", Host])).
135+
136+
%% get name type from arg
137+
get_name_type(NameTypeArg) ->
138+
case NameTypeArg of
139+
"-sname" ->
140+
shortnames;
141+
_ ->
142+
longnames
143+
end.

‎priv/templates/nodetool.eex

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env escript
2+
%% -*- mode: erlang;erlang-indent-level: 4;indent-tabs-mode: nil -*-
3+
%% ex: ft=erlang ts=4 sw=4 et
4+
%% -------------------------------------------------------------------
5+
%%
6+
%% nodetool: Helper Script for interacting with live nodes
7+
%%
8+
%% -------------------------------------------------------------------
9+
10+
main(Args) ->
11+
ok = start_epmd(),
12+
%% Extract the args
13+
{RestArgs, TargetNode} = process_args(Args, [], undefined),
14+
15+
%% See if the node is currently running -- if it's not, we'll bail
16+
case {net_kernel:hidden_connect_node(TargetNode), net_adm:ping(TargetNode)} of
17+
{true, pong} ->
18+
ok;
19+
{_, pang} ->
20+
io:format("Node ~p not responding to pings.\n", [TargetNode]),
21+
halt(1)
22+
end,
23+
24+
case RestArgs of
25+
["ping"] ->
26+
%% If we got this far, the node already responsed to a ping, so just dump
27+
%% a "pong"
28+
io:format("pong\n");
29+
["stop"] ->
30+
io:format("~p\n", [rpc:call(TargetNode, init, stop, [], 60000)]);
31+
["restart"] ->
32+
io:format("~p\n", [rpc:call(TargetNode, init, restart, [], 60000)]);
33+
["reboot"] ->
34+
io:format("~p\n", [rpc:call(TargetNode, init, reboot, [], 60000)]);
35+
["rpc", Module, Function | RpcArgs] ->
36+
case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function),
37+
[RpcArgs], 60000) of
38+
ok ->
39+
ok;
40+
{badrpc, Reason} ->
41+
io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]),
42+
halt(1);
43+
_ ->
44+
halt(1)
45+
end;
46+
["rpcterms", Module, Function | ArgsAsString] ->
47+
case rpc:call(TargetNode, list_to_atom(Module), list_to_atom(Function),
48+
consult(lists:flatten(ArgsAsString)), 60000) of
49+
{badrpc, Reason} ->
50+
io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]),
51+
halt(1);
52+
Other ->
53+
io:format("~p\n", [Other])
54+
end;
55+
["eval" | ListOfArgs] ->
56+
% shells may process args into more than one, and end up stripping
57+
% spaces, so this converts all of that to a single string to parse
58+
String = binary_to_list(
59+
list_to_binary(
60+
string:join(ListOfArgs," ")
61+
)
62+
),
63+
64+
% then just as a convenience to users, if they forgot a trailing
65+
% '.' add it for them.
66+
Normalized =
67+
case lists:reverse(String) of
68+
[$. | _] -> String;
69+
R -> lists:reverse([$. | R])
70+
end,
71+
72+
% then scan and parse the string
73+
{ok, Scanned, _} = erl_scan:string(Normalized),
74+
{ok, Parsed } = erl_parse:parse_exprs(Scanned),
75+
76+
% and evaluate it on the remote node
77+
case rpc:call(TargetNode, erl_eval, exprs, [Parsed, [] ]) of
78+
{value, Value, _} ->
79+
io:format ("~p\n",[Value]);
80+
{badrpc, Reason} ->
81+
io:format("RPC to ~p failed: ~p\n", [TargetNode, Reason]),
82+
halt(1)
83+
end;
84+
Other ->
85+
io:format("Other: ~p\n", [Other]),
86+
io:format("Usage: nodetool {ping|stop|restart|reboot|rpc|rpcterms|eval [Terms]} [RPC]\n")
87+
end,
88+
net_kernel:stop().
89+
90+
process_args([], Acc, TargetNode) ->
91+
{lists:reverse(Acc), TargetNode};
92+
process_args(["-setcookie", Cookie | Rest], Acc, TargetNode) ->
93+
erlang:set_cookie(node(), list_to_atom(Cookie)),
94+
process_args(Rest, Acc, TargetNode);
95+
process_args(["-name", TargetName | Rest], Acc, _) ->
96+
ThisNode = append_node_suffix(TargetName, "_maint_"),
97+
{ok, _} = net_kernel:start([ThisNode, longnames]),
98+
process_args(Rest, Acc, nodename(TargetName));
99+
process_args(["-sname", TargetName | Rest], Acc, _) ->
100+
ThisNode = append_node_suffix(TargetName, "_maint_"),
101+
{ok, _} = net_kernel:start([ThisNode, shortnames]),
102+
process_args(Rest, Acc, nodename(TargetName));
103+
process_args([Arg | Rest], Acc, Opts) ->
104+
process_args(Rest, [Arg | Acc], Opts).
105+
106+
107+
start_epmd() ->
108+
[] = os:cmd("\"" ++ epmd_path() ++ "\" -daemon"),
109+
ok.
110+
111+
epmd_path() ->
112+
ErtsBinDir = filename:dirname(escript:script_name()),
113+
Name = "epmd",
114+
case os:find_executable(Name, ErtsBinDir) of
115+
false ->
116+
case os:find_executable(Name) of
117+
false ->
118+
io:format("Could not find epmd.~n"),
119+
halt(1);
120+
GlobalEpmd ->
121+
GlobalEpmd
122+
end;
123+
Epmd ->
124+
Epmd
125+
end.
126+
127+
128+
nodename(Name) ->
129+
case string:tokens(Name, "@") of
130+
[_Node, _Host] ->
131+
list_to_atom(Name);
132+
[Node] ->
133+
[_, Host] = string:tokens(atom_to_list(node()), "@"),
134+
list_to_atom(lists:concat([Node, "@", Host]))
135+
end.
136+
137+
append_node_suffix(Name, Suffix) ->
138+
case string:tokens(Name, "@") of
139+
[Node, Host] ->
140+
list_to_atom(lists:concat([Node, Suffix, os:getpid(), "@", Host]));
141+
[Node] ->
142+
list_to_atom(lists:concat([Node, Suffix, os:getpid()]))
143+
end.
144+
145+
%%
146+
%% Given a string or binary, parse it into a list of terms, ala file:consult/0
147+
%%
148+
consult(Str) when is_list(Str) ->
149+
consult([], Str, []);
150+
consult(Bin) when is_binary(Bin)->
151+
consult([], binary_to_list(Bin), []).
152+
153+
consult(Cont, Str, Acc) ->
154+
case erl_scan:tokens(Cont, Str, 0) of
155+
{done, Result, Remaining} ->
156+
case Result of
157+
{ok, Tokens, _} ->
158+
{ok, Term} = erl_parse:parse_term(Tokens),
159+
consult([], Remaining, [Term | Acc]);
160+
{eof, _Other} ->
161+
lists:reverse(Acc);
162+
{error, Info, _} ->
163+
{error, Info}
164+
end;
165+
{more, Cont1} ->
166+
consult(Cont1, eof, Acc)
167+
end.

‎priv/templates/vm.args.eex

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
## Name of the node
2+
-name <%= rel_name %>@127.0.0.1
3+
4+
## Cookie for distributed erlang
5+
-setcookie <%= rel_name %>
6+
7+
## Heartbeat management; auto-restarts VM if it dies or becomes unresponsive
8+
## (Disabled by default..use with caution!)
9+
##-heart
10+
11+
## Enable kernel poll and a few async threads
12+
##+K true
13+
##+A 5
14+
15+
## Increase number of concurrent ports/sockets
16+
##-env ERL_MAX_PORTS 4096
17+
18+
## Tweak GC to run more often
19+
##-env ERL_FULLSWEEP_AFTER 10

‎test/config_test.exs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
defmodule ConfigTest do
2+
use ExUnit.Case, async: true
3+
4+
alias Mix.Releases.Config
5+
alias Mix.Releases.Config.ReleaseDefinition
6+
7+
@standard_app Path.join([__DIR__, "fixtures", "standard_app"])
8+
9+
describe "standard app" do
10+
test "can load config" do
11+
config = Mix.Project.in_project(:standard_app, @standard_app, fn _mixfile ->
12+
Mix.Releases.Config.read!(Path.join([@standard_app, "rel", "config.exs"]))
13+
end)
14+
expected = %Config{releases: [
15+
%ReleaseDefinition{name: :standard_app, version: "0.0.1", applications: [:elixir, :iex, :sasl, :standard_app]}
16+
]}
17+
assert ^expected = config
18+
end
19+
end
20+
end

‎test/fixtures/standard_app/.gitignore

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps
9+
10+
# Where 3rd-party dependencies like ExDoc output generated docs.
11+
/doc
12+
13+
# If the VM crashes, it generates a dump, let's ignore it too.
14+
erl_crash.dump
15+
16+
# Also ignore archive artifacts (built via "mix archive.build").
17+
*.ez
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
use Mix.Config
4+
5+
# This configuration is loaded before any dependency and is restricted
6+
# to this project. If another project depends on this project, this
7+
# file won't be loaded nor affect the parent project. For this reason,
8+
# if you want to provide default values for your application for
9+
# 3rd-party users, it should be done in your "mix.exs" file.
10+
11+
# You can configure for your application as:
12+
#
13+
# config :standard_app, key: :value
14+
#
15+
# And access this configuration in your application as:
16+
#
17+
# Application.get_env(:standard_app, :key)
18+
#
19+
# Or configure a 3rd-party app:
20+
#
21+
# config :logger, level: :info
22+
#
23+
24+
# It is also possible to import configuration files, relative to this
25+
# directory. For example, you can emulate configuration per environment
26+
# by uncommenting the line below and defining dev.exs, test.exs and such.
27+
# Configuration from the imported file will override the ones defined
28+
# here (which is why it is important to import them last).
29+
#
30+
# import_config "#{Mix.env}.exs"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
defmodule StandardApp do
2+
use Application
3+
4+
# See http://elixir-lang.org/docs/stable/elixir/Application.html
5+
# for more information on OTP Applications
6+
def start(_type, _args) do
7+
import Supervisor.Spec, warn: false
8+
9+
# Define workers and child supervisors to be supervised
10+
children = [
11+
# Starts a worker by calling: StandardApp.Worker.start_link(arg1, arg2, arg3)
12+
# worker(StandardApp.Worker, [arg1, arg2, arg3]),
13+
]
14+
15+
# See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
16+
# for other strategies and supported options
17+
opts = [strategy: :one_for_one, name: StandardApp.Supervisor]
18+
Supervisor.start_link(children, opts)
19+
end
20+
end

‎test/fixtures/standard_app/mix.exs

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
defmodule StandardApp.Mixfile do
2+
use Mix.Project
3+
4+
def project do
5+
[app: :standard_app,
6+
version: "0.0.1",
7+
elixir: "~> 1.3-rc",
8+
build_embedded: Mix.env == :prod,
9+
start_permanent: Mix.env == :prod,
10+
deps: deps]
11+
end
12+
13+
# Configuration for the OTP application
14+
#
15+
# Type "mix help compile.app" for more information
16+
def application do
17+
[applications: [:logger],
18+
mod: {StandardApp, []}]
19+
end
20+
21+
# Dependencies can be Hex packages:
22+
#
23+
# {:mydep, "~> 0.3.0"}
24+
#
25+
# Or git/path repositories:
26+
#
27+
# {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"}
28+
#
29+
# Type "mix help deps" for more examples and options
30+
defp deps do
31+
[]
32+
end
33+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
use Mix.Releases.Config
2+
3+
config debug?: false,
4+
include_erts: true
5+
6+
release :standard_app, version(:standard_app)

‎test/test_helper.exs

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)
Please sign in to comment.