defmodule Burrito.Builder do alias Burrito.Builder.Context alias Burrito.Builder.Log alias Burrito.Builder.Target alias Burrito.Steps.Build alias Burrito.Steps.Fetch alias Burrito.Steps.Patch @moduledoc """ Burrito builds in "phases". Each phase contains any number of "steps" which are executed one after another. There are 3 phases: `:fetch` - This phase is responsible for downloading or copying in any replacement ERTS builds for cross-build targets. `:patch` - The patch phase injects custom scripts into the build directory, this phase is also where any custom files should be copied into the build directory before being archived. `:build` - This is the final phase in the build flow, it produces the final wrapper binary with a payload embedded inside. You can add your own steps before and after phases execute. Your custom steps will also receive the build context struct, and can return a modified one to customize a build to your liking. An example of adding a step before the fetch phase, and after the build phase: ``` # ... mix.exs file def releases do [ my_app: [ steps: [:assemble, &Burrito.wrap/1], burrito: [ # ... other Burrito configuration extra_steps: [ fetch: [pre: [MyCustomStepModule, AnotherCustomStepModule]], build: [post: [CustomStepAgain, YetAnotherCustomStepModule]] # ... ] ] ] ] end # ... ``` """ @phases [ fetch: [Fetch.Init, Fetch.FetchMusl, Fetch.ResolveERTS], patch: [Patch.CopyERTS, Patch.RecompileNIFs], build: [Build.PackAndBuild, Build.CopyRelease] ] def build(%Mix.Release{} = release) do options = release.options[:burrito] || [] debug? = Keyword.get(options, :debug, false) build_targets = options[:targets] # look for override target in system env # if it's a valid target, set it as the only target target_override_string = System.get_env("BURRITO_TARGET") build_targets = if target_override_string do Log.warning( :build, "Target is being overridden with BURRITO_TARGET #{target_override_string}" ) override_atom = try do String.to_existing_atom(target_override_string) rescue _ -> raise_invalid_target(target_override_string) end # If we have a named target defined that matches this atom use that # otherwise :error, not a valid target cond do Keyword.has_key?(build_targets, override_atom) -> Keyword.take(build_targets, [override_atom]) true -> raise_invalid_target(override_atom) end else build_targets end # Build every target Enum.each(build_targets, fn {name, t} -> %Target{} = target = Target.init_target(name, t) target = %{target | debug?: debug?} self_path = __ENV__.file |> Path.dirname() |> Path.split() |> List.delete_at(-1) |> List.delete_at(-1) |> Path.join() initial_context = %Context{ target: target, mix_release: release, work_dir: "", self_dir: self_path, extra_build_env: [], halted: false } Log.info(:build, "Burrito is building target: #{target.alias}") Log.info( :build, "Burrito will build for target:\n\tOS: #{target.os}\n\tCPU: #{target.cpu}\n\tQualifiers: #{inspect(target.qualifiers)}\n\tDebug: #{target.debug?}" ) phases = [ fetch: options[:phases][:fetch] || @phases[:fetch], patch: options[:phases][:patch] || @phases[:patch], build: options[:phases][:build] || @phases[:build] ] Enum.reduce(phases, initial_context, &run_phase/2) end) # All done! release end defp run_phase({phase_name, mod_list}, %Context{} = context) do Log.info(:phase, "PHASE: #{inspect(phase_name)}") # Load in extra steps, pre and post extra_steps = context.mix_release.options[:burrito][:extra_steps] extra_steps_pre = extra_steps[phase_name][:pre] || [] extra_steps_post = extra_steps[phase_name][:post] || [] mod_list = extra_steps_pre ++ mod_list ++ extra_steps_post Enum.reduce(mod_list, context, fn mod, %Context{} = acc -> %Context{} = new_context = mod.execute(acc) # Halt if `halt` flag was set if new_context.halted do Log.error( :build, "Halt requested from phase: #{inspect(phase_name)} in step #{inspect(mod)}" ) exit(1) end new_context end) end def raise_invalid_target(target) do raise "#{target} is not a valid target!" end end