While I think there’s a lot of good points addressed with the above changes, I do think that keeping single system of configuration files and using it at build and run time will be confusing.
At the moment you have to do some gymnastics to get it working with
replace_os_vars and special escaping of strings to support runtime configuration options. It’s not ideal.
I think that inversing the way config files are required would also be beneficial, and precisely for the case of umbrella apps where at the moment you can end up with different config when you start something from app directory than you would get from umbrella root. For the simple umbrellas many people would be happy with just top-level config files, and we could get rid of in-app-directory config files altogether. Single config to rule them all, that’d be good, while not taking away the flexibility to do it your way.
Having said that all above, when we inverse things and have the configuration evaluated by default, there will be a lot of compile-time settings being evaluated at runtime. This is not great because the user cannot immediatelly tell what is what. Let’s have a look at few examples from my config files:
config :logger, :console,
format: "$time $metadata[$level] $message\n",
metadata: [:request_id, :application, :module, :ip, :endpoint, :uid, :oid, :type, :custom]
is this a run-time or compile-time setting?
How about Phoenix endpoint like this:
config :ui, UI.Endpoint,
url: [host: "localhost"],
render_errors: [view: UI.ErrorView, accepts: ~w(html json)],
pubsub: [name: UI.PubSub, adapter: Phoenix.PubSub.PG2]
Is this compile-time, or run-time? Can you swap “host” at run-time? Can you change others too? See, this is confusing if this is going to be executed at run time and compile time alike.
At least currently you can, looking at the config files identify which are run time options (because you used escaping/env vars) on these. It’s not ideal but you can figure it out having already existing config file in front of you. Think about that new dev-ops engineer who is taking over maintaining of the project. They’ll know what the settings are by looking at the current config files - something they no longer can do if we implement evaluation of config files at run-time by default.
What I would propose instead
1. Let’s keep the current system but make it slightly more flexible, so you can actually inverse the control yourself.
This means we add
config_path as proposed by @josevalim, and it would default to
config/config.exs. From there, it can either include other config files (as does now) or you can walk away and inverse the control by not including other configs in that file and manipulating
config_path instead. New apps, generators etc. can do it by default at some point.
2. Introduce runtime config
That can be
config/runtime_config.exs by default. That can work precisely the same as current
Mix.Config, but without relying on
Mix to exist, like in the proposal above. To make things consistent, I would even allow these config files to include other config files. In short:
Mix.Config would just be kept as name for backward compatability, but the code itself can be moved to
Application.Config so we do not maintain 2 code bases.
The runtime config is then clear place where you put your configuration that is going to be evaluated at run time, so you can expect
System.get_env and similar to work here.
In dev, when you run
iex -S mix it would:
config/config.exs (i.e. compile-time config)
- compile the app
config/runtime_config.exs (i.e. run-time config)
- run the app
while in prod, when started a release executable, where there is no mix only steps 3 & 4 would be performed.
The proposal above is 100% backward compatible. We can give time for library authors to separate their run-time and compile-time configuration options. Phoenix would generate some compile-time config in
config/config.exs, but would move run time configuration to
config/runtime_config.exs by default. If you still need to use some old hex package and are forced to use old-style runtime config, you will still be able to do it with this hybrid approach. But with time we get to end up with nice separation of run-time and build-time configuration options, which will make dev-ops engineer’s lifes way easier.