Proposal: moving towards discoverable config files

Another idea, which might make the difference between compile-time configuration vs run-time(/on-boot) configuration more clear to the user but only in the instances they have to care about it is to:

  • Introduce Application.Config which has the same behaviour as Mix.Config but lives in :elixir instead of :mix so it is available in Releases. (as in original proposal)
  • Choose which configuration files (and the order they should have) in the mix.exs file by using a :config_paths key.(as in original proposal)
  • Expose whether we want to look at the application’s mix.exs information for compile-mode or for run-time/boot-time-mode so that we can in e.g. the implementation of the mix.exs's project() fields (most importantly: :config_paths) potentially choose between different groups of configuration files. This is very similar to how different Mix environments are currently used to e.g. switch between what dependencies you include.
  • Using import_config just like you do today (including string-replacements) is still allowed, but we might decide to deprecate this feature, and definitely show warnings (or potentially hard errors) when the configuration file that was linked to can not currently be found because its string replacement was ‘too smart’, and point the user towards the new way to configure their application using a conditional config_paths: list.

I think that when we would set it up like this, the abstraction is the least leaky.
Example usage:

  1. A user that starts an Elixir project does not need to care about the difference between compile-time and run-time because exactly the same configuration is read by default (because the value for :config_paths is constant)
  2. If they start using Releases and find out that they want to perform some configuration differently at boot-time, they can change :config_paths.
  3. If they try to use configuration that is ‘too smart’ to work on boot, we can in most cases generate a warning during compilation-time (and otherwise a hard error during boot time) that should tell the user to rewrite the configuration using a conditional :config_paths setup.

In the simplest case, it would looke like:

def project do
[
  ...
  config_paths: ~w(config/config.exs config/#{Mix.env()}.exs)
  ...
]
end

Expanded to do different things based on compile-time/boot-time mode:

def project do
[
  ...
  config_paths: ~w{config/base_config.exs} ++ config_paths(Mix.env(), Mix.configuration_target())
  ...
]
end

def config_paths(_, :compile_time), do: ~w{config/compile_time_overrides.exs}
def config_paths(:dev,  :boot_time) do: ~w{config/production_boot_overrides.exs config/production_secrets.exs}
def config_paths(:prod, :boot_time) do: ~w{config/dev_boot_overrides.exs}

This proposed solution is backwards compatible, because:

  1. switching in an existing project from Mix.Config to Application.Config is optional (although we probably should make Application.Config the new default for new projects).
  2. Mix.Config will just like now work as it always did: Releases only can use it during compiletime, applications running through mix use it at compiletime + boot-time.
  3. When using Application.Config, we look at the :config_paths field in the mix.exs file. This field (and potentially other fields of the project() function) might return different results based on the current Mix.configuration_target() (Maybe we can find a better name for configuration_target?), which will allow Releases to know what files to include for boot-time, and will allow all projects to only expose the correct configuration in the correct configuration environment.
  4. Application.Config will complain if import_config is encountered inside a configuration file that is flagged for inclusion into the list of boot-time configuration. Mix.Config will do no such thing and will happily ‘just not work’ on boot-time for a Release.