Repository: railwayapp/nixpacks
Pull request #485 (2022-08-31 11:00:56)

Add Elixir ecto & postgres support
<!-- Thanks for sending a pull request! -->
<!-- Please add a useful description explaining what this PR does -->

This PR adds support for detecting Ecto & Postgress to Elixir provider

<!-- PR Checklist  -->
<!-- - [ ] Tests are added/updated if needed  -->
<!-- - [ ] Docs are updated if needed  -->



Patch:
diff --git a/examples/elixir-ecto/.formatter.exs b/examples/elixir-ecto/.formatter.exs
new file mode 100644
index 0000000..d2cda26
--- /dev/null
+++ b/examples/elixir-ecto/.formatter.exs
@@ -0,0 +1,4 @@
+# Used by "mix format"
+[
+  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
+]
diff --git a/examples/elixir-ecto/.gitignore b/examples/elixir-ecto/.gitignore
new file mode 100644
index 0000000..2c442b1
--- /dev/null
+++ b/examples/elixir-ecto/.gitignore
@@ -0,0 +1,26 @@
+# The directory Mix will write compiled artifacts to.
+/_build/
+
+# If you run "mix test --cover", coverage assets end up here.
+/cover/
+
+# The directory Mix downloads your dependencies sources to.
+/deps/
+
+# Where third-party dependencies like ExDoc output generated docs.
+/doc/
+
+# Ignore .fetch files in case you like to edit your project deps locally.
+/.fetch
+
+# If the VM crashes, it generates a dump, let's ignore it too.
+erl_crash.dump
+
+# Also ignore archive artifacts (built via "mix archive.build").
+*.ez
+
+# Ignore package tarball (built via "mix hex.build").
+friends-*.tar
+
+# Temporary files, for example, from tests.
+/tmp/
diff --git a/examples/elixir-ecto/README.md b/examples/elixir-ecto/README.md
new file mode 100644
index 0000000..8fe96bd
--- /dev/null
+++ b/examples/elixir-ecto/README.md
@@ -0,0 +1,21 @@
+# Friends
+
+**TODO: Add description**
+
+## Installation
+
+If [available in Hex](https://hex.pm/docs/publish), the package can be installed
+by adding `friends` to your list of dependencies in `mix.exs`:
+
+```elixir
+def deps do
+  [
+    {:friends, "~> 0.1.0"}
+  ]
+end
+```
+
+Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
+and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
+be found at <https://hexdocs.pm/friends>.
+
diff --git a/examples/elixir-ecto/config/config.exs b/examples/elixir-ecto/config/config.exs
new file mode 100644
index 0000000..c66a74f
--- /dev/null
+++ b/examples/elixir-ecto/config/config.exs
@@ -0,0 +1,15 @@
+import Config
+
+database_url =
+  System.get_env("DATABASE_URL") ||
+    raise """
+    environment variable DATABASE_URL is missing.
+    For example: ecto://USER:PASS@HOST/DATABASE
+    """
+    
+config :friends, Friends.Repo,
+  url: database_url,
+  pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
+
+
+config :friends, ecto_repos: [Friends.Repo]
diff --git a/examples/elixir-ecto/lib/friends.ex b/examples/elixir-ecto/lib/friends.ex
new file mode 100644
index 0000000..f098cd9
--- /dev/null
+++ b/examples/elixir-ecto/lib/friends.ex
@@ -0,0 +1,18 @@
+defmodule Friends do
+  @moduledoc """
+  Documentation for `Friends`.
+  """
+
+  @doc """
+  Hello world.
+
+  ## Examples
+
+      iex> Friends.hello()
+      :world
+
+  """
+  def hello do
+    :world
+  end
+end
diff --git a/examples/elixir-ecto/lib/friends/application.ex b/examples/elixir-ecto/lib/friends/application.ex
new file mode 100644
index 0000000..556baa4
--- /dev/null
+++ b/examples/elixir-ecto/lib/friends/application.ex
@@ -0,0 +1,21 @@
+defmodule Friends.Application do
+  # See https://hexdocs.pm/elixir/Application.html
+  # for more information on OTP Applications
+  @moduledoc false
+
+  use Application
+
+  @impl true
+  def start(_type, _args) do
+    children = [
+      # Starts a worker by calling: Friends.Worker.start_link(arg)
+      # {Friends.Worker, arg}
+      Friends.Repo,
+    ]
+
+    # See https://hexdocs.pm/elixir/Supervisor.html
+    # for other strategies and supported options
+    opts = [strategy: :one_for_one, name: Friends.Supervisor]
+    Supervisor.start_link(children, opts)
+  end
+end
diff --git a/examples/elixir-ecto/lib/friends/person.ex b/examples/elixir-ecto/lib/friends/person.ex
new file mode 100644
index 0000000..a3a027c
--- /dev/null
+++ b/examples/elixir-ecto/lib/friends/person.ex
@@ -0,0 +1,15 @@
+defmodule Friends.Person do
+  use Ecto.Schema
+
+  schema "people" do
+    field :first_name, :string
+    field :last_name, :string
+    field :age, :integer
+  end
+
+  def changeset(person, params \\ %{}) do
+    person
+    |> Ecto.Changeset.cast(params, [:first_name, :last_name, :age])
+    |> Ecto.Changeset.validate_required([:first_name, :last_name])
+  end
+end
\ No newline at end of file
diff --git a/examples/elixir-ecto/lib/friends/repo.ex b/examples/elixir-ecto/lib/friends/repo.ex
new file mode 100644
index 0000000..428b72b
--- /dev/null
+++ b/examples/elixir-ecto/lib/friends/repo.ex
@@ -0,0 +1,5 @@
+defmodule Friends.Repo do
+  use Ecto.Repo,
+    otp_app: :friends,
+    adapter: Ecto.Adapters.Postgres
+end
diff --git a/examples/elixir-ecto/mix.exs b/examples/elixir-ecto/mix.exs
new file mode 100644
index 0000000..7b5e04d
--- /dev/null
+++ b/examples/elixir-ecto/mix.exs
@@ -0,0 +1,31 @@
+defmodule Friends.MixProject do
+  use Mix.Project
+
+  def project do
+    [
+      app: :friends,
+      version: "0.1.0",
+      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],
+      mod: {Friends.Application, []}
+    ]
+  end
+
+  # Run "mix help deps" to learn about dependencies.
+  defp deps do
+    [
+      # {:dep_from_hexpm, "~> 0.3.0"},
+      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
+      {:ecto_sql, "~> 3.8.3"},
+      {:postgrex, ">= 0.16.4"}
+    ]
+  end
+end
diff --git a/examples/elixir-ecto/mix.lock b/examples/elixir-ecto/mix.lock
new file mode 100644
index 0000000..b17be3d
--- /dev/null
+++ b/examples/elixir-ecto/mix.lock
@@ -0,0 +1,9 @@
+%{
+  "connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
+  "db_connection": {:hex, :db_connection, "2.4.2", "f92e79aff2375299a16bcb069a14ee8615c3414863a6fef93156aee8e86c2ff3", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "4fe53ca91b99f55ea249693a0229356a08f4d1a7931d8ffa79289b145fe83668"},
+  "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"},
+  "ecto": {:hex, :ecto, "3.8.4", "e06b8b87e62b27fea17fd2ff6041572ddd10339fd16cdf58446e402c6c90a74b", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f9244288b8d42db40515463a008cf3f4e0e564bb9c249fe87bf28a6d79fe82d4"},
+  "ecto_sql": {:hex, :ecto_sql, "3.8.3", "a7d22c624202546a39d615ed7a6b784580391e65723f2d24f65941b4dd73d471", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.8.4", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0 or ~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "348cb17fb9e6daf6f251a87049eafcb57805e2892e5e6a0f5dea0985d367329b"},
+  "postgrex": {:hex, :postgrex, "0.16.4", "26d998467b4a22252285e728a29d341e08403d084e44674784975bb1cd00d2cb", [:mix], [{:connection, "~> 1.1", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "3234d1a70cb7b1e0c95d2e242785ec2a7a94a092bbcef4472320b950cfd64c5f"},
+  "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"},
+}
diff --git a/examples/elixir-ecto/priv/repo/migrations/20220831094119_create_people.exs b/examples/elixir-ecto/priv/repo/migrations/20220831094119_create_people.exs
new file mode 100644
index 0000000..a7d6135
--- /dev/null
+++ b/examples/elixir-ecto/priv/repo/migrations/20220831094119_create_people.exs
@@ -0,0 +1,11 @@
+defmodule Friends.Repo.Migrations.CreatePeople do
+  use Ecto.Migration
+
+  def change do
+    create table(:people) do
+      add :first_name, :string
+      add :last_name, :string
+      add :age, :integer
+    end
+  end
+end
diff --git a/examples/elixir-ecto/priv/repo/seeds.exs b/examples/elixir-ecto/priv/repo/seeds.exs
new file mode 100644
index 0000000..76b1a00
--- /dev/null
+++ b/examples/elixir-ecto/priv/repo/seeds.exs
@@ -0,0 +1,1 @@
+# Add database seeds here
\ No newline at end of file
diff --git a/examples/elixir_no_ecto/config/config.exs b/examples/elixir_no_ecto/config/config.exs
index a617915..8c92051 100644
--- a/examples/elixir_no_ecto/config/config.exs
+++ b/examples/elixir_no_ecto/config/config.exs
@@ -46,4 +46,4 @@ config :phoenix, :json_library, Jason
 
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
-import_config "#{config_env()}.exs"
+import_config "#{config_env()}.exs"
\ No newline at end of file
diff --git a/src/providers/elixir.rs b/src/providers/elixir.rs
index 98b2670..868ffa4 100644
--- a/src/providers/elixir.rs
+++ b/src/providers/elixir.rs
@@ -26,13 +26,12 @@ impl Provider for ElixirProvider {
     fn get_build_plan(&self, app: &App, env: &Environment) -> Result<Option<BuildPlan>> {
         let mut plan = BuildPlan::default();
 
-        let elixir_pkg = ElixirProvider::get_nix_elixir_package(app, env)?;
-
         plan.add_variables(EnvironmentVariables::from([(
             "MIX_ENV".to_string(),
             "prod".to_string(),
         )]));
 
+        let elixir_pkg = ElixirProvider::get_nix_elixir_package(app, env)?;
         let setup_phase = Phase::setup(Some(vec![elixir_pkg]));
         plan.add_phase(setup_phase);
 
@@ -44,11 +43,17 @@ impl Provider for ElixirProvider {
 
         // Build Phase
         let mut build_phase = Phase::build(Some("mix compile".to_string()));
-        build_phase.add_cmd("mix assets.deploy".to_string());
-        plan.add_phase(build_phase);
+        let mix_exs_content = app.read_file("mix.exs")?;
 
-        // TODO: Detect if this needs to be run
-        // MIX_ENV=prod mix ecto.migrate
+        if mix_exs_content.contains("assets.deploy") {
+            build_phase.add_cmd("mix assets.deploy".to_string());
+        }
+
+        if mix_exs_content.contains("postgrex") && mix_exs_content.contains("ecto") {
+            build_phase.add_cmd("mix ecto.migrate");
+            build_phase.add_cmd("mix run priv/repo/seeds.exs");
+        }
+        plan.add_phase(build_phase);
 
         // Start Phase
         let start_phase = StartPhase::new("mix phx.server".to_string());


Test patch:
diff --git a/tests/snapshots/generate_plan_tests__elixir_ecto.snap b/tests/snapshots/generate_plan_tests__elixir_ecto.snap
new file mode 100644
index 0000000..f75808c
--- /dev/null
+++ b/tests/snapshots/generate_plan_tests__elixir_ecto.snap
@@ -0,0 +1,46 @@
+---
+source: tests/generate_plan_tests.rs
+expression: plan
+---
+{
+  "nixpacksVersion": "[version]",
+  "buildImage": "[build_image]",
+  "variables": {
+    "MIX_ENV": "prod"
+  },
+  "phases": [
+    {
+      "name": "install",
+      "dependsOn": [
+        "setup"
+      ],
+      "commands": [
+        "mix local.hex --force",
+        "mix local.rebar --force",
+        "mix deps.get --only prod"
+      ]
+    },
+    {
+      "name": "setup",
+      "nixPackages": [
+        {
+          "name": "elixir"
+        }
+      ]
+    },
+    {
+      "name": "build",
+      "dependsOn": [
+        "install"
+      ],
+      "commands": [
+        "mix compile",
+        "mix ecto.migrate",
+        "mix run priv/repo/seeds.exs"
+      ]
+    }
+  ],
+  "startPhase": {
+    "cmd": "mix phx.server"
+  }
+}