How to switch between local or Hex dependency?

For my public libs I have created demo apps. During development the demo app should depend on the development version of the library; so I use path: ../my_lib. However, when the demo app is cloned by a user it should depend on the Hex version instead.

Currently I set an environment flag which toggles the location of the dependency, but it keeps nagging in my head: this should be easier. Maybe detecting the existence of a file not committed to Git…?

Anyone having a better idea?

Ps. Detecting :dev runtime environment is not gonna cut it, as the user will probably run the demo in :dev too.

1 Like

This may not work for you but taking inspiration from Doggo, I package the demo app alongside the library in the same repo. While there some annoyances around grepping and how some editor tools handle nested mix.exs, it’s generally really convenient for everyone: 1) You can simply leave the dep as {:my_lib, path: ".."} in your demo’s mix.exs, 2) the demo repo is source controlled alongside the lib, and 3) when users only have to clone one repo to get everything.

I can imagine this isn’t always viable so I’m also interested in other options.

I do this in the demo for one of my projects, in “mix.exs”:

defp deps do
  [
    {:phoenix, "~> 1.7.14"},
    ...,
    my_lib_dep()
  ]
end

defp my_lib_dep() do
    if File.exists?(Path.join(__DIR__, "../my_lib/mix.exs")) do
      {:my_lib, path: "../my_lib"}
    else
      {:my_lib, "~> 0.1.0"}
    end
end

Or you can append an entire list to the end if you have multiple deps. The nice thing about “mix.exs” is that it’s code, and is executed every time you run mix.

2 Likes

I didn’t do it this way because it feels wrong to reach out into an unsuspecting user’s filesystem to execute code (at least from an unsanctioned directory). While I’m fully aware it’s super edge case, you never know what people have on their machines. For example, maybe they clone the example app into a directory where a long time ago they were experimenting with a mix project with the same name as your lib. Confusion ensues. It’s certainly an option, but I wish there was a more “official” way.

If you don’t like that option, you can always use a environment variable to achieve the same, the important point is as @Nicodemus mentioned: you can execute elixir code inside of mix.exs, which is a very powerful concept.

@BartOtten already mentioned the env variable approach which I do like better. I’m certainly aware mix.exs is just code which I thought was implied by my saying I already considered @Nicodemus’s approach (well, same goes for checking an env var, really).

1 Like

Indeed, after reading it again I misunderstood the question.

Addressing your previous concerns, just opening a project with a LSP carries risk of executing stuff you are unaware of, especially since elixir metaprogramming allows execution of code at compile-time.

Current implementation is like this. I did commit an .envrc file which sets the path. It seems checking path existence is the only ‘fully native’ solution. Combining it with env override and a warning might hit the sweet spot.

defp routex_dep() do
    if path = System.get_env("ROUTEX_PATH") do
      IO.puts(">> !! USING LOCAL ROUTEX PATH !! <<")
      {:routex, path: path}
    else
      {:routex, ">= 0.0.0"}
    end
  end
2 Likes

If the solution works without many hitches, what is inherently wrong with it?

I guess you could use a mix task to ask the user the default option before he starts the project, but I don’t see that as a much better approach.

Not much. But as I can imagine others having the same use case, I wondered if there would be a better way.

And because I rather have a minimum of helper apps (such as direnv).

—nvm—…

Ya, same here. There are easy ways to make it work but none that I can think of feel “right.” Though I do quite like the “Doggo way.” It does also mean shipping the demo with package but it’s not like it gets compiled into peoples’ apps or anything. And of course if you want multiple demos then it’s a pain.

Something that might be useful is that Mix.env() returns :prod when your package is compiled for use in a user’s application.

So

@dependency if Mix.env() == :prod do
  {:package, "~> ..."}
else
  {:package, path: "..."}
end
2 Likes

I awarded Zach the solution but….had to revert.

When ExampleApp uses this code, it will still always fetch MyLib from Hex, no?

oh, right :laughing:

Question: did you see my Oscar quality presentation of the wrong solution pick? :sweat_smile:

Gotta get some sleep now. Just came back very fast as my brain corrected me just before I catched sleep.

So far my top pick: Demo code in same repo (excluded from Hex package)

Pre:

  • use demo app for integration test
  • (major) 1 clone and easy hacking in both example as lib
  • no split commits; no sync issues

——-

Also on the table: dotfile + env var

  • Presence check a dotfile which is .gitignored. (Default). If available, use path: …/my_lib
  • If env set and path exists, use path of env var. (Override)
  • If env set and false: use Hex (Override)

Pre over only env variable:

  • no need for an env-setter.
  • no clobbering ~.profile etc.
  • shell and terminal agnostic.
  • Easy (un)set: touch .local / rm .local.

Pre over only path check:

  • can set set non-default path.
  • can force Hex.

no env, no file == no possible path execution

1 Like

You sure?

No, of course not! :sweat_smile: I have a problem with being either overly diplomatic or overly blunt. When I say “super edge case” that is my diplomatic way of saying: “this will definitely 100% absolutely for sure ruin someone’s day one day but if you don’t care about that one person then go ahead” (apparently I throw some passive aggression in there as well)

The dotfile is a decent approach since I’m realizing this problem can be summed up as: “Only compile this dep from source if it I’M the one working on it.” This is exactly the use-case for dotfiles. It’s simple to throw these instructions in a README or CONTRIBUTING doc for those looking to hack on your lib. I do still prefer collocation, though (seems you do as well!) and will be keeping it up in my own situation.

To clarify for those who didn’t look at Doggo, the example app lives in the root of the package alongside lib, priv, etc… ie, there’s no parent directory contained the two. I’m just making sure as I’ve been misunderstood more than a couple of times today. Blame Halloween? I dunno.

OK, I’m convinced, and will probably switch to the dotfile solution instead of path existence check.

I considered colocation, but I have many different demos for my library and don’t want them all colocated. I’d rather keep them very closely to a “stock” mix phx.gen codebase for easier understanding by newbies.