defmodule Microwaveprop.MixProject do use Mix.Project def project do [ app: :microwaveprop, version: "0.1.0", elixir: "~> 1.15", elixirc_paths: elixirc_paths(Mix.env()), start_permanent: Mix.env() == :prod, aliases: aliases(), deps: deps(), compilers: [:phoenix_live_view] ++ Mix.compilers(), listeners: [Phoenix.CodeReloader], test_coverage: [summary: [threshold: 85]], dialyzer: [ plt_add_apps: [:mix, :ex_unit], plt_file: {:no_warn, "priv/plts/project.plt"}, flags: [ :error_handling, :unknown, :unmatched_returns, :extra_return, :missing_return ] ] ] end # Configuration for the OTP application. # # Type `mix help compile.app` for more information. def application do [ mod: {Microwaveprop.Application, []}, extra_applications: [:logger, :runtime_tools, :os_mon, :xmerl] ] end def cli do [ preferred_envs: [precommit: :test] ] end # Specifies which paths to compile per environment. defp elixirc_paths(:test), do: ["lib", "lib_ml", "test/support"] defp elixirc_paths(:dev), do: ["lib", "lib_ml"] defp elixirc_paths(_), do: ["lib"] # Specifies your project dependencies. # # Type `mix help deps` for examples and options. defp deps do [ {:bcrypt_elixir, "~> 3.0"}, {:phoenix, "~> 1.8.3"}, {:phoenix_ecto, "~> 4.5"}, {:ecto_sql, "~> 3.13"}, {:postgrex, ">= 0.0.0"}, {:phoenix_html, "~> 4.1"}, {:phoenix_live_reload, "~> 1.2", only: :dev}, {:phoenix_live_view, "~> 1.2.0"}, {:lazy_html, ">= 0.1.0", only: :test}, {:stream_data, "~> 1.0", only: :test}, {:live_stash, "~> 1.0"}, {:phoenix_live_dashboard, "~> 0.8.3"}, {:esbuild, "~> 0.10", runtime: Mix.env() == :dev}, {:tailwind, "~> 0.3", runtime: Mix.env() == :dev}, {:heroicons, github: "tailwindlabs/heroicons", tag: "v2.2.0", sparse: "optimized", app: false, compile: false, depth: 1}, {:swoosh, "~> 1.16"}, {:gen_smtp, "~> 1.2"}, {:req, "~> 0.5"}, {:telemetry_metrics, "~> 1.0"}, {:telemetry_poller, "~> 1.0"}, {:prom_ex, "~> 1.11"}, {:jason, "~> 1.2"}, # Structured JSON log formatter plugged into Elixir 1.15+ :logger. # OTP 28 / Elixir 1.19 ships `Logger.Formatter.new/1` but no built-in # JSON encoder, so the hex package fills the gap. Prod flips to # LOG_FORMAT=json; dev/test stay on the default text formatter. {:logger_json, "~> 7.0"}, {:msgpax, "~> 2.4"}, {:bandit, "~> 1.5"}, {:styler, "~> 1.11", only: [:dev, :test], runtime: false}, {:oban, "~> 2.21"}, # All three Oban Pro packages stay vendored so production deploys # don't have to authenticate against the licensed oban.pro hex # repo on every image build. Bumping a version is a fetch-and- # extract step (`mix hex.package fetch oban_pro --repo=oban # --unpack`) into vendor/, then commit. {:oban_met, "~> 1.0", path: "vendor/oban_met", override: true}, {:oban_pro, "~> 1.7", path: "vendor/oban_pro"}, # Note: override: true on oban_web is required because live_table # transitively pulls `oban_web ~> 2.11` from hexpm; without the # override mix tries to resolve from hex and fails. towerops-web # doesn't have live_table so it doesn't need the override. {:oban_web, "~> 2.12", path: "vendor/oban_web", override: true}, {:credo, "~> 1.7", only: [:dev, :test], runtime: false}, {:jump_credo_checks, "~> 0.4", only: [:dev, :test], runtime: false}, {:nx, "~> 0.13", only: [:dev, :test]}, {:axon, "~> 0.7", only: [:dev, :test]}, {:exla, "~> 0.9", only: [:dev, :test]}, {:polaris, "~> 0.1", only: [:dev, :test]}, {:tidewave, "~> 0.5", only: :dev}, {:libcluster, "~> 3.4"}, # Cross-pod cache backend for Microwaveprop.Weather.GridCache. # Talks to the Valkey/Redis instance at VALKEY_URL; falls back to # the legacy ETS implementation when unset (dev/test). {:redix, "~> 1.5"}, # Redis-backed Phoenix.PubSub adapter. Used in prod when # VALKEY_URL is set so broadcasts go through Valkey instead of # libcluster's Erlang distribution; dev/test stay on the default # PG2 adapter (in-process). Wires up via application.ex's # PubSub child spec, not a config block. {:phoenix_pubsub_redis, "~> 3.0"}, # MQTT 3.1.1 transport for `Microwaveprop.Pskr.Client` is # implemented inline in `Microwaveprop.Pskr.Mqtt` — the wire # protocol for a subscribe-only QoS 0 client is ~150 lines of # binary parsing, less code than the dep-management overhead # of either emqtt (transitive quicer + cmake) or tortoise311 # (gen_state_machine + retry/inflight machinery we don't use). {:ecto_psql_extras, "~> 0.8"}, {:mox, "~> 1.0", only: :test}, {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false}, {:live_table, "~> 0.4.1"}, # Transitive dep of live_table (via LiveTable.TableComponent's # generated code pulls in SutraUI.DropdownMenu). Pinned here # because live_table doesn't list it in its own deps. {:sutra_ui, "~> 0.4.0"}, # Required as a compile-time build dependency for live_table; its mix # tasks (lib/mix/tasks/live_table.gen.live.ex, live_table.install.ex) # unconditionally `use Igniter.Mix.Task` so the dep has to exist in # every env live_table is compiled in, not just dev/test. runtime: false # keeps it out of the release bundle. {:igniter, "~> 0.7", runtime: false} ] end # Aliases are shortcuts or tasks specific to the current project. # For example, to install project dependencies and perform other setup tasks, run: # # $ mix setup # # See the documentation for `Mix` for more info on aliases. defp aliases do [ setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"], "deps.get": ["deps.get", &apply_dep_patches/1], "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"], "ecto.reset": ["ecto.drop", "ecto.setup"], "test.setup": ["ecto.create --quiet", "ecto.migrate --quiet"], test: ["test"], "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], "assets.build": [ "compile", "tailwind microwaveprop", "esbuild microwaveprop", ©_leaflet_images/1 ], "assets.deploy": [ "tailwind microwaveprop --minify", "esbuild microwaveprop --minify", ©_leaflet_images/1, "phx.digest" ], precommit: ["cmd make precommit"] ] end # Mirrors `assets/vendor/leaflet/images/` into `priv/static/assets/css/images/` # so Leaflet's CSS (which references `images/layers.png` etc. relative to the # bundled stylesheet) can resolve those URLs in production. `priv/static/` # is gitignored, so this needs to run on every build/deploy. defp copy_leaflet_images(_args) do src = "assets/vendor/leaflet/images" dest = "priv/static/assets/css/images" if File.dir?(src) do File.mkdir_p!(dest) File.cp_r!(src, dest) end :ok end # Applies every patch file in `priv/dep_patches/*.patch` to its corresponding # dependency under `deps/`. Inlined here (instead of a `Mix.Task` module # under `lib/mix/tasks/`) so it can run during `mix deps.get` itself — # `lib/` isn't compiled yet at that point, so a proper task would not be # resolvable. Idempotent: a sentinel file per patch prevents re-application. @patches_dir "priv/dep_patches" defp apply_dep_patches(_args) do case File.ls(@patches_dir) do {:ok, entries} -> entries |> Enum.filter(&String.ends_with?(&1, ".patch")) |> Enum.sort() |> Enum.each(&apply_single_patch/1) {:error, :enoent} -> :ok end end defp apply_single_patch(filename) do dep_name = filename |> Path.basename(".patch") |> String.split("-") |> Enum.drop(-1) |> Enum.join("-") dep_dir = Path.join("deps", dep_name) sentinel = Path.join(dep_dir, ".#{filename}.applied") cond do not File.dir?(dep_dir) -> Mix.shell().info("deps.patch: skipping #{filename} — deps/#{dep_name} not present") File.exists?(sentinel) -> Mix.shell().info("deps.patch: #{filename} already applied") true -> patch_path = Path.absname(Path.join(@patches_dir, filename)) {output, exit_code} = System.cmd("patch", ["-p1", "-i", patch_path], env: %{}, cd: dep_dir) if exit_code == 0 do Mix.shell().info("deps.patch: applied #{filename}") File.touch!(sentinel) else Mix.raise("deps.patch: failed to apply #{filename}:\n#{output}") end end end end