This is going to be an oddball question and not really something that others are going to commonly encounter. I’m also pretty sure that what I’m going to be asking about is simply not possible… but as I prove time and time again that I don’t know everything: here I am, question in hand.
The Short, Less Clear Version
I have an Elixir project which is library-like and will be used in several other Elixir projects. This library project will also have some dependencies on other Elixir projects I’ve also built. Some of those dependencies could benefit from the library itself and so would be great to include in those projects; but of course, this creates circular references between projects. So the question becomes this: is it possible to programmatically exclude a project’s dependencies at compile time (or deps.get
time which seems more likely to be what I need).
The Long, Possibly More Clear Version
(Note that I’ll be simplifying some things here for the purposes of discussion)
My application is built up from a fair (and growing) number of library-like Elixir projects which are dedicated to narrowly defined feature sets; I’ll call these “Components” because they aren’t really true libraries like you might find via Hex. Some of these Components define runtime services which the top-level application ultimately will be responsible for starting and supervising. At the end of the day, however loosely, these Components are intended to work together and therefore can assume that some of these services will have been started and supervised by that top-level application.
In terms of building a running application which I can build successfully into releases, all of this works fine and as intended. There is no issue of circular dependencies in the normal course or even threat thereof; this is avoided by design.
However… during REPL aided development and during testing, having some of these services up-and-running in some minimal way can be very helpful (or in the case of testing actually essential). Up to this point I have, in each Component that would be helped in this way, manually defined helper functions to get the requisite services loaded/started. This works, but results in a lot of boilerplate, duplicating the same configurations, supervisor setups, childspecs etc. across components where this needs to happen. I hate that. So, I think to myself, why not create yet another Component that’s only available when Mix.env()
is either :dev
or :test
encapsulating these helper common services? I’ll still have a little boilerplate, but it will be much minimized.
This utility Component also works well much of the time, but there are some cases where this can end up in circular dependencies. Consider the following scenario:
I have the following hypothetical Components:
-
Db
- functionality to build, load, start, etc. involving databases. -
Flags
- Feature Flag-like functionality backed by an ETS table fronted by a GenServer holding runtime-manageable configurations. Depends onDb
for persistence. -
DevUtils
- the:dev
/:test
only support Component which provides and extends features from bothDb
andFlags
for REPL aided development and testing. Depends onDb
andFlags
. -
Features
- higher level business logic. Depends onDb
,Flags
, andDevUtils
(only:dev
/:test
). -
App
- the top-level application. Depends onDb
,Flags
,Features
, andDevUtils
(only:dev
/:test
)`.
As defined above, things work OK. But notice that Flags
depends on Db
. This means maintaining Flags
would benefit from the Db
supporting parts of DevUtils
and there’s the rub: DevUtils
already depends on Flags
. Trying to make DevUtils
a dependency of Flags
results in a circular dependency.
The Question
Given the above, ideally, I’d love for DevUtils
, while being added as a dependency to Flags
, to see that fact and to not try to use Flags
as its own dependency, thus avoiding the circular dependency. So… simply put, is there a way to do that?
Naturally, I’ve tried a few things hoping it might work (for example optional: true
in relevant dependencies)… but the more typical mix.exs
dependency exclusions or conditional compilation methods I expect won’t work because I’m relatively sure it’s already too late by the time you get to compilation where those things would matter; the following seems to suggest as much:
While I expect that’s still true and that I take its meaning correctly, that quote is nonetheless pretty old and on the off-chance things have changed or I’m missing something, I thought I’d ask.
This is nothing urgent and I have acceptable “plan B” approaches which avoid the circular dependencies, but all my alternates are less clean than having a singular DevUtils
package which supports all the development support scenarios I’d like it to.