1. Having an “entrypoint to your application” isn’t the right way to think about an Erlang release.
An Erlang release is like a virtual appliance: an OS (ERTS) with a set of “service” packages installed in it (your apps and libraries.)
And, just like when assembling a VM instance using Terraform or the like, you can set up/install multiple root-level applications/services within that VM. Like, say, a LAMP stack. Different root-level services, none installed as a dependency of the other; all just running as siblings and configured to talk to one-another; all in one VM image. The VM image, so-composed, would be a “release” of a single networked system component. Not a single service, but a single set of services, deployed and upgraded atomically, along with the virtual-machine OS substrate they run on. A “node”, in Erlang parlance.
So, then, how do you set up an entrypoint for such an OS service? On Linux, you’d use systemd service-units. Each systemd service has its own entrypoint, configured by the unit file. Equivalently, each Erlang app has its own entrypoint, configured by the .app manifest. (Which is, in Elixir, generated from the Mix project file, which is why the application `mod` directive ends up there in Elixir.)
2. Those config files are the config files for the “OS” (ERTS), not for your app. They’re things that — in a different “multitenant” abstract-machine runtime, e.g. Smalltalk — would be hiding within the “image” that the emulator works with. Things that in a regular Linux VM, would be in /etc of the VM’s virtual block-device image.
Why are they there? Because Erlang is not designed under the expectation of heavy dev-ops collaboration. Releases may very well be created by an “upstream” of devs, and then thrown over a very tall wall to a hapless operations staff. The operations staff then has to deal with deploying this thing, where the only things they can tweak to get a release to work on a particular system, are those very config files. If they were inside the image, they’d have to ask the devs to burn them a new release with the fixes in place. As it stands, they can just tweak the deploy themselves.
3. And that’s also why there’s so many executables: a good few of them are different (static-compiled) emulators for the different operational deployment scenarios that won’t be known at build time, e.g. single-core vs. multi-core, where all this detail is abstracted away by detection-steps run just before emulator boot-time by those batch files. (Make no mistake: for any “runtime” package you might install — e.g. the JVM, the CLR, etc. — you get a similar menagerie of executables, just hidden somewhere out-of-sight.)
Oh, and some of them (e.g. EPMD) are just ERTS “daemons” that run as sub-processes of the emulator, rather than “in” the emulator. Since there are several variant emulators shipped in the release, burning this code into each of them and running it with fork(2), would result in more bulk to the release than just factoring it into a separate executable would. (And besides, Windows doesn’t fork(2).)
And also also, a more Erlang-y reason: isolating this code into separate processes, means you can validate it solely in terms of its failure-state IPC behaviour, rather than needing to take into account its failure-state emulator memory-state behaviour. It’s the same reason Erlang encourages the use of port processes over NIFs. It’s the same reason microkernels exist. Isolating failure, so things can crash hard, without the important things crashing.
4. The C header files are something you’ll see with any runtime that both ‘vendors’ the emulator itself; and ships a compiler accessible at runtime; and where that compiler supports FFI/building runtime extensions. In Erlang, you can run relups against a deployed release, that will install new Erlang applications into that release. If those Erlang apps contain native C code that needs to be compiled, the header files need to come from somewhere.
If they came from the host, they’d not be guaranteed to be compatible with the destination. Even if you wanted to set up some sort of cross-compilation toolchain matching the target, “the target” is a moving target, because relups can boot into a new version of the emulator; and because ops staff might independently upgrade/downgrade between relups (think “rolling upgrade failure”), meaning that any one of the set of so-far deployed copies of ERTS/BEAM might be the one running on any given node.
An Erlang node is a stateful, living system. Imagine it like a Windows virtual appliance that’s created as a series of Windows Deployment update files by a dev team; but where any given installation’s ops staff may-or-may-not choose to apply any given update pack. On such an appliance, the OS version isn’t really under the dev team’s control. Despite having atomic upgrades using atomic whole-release patches, it’s still not “immutable infrastructure” in the sense of e.g. a Docker image, where the whole image gets swapped out. And, as such, any given instance of the appliance can’t really be predicted in advance by the devs team, to have a particular OS version running on it. Rather, if the dev team wants generality, they have to build updates for multiple possible “base” versions of the OS; and then the update install system needs to interrogate/verify/select a matching update for the OS version that turns out to be running. And if they want specificity (e.g. to deliver a hot-fix update to a specific client), then they need to find out right before building the update, what OS version their appliance is currently running.
You can’t make the fully-general update-distribution problem any easier; but you can partially automate the hotfix-build-discovery problem. Just set up the virtual appliance so any running prod instance can be interrogated by your dev toolchain, whereupon it will deliver to your toolchain a tiny little cross-complication toolchain (i.e. C header files et al) precisely matching the running instance.
Which is... precisely what ERTS does. Relups are weird.
—————
Even what I said before (an Erlang release being a VM) is a bad abstraction — an Erlang release is an atomic patch of a VM, that the VM itself can then switch to. Like a base-image in CoreOS... but where the VM can switch to it without needing to reboot. That has a lot of complications.
Some languages (e.g. Go, Rust) are “closed-world”: they assume that, within some boundary (in Rust, a “crate”), everything will become fixed at compile-time, with nothing further able to change or intercede at runtime. These language compilers can thus execute Whole Program Optimizations.
Other languages (e.g. Java) are “open-world”: they assume that code can be loaded at runtime, right into the middle of any boundary you might draw; and, therefore, optimizations can only occur at the level of the code-unit (e.g. module, class), guaranteeing that all replacements will at least happen atomically at the level of the code-unit.
And then there’s Erlang, which takes “open-world” to a whole different level.
What you’re basically imagining here, is a version of ERTS that takes a “closed-world” assumption. No relups, no runtime module loading, maybe even burning the whole system into a single BEAM file with WPO. This would disable much of what makes Erlang, Erlang — but it would be possible. It’s just not possible to build this on top of the current OTP version of ERTS, since the open-world/closed-world assumption of a runtime is baked into basically every implementation decision of a VM and runtime at a deep level. You’d need to write your own (much less complex!) VM and runtime.