Elixir 1.13 does not find dependency anymore ... in some environments

Hi.

I’m using the :syn library as a dependency in an umbrella app.

In my root level mix.exs it is defined like this:

def project do
  [ apps_path: "apps",
    build_embedded: Mix.env == :prod,
    start_permanent: Mix.env == :prod,
    deps: deps(),
    releases: releases() ]
end

def deps do
  [ {:syn, path: "deps/syn", override: true, app: false},
[...]

(I copy my dependencies into the deps folder before compiling because we have a policy of not pulling from the internet during compilation.)

And my releases for the build I’m running looks like this:

defp releases do
  [ unified_host:         unified_host(),
    unified_target:       unified_target() ]
end

defp unified_host do
  [ steps:                   [ :assemble, &create_tarball/1 ],
    include_executables_for: [:unix],
    include_erts:            true,
[...]
    applications:            unified_apps() ]
end

[...]
defp unified_apps do
  [ runtime_tools:    :none,
    poison:           :load,
    sweet_xml:        :load,
    syn:              :load,

(I only “load” :syn since I manually start it later during application start.)

I have an in-umbrella app named “common” that requires :syn. Its mix.exs looks like this:

  def project do
    [app: :common,
[...]
     compilers: compilers(),
     elixirc_options: [ warnings_as_errors: true, long_compilation_threshold: 30 ],
     deps: deps()]
  end

  # replace default Erlang compilation with parallel one
  # this one is specifically tuned for compiling our
  # huge Erlang files faster
  Code.require_file("erlang.parallel.ex",     "lib")
  Code.require_file("compile.erlparallel.ex", "lib")
  defp compilers, do: [ :erlparallel | List.delete(Mix.compilers, :erlang) ]

[...]
  def deps do
     [{:syn,        in_umbrella: true,         override: true, app: false},
[...]
end

Compilation is done within a Docker image. When I run this “locally,” all works fine. My applications are compiled - dependencies first - and I see both dependencies from umbrella mix.exs and apps/common/mix.exs. Then it starts compiling the “common” app. It passes and continues to compile other umbrella apps that are dependent on common, etc.

===> Compiling syn
==> common
Compiling 8 files (.erl)

If the same Docker image and code is run on a CI host, the console is flooded with warnings like this one:

warning: :syn.send/2 is undefined (module :syn is not available or is yet to be defined)
Invalid call found at 2 locations:
  lib/interface.ex:30: If.Dispatcher.Registry.send/1
  lib/interface.ex:30: If.Dispatcher.Registry.strict_send/1

Even though:

11:32:23 ===> Compiling syn
11:32:29 ==> common
11:32:29 Compiling 8 files (.erl)

The same steps, same tooling from Docker image, the same mix.exs files, different machine. Unfortunately I have no access to the machine to start the same build manually. The only difference I can immediately see is that the “CI machine” (where it fails) has 16 cores and the “local” work server I have access to has 48 cores.

With elixir 1.10.4 this approach was working locally and in CI.
With elixir 1.13.4 this approach is working locally but not in CI.

I’m really stumped and would appreciate some help.

I can see that you define the path: "deps/syn". Why do you do this?
And how do you pull syn into deps directory in CI and locally?

Is this correct? :syn is not an umbrella sibling, but rather in the deps/syn folder, no?

I can see that you define the path: "deps/syn" . Why do you do this?

I think it was required to compile the override dependency originally. It’s been there a long time…

And how do you pull syn into deps directory in CI and locally?

It is copied from a static version in the local file system.

Oh yes, you’re right.

Let me double check when I introduced this…

An earlier version had it like this:

  def deps do
    base_path = case Mix.env do
      :test -> "../.."
      _     -> "."
    end

    [{:syn,        path: "#{base_path}/deps/syn",        override: true, app: false},

Which I set it back to and recompiled, but the problem is the same - it can’t see :syn.

The interesting thing is that it works on re-runs. So it seems like a problem of order, still.

If I delete the contents of the deps/ folder, then let the script copy in the (uncompiled) dependencies, then run - I get the problem. (So this is the blank slate CI starts from every time.) Even though syn is supposed to be compiled long before the files in “common”.

If I leave the contents of the deps/ folder (which now contain the ebin folder with the .beam files from previous compilation) - I don’t get the problem.

I guess that’s why I missed the problem locally. At least it’s not a difference between machines.

So if these are present in deps:

ls deps/syn/ebin/
syn.app  syn_app.beam  syn_backbone.beam  syn.beam  syn_event_handler.beam  syn_groups.beam  syn_registry.beam  syn_sup.beam

Then I have no problem. But if they are generated by the same compilation run, I have a problem.

[EDIT:] I also see once compiled, :syn does have an .app file. I added the “app: false” option only because compilation was complaining it didn’t have an app file - is this also a problem of when the file was made?

Unchecked dependencies for environment dev:
* syn (deps/syn)
  could not find an app file at "_build/dev/lib/syn/ebin/syn.app". This may happen if the dependency was not yet compiled or the dependency indeed has no app file (then you can pass app: false as option)
** (Mix) Can't continue due to errors on dependencies

This problem also goes away if and only if the file is present in deps/syn/ebin from the start - basically when starting compilation a second time with the deps/syn/ebin folder intact.

Is this a problem of me copying the dependencies into the deps folder manually? Has something changed between elixir 1.10.4 (where it works) and 1.13.4 (where it doesn’t)?

It should be fine to point to deps/syn but I would also suggest (for unrelated reasons) to have them in vendor/syn. deps is usually a directory one can delete, but that’s not true for you.

I am out of ideas. If you can isolate it, I will be glad to take a look. Nothing should have changed here. You might as well try 1.14.0.

Thank you, José.

I’m trying to assemble a minimal project that resembles my situation, but I’m seeing differences right off the bat.

I’m using mix from elixir 1.13 for generating the project layout:

mix new compile_demo_113 --umbrella
cd compile_demo_113/apps/
mix new inner_app
mix new outer_app
cd ..
mkdir deps
cp -R <local>/syn deps/

In the most simple version, umbrella mix.exs:

defmodule CompileDemo.MixProject do

use Mix.Project

def project do
  [
    apps_path: "apps",
    version: "0.1.0",
    start_permanent: Mix.env() == :prod,
    deps: deps()
  ]
end

defp deps do
  [{:syn, path: "deps/syn", override: true}]
end

end

InnerApp:

defmodule InnerApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :inner_app,
      version: "0.1.0",
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.13",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [ {:syn, path: "./deps/syn" }
  end

With a reference in the code:

defmodule InnerApp do
  def hello do
    :syn.start
  end
end

OuterApp:

defmodule OuterApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :outer_app,
      version: "0.1.0",
      build_path: "../../_build",
      config_path: "../../config/config.exs",
      deps_path: "../../deps",
      lockfile: "../../mix.lock",
      elixir: "~> 1.13",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      extra_applications: [:logger]
    ]
  end

  defp deps do
    [ {:inner_app, in_umbrella: true, runtime: false}  ]
  end
end

With a reference in the code:

defmodule OuterApp do
  def hello do
    InnerApp.hello
  end
end

When I use this simple version and compile, I get:

# mix compile
===> Analyzing applications...
===> Compiling syn
==> inner_app
Compiling 1 file (.ex)
Generated inner_app app
==> outer_app
Compiling 1 file (.ex)
Generated outer_app app

Two differences stand out:

  • In my real-life application, building with elixir 1.13 never prints “===> Analyzing applications…” at any point. It jumps straight to build dependencies.
  • Building the sample creates a _build folder in deps/syn which only contains a file named deps/syn/_build/prod/lib/.rebar3/rebar_compiler_erl/source.dag, and the .beam files appear under global _build/dev/lib/syn/ebin

In contrast, in my actual application building syn actually creates:

  • The .beam and .app file in deps/syn/ebin
  • The only thing created in global _build for syn is a file named _build/dev/lib/syn/mix.rebar.config which contains this line only: {overrides,[]}.
  • In contrast, all other dependencies show up in _build/dev/lib with ebin sub-folders.
  • If I rebuild without cleaning, a symlink shows up in _build/dev/lib/syn which points to ebin -> ../../../../deps/syn/ebin - and that’s why a second compilation run works
  • If I clean up and rebuild with elixir 1.10.4 from scratch, the _build/dev/lib/syn / ebin -> ../../../../deps/syn/ebin symlink is already there on first build.

So, this at least explains at least why syn modules aren’t found in the original run but in a second run. They aren’t in _build altogether in my original case.

I’m not sure why the syn dependency gets this special treatment.

Right. Given it is the same Elixir and Rebar versions and it behaves differently, there is likely additional configuration in your project making a difference?

I have found a rather minimal configuration that reproduces the problem in the sample umbrella app.

In order to compile other dependencies in my original project I need a local version of Hex without having to install it to the user (like I would not want to in CI), so I have to set a MIX_HOME.

MIX_HOME=<…some path…>/mix_home_10 mix release

This mix home contains the following:

mix_home_10/
mix_home_10/rebar
mix_home_10/archives
mix_home_10/archives/hex-0.20.5
mix_home_10/archives/hex-0.20.5/hex-0.20.5
mix_home_10/archives/hex-0.20.5/hex-0.20.5/.elixir
mix_home_10/archives/hex-0.20.5/hex-0.20.5/ebin
mix_home_10/archives/hex-0.20.5/hex-0.20.5/ebin/Elixir.Hex.Crypto.AES_CBC_HMAC_SHA2.beam
[...]
mix_home_10/archives/hex-0.20.5/hex-0.20.5/ebin/Elixir.Hex.Crypto.ContentEncryptor.beam
mix_home_10/rebar3
mix_home_10/protoc-gen-elixir

So basically:

  • a fixed rebar executable
  • a fixed rebar3 executable
  • Hex 0.20.5 (supposedly still compatible with 1.13.4)
  • a precompiled escript for generating protobuf files

And then I get the error!

# MIX_HOME=<...some path...>/mix_home_10 mix release
===> Compiling syn
Unchecked dependencies for environment dev:
* syn (deps/syn)
  could not find an app file at "_build/dev/lib/syn/ebin/syn.app". This may happen if the dependency was not yet compiled or the dependency indeed has no app file (then you can pass app: false as option)
** (Mix) Can't continue due to errors on dependencies

As soon as I do that, the “ebin” folder is created inside deps/syn and the problem occurs.

I’ll try getting a newer static version of rebar3 and see if it helps.

I got the newest version of rebar3 wget https://s3.amazonaws.com/rebar3/rebar3 && chmod +x rebar3

But I get this error when executing it during build from a MIX_HOME directory:

escript: exception error: undefined function rebar3:main/1
  in function  escript:run/2 (escript.erl, line 758)
  in call from escript:start/1 (escript.erl, line 277)
  in call from init:start_em/1
  in call from init:do_boot/3
** (Mix) Could not compile dependency :syn, "/workspace/mix_home_13/rebar3 bare compile --paths /workspace/l2_l3_test/l3_framework/_build/dev/lib/*/ebin" command failed. Errors may have been logged above. You can recompile this dependency with "mix deps.compile syn", update it with "mix deps.update syn" or clean it with "mix deps.clean syn"
ERROR: 1

I found a version of rebar3 not exposing the problem.

It was the one in my regular ~/.mix directory. (That’s why it worked before when compiling without setting an explicit MIX_HOME.)

This solves my issue - right rebar3 version.

Thank you for all your help! :slight_smile:

I’m not sure if this is related, but are you using the “official” docker image? There have been some breaking changes reported on v1.13: Issues · erlef/docker-elixir · GitHub

If your CI server pulls the image each time, but your local environment doesn’t, the images could differ, because the tags are not immutable.

edit: posted before I saw the latest replies above.

Hello, @adamu.

Thanks for reminding me but I do not. I was referring to a proprietary image.