Question/Idea: Can we compile umbrella apps in same order they're started so that implicit dependencies are impossible without causing warnings?

So I was just chatting with a co-worker about pros and cons of Umbrella apps, and we’ve occasionally had a module that didn’t have tests that had dependency on an app in another umbrella app that wasn’t declared/intentional. But it got me wondering, why can’t we have the compiler start clean from the deepest dependency in the umbrella app, and if using --warnings-as-errors have it warn and error if any module/function is referenced that’s not defined in that app or one of it’s dependencies? Maybe I’m missing something, or maybe there’s already a way to do this? (we didn’t always have warnings-as-errors turned on in our deploy compile, so I suppose I could be remembering from before that?)

Curious for thoughts or ideas.

1 Like

I am running into this now. I haven’t been able to figure out how to get rid of the compiler warnings in app 2 when app 2 uses modules from app 1 and app 1 already lists app 2 as a dependency in its mix.exs, which means app 2 cannot list app 1 in its mix.exs as that could cause a dependency cycle. When I run mix compile, app 2 always gets compiled first. Is it done alphabetically?

1 Like

As the doctor said to the patient, “have you considered not doing that?” :slight_smile:

App 1 can’t be compiled first, because it needs to have its dependencies compiled first - including app 2!

What you’ve got is a circular dependency; the way to fix the warning is to remove the cycle.

A general approach that I’ve found useful for these situations is to replace the backwards-link with a runtime reference. In app 1, you’d write code like:

thing_from_app2 = App1.App2Bridge.module().some_function_from_app2(...)

# in app1/app2_bridge.ex
defmodule App1.App2Bridge do
  def module do
    Application.get_env(:app1, :app2_module)
  end
end

# in config/config.exs or similar

config :app1, :app2_module, App2.Whatever

This breaks the cycle, but it has a cost: App 1 can’t give any compiler errors if code from app2_module is misused (called with wrong arity, etc) because it doesn’t see those definitions.

You can mitigate that by defining the desired functions explicitly in App1.App2Bridge, but the compiler can’t help you keep those definitions aligned with the definitions from App2.

You can further mitigate that by making App1.App2Bridge a behavior and implementing it on the App2 side; this is allowed since App2 depends on App1. This could also be handy if you’re an ardent devotee of the “fully isolated unit tests” philosophy, since Mox etc depend on behaviors.

1 Like

Yes, of course, but that isn’t relevant to the question. This is a new codebase in a new job. The eventual goal is to get rid of the umbrella app and merge everything into a single, normal Elixir project. But first things first.

Everything compiles and runs just fine. So this somehow works aside from the compiler warning. (It’s my understanding that umbrella apps still run in the same instance. As an aside, I’m not terribly sure of the point of umbrella apps.) I am just trying to get rid of as many compiler warnings as I can, but I suppose this one in particular is of low priority and just a warning we’ll deal with for the time being until we refactor. Perhaps its even beneficial as a reminder to clean up dependencies.