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.
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).
(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
: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 on
:testonly support Component which provides and extends features from both
Flagsfor REPL aided development and testing. Depends on
Features- higher level business logic. Depends on
App- the top-level application. Depends on
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.
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.