Current section

Files

Jump to
rindle mix.exs
Raw

mix.exs

defmodule Rindle.MixProject do
  use Mix.Project

  @version "0.4.10"
  @source_url "https://github.com/szTheory/rindle"

  def project do
    [
      app: :rindle,
      version: @version,
      elixir: "~> 1.15",
      elixirc_paths: elixirc_paths(Mix.env()),
      # Optional-dependency modules (and the Mux-gated worker) are legitimately
      # absent under `mix compile --no-optional-deps`. Elixir 1.19 auto-suppresses
      # these via the `optional: true` deps, but 1.15/1.17 do not — so the ADMIN-06
      # no-optional `--warnings-as-errors` gate fails there without this list.
      elixirc_options: [
        no_warn_undefined: [
          Bandit,
          CAStore,
          Finch,
          Goth,
          GcsSignedUrl,
          GcsSignedUrl.Client,
          JOSE.JWK,
          Mux.Base,
          Mux.Video.Assets,
          Rindle.Workers.MuxIngestVariant
        ]
      ],
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps(),
      docs: docs(),
      package: package(),
      name: "Rindle",
      description: "Phoenix/Ecto-native media lifecycle library. Media, made durable.",
      dialyzer: [
        plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
        plt_add_apps: [:mix, :ex_unit, :mux, :jose, :goth, :finch, :gcs_signed_url],
        ignore_warnings: ".dialyzer_ignore.exs"
      ],
      test_coverage: [tool: ExCoveralls]
    ]
  end

  def cli do
    [
      preferred_envs: [
        coveralls: :test,
        "coveralls.detail": :test,
        "coveralls.html": :test,
        "coveralls.json": :test,
        "coveralls.multiple": :test,
        precommit: :test,
        # under MIX_ENV=test (same as `precommit`); otherwise the nested `test`
        # task raises the "running in dev environment" guard.
        ci: :test
      ]
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {Rindle.Application, []}
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support", "test/adopter"]
  defp elixirc_paths(_), do: ["lib"]

  defp deps do
    [
      # Core
      {:ecto_sql, "~> 3.11"},
      {:postgrex, "~> 0.18"},
      {:jason, "~> 1.4"},
      {:muontrap, "~> 1.7"},

      # Background processing
      {:oban, "~> 2.21"},

      # Image processing
      {:image, "~> 0.65"},

      # LiveView integration (optional — Rindle.LiveView helpers are no-op without it)
      {:phoenix_live_view, "~> 1.0", optional: true},

      # Streaming providers (optional — Mux adapter only loads when these are present)
      {:mux, "~> 3.2", optional: true},
      {:jose, "~> 1.11", optional: true},

      # GCS adapter (optional — Rindle.Storage.GCS only loads when these are present)
      {:goth, "~> 1.4", optional: true},
      {:finch, "~> 0.21", optional: true},
      {:gcs_signed_url, "~> 0.6", optional: true},

      # Configuration validation
      {:nimble_options, "~> 1.1"},

      # Security
      {:ex_marcel, "~> 0.2"},
      {:ex_aws, "~> 2.5"},
      {:ex_aws_s3, "~> 2.5"},
      # ExAws optional HTTP client — needed for the integration + adopter CI
      # lanes to talk to MinIO/S3. Req avoids ExAws's legacy Hackney 1.x default,
      # and is also the transport used by gcs_signed_url >= 0.6.
      {:req, "~> 0.6", optional: true},

      # Observability
      {:telemetry, "~> 1.2"},

      # Plug — required by :image (color visualizer) and phoenix_live_view transitively
      {:plug, "~> 1.16"},

      # Dev/Test
      {:lazy_html, ">= 0.1.0", only: :test},
      {:junit_formatter, "~> 3.4", only: :test},
      {:mox, "~> 1.2", only: :test},
      {:bypass, "~> 2.1", only: :test},
      {:ex_machina, "~> 2.7", only: :test},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
      {:doctor, "~> 0.23.0", only: [:dev, :test], runtime: false},
      {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
      {:yaml_elixir, "~> 2.12", only: [:dev, :test]},
      {:excoveralls, "~> 0.18", only: [:test, :dev], runtime: false},
      json_polyfill_dep(),
      {:ex_doc, "~> 0.40", only: :dev, runtime: false}
    ]
    |> Enum.reject(&is_nil/1)
  end

  defp json_polyfill_dep do
    otp_major = System.otp_release() |> String.to_integer()

    if otp_major < 27 do
      {:json_polyfill, "~> 0.2", only: [:test, :dev]}
    end
  end

  defp docs do
    [
      main: "Rindle",
      source_url: @source_url,
      logo: "brandbook/assets/logo/rindle-mark-dark.svg",
      favicon: "brandbook/assets/logo/favicon.svg",
      extras: [
        "README.md",
        "RUNNING.md",
        "guides/user_flows.md",
        "guides/docker_demo_dx.md",
        "guides/getting_started.md",
        "guides/upgrading.md",
        "guides/core_concepts.md",
        "guides/storage_capabilities.md",
        "guides/storage_gcs.md",
        "guides/profiles.md",
        "guides/admin_console.md",
        "guides/secure_delivery.md",
        "guides/resumable_uploads.md",
        "guides/streaming_providers.md",
        "guides/background_processing.md",
        "guides/operations.md",
        "guides/troubleshooting.md",
        "guides/release_publish.md"
      ],
      groups_for_extras: [
        Guides: ~r/guides\/(?!release_publish).*\.md$/,
        Maintainer: ~r/guides\/release_publish\.md$/
      ],
      groups_for_modules: [
        Facade: [
          Rindle,
          Rindle.Error
        ],
        Profiles: [
          Rindle.Profile
        ],
        Upload: [
          Rindle.Upload.Broker
        ],
        Delivery: [
          Rindle.Delivery
        ],
        "Optional Integrations": [
          Rindle.LiveView,
          Rindle.HTML
        ],
        "Admin Console": [
          Rindle.Admin.Router
        ],
        "Extension Points": [
          Rindle.Authorizer,
          Rindle.Analyzer,
          Rindle.Scanner,
          Rindle.Processor
        ],
        "Storage and Processor Adapters": [
          Rindle.Storage,
          Rindle.Storage.Local,
          Rindle.Storage.S3,
          Rindle.Storage.GCS,
          Rindle.Processor.Image,
          Rindle.Processor.AV
        ],
        Operations: [
          Mix.Tasks.Rindle.AbortIncompleteUploads,
          Mix.Tasks.Rindle.BackfillMetadata,
          Mix.Tasks.Rindle.BatchOwnerErasure,
          Mix.Tasks.Rindle.CleanupOrphans,
          Mix.Tasks.Rindle.Doctor,
          Mix.Tasks.Rindle.RegenerateVariants,
          Mix.Tasks.Rindle.RuntimeStatus,
          Mix.Tasks.Rindle.SweepOrphanedTempFiles,
          Mix.Tasks.Rindle.VerifyStorage,
          Rindle.Workers.AbortIncompleteUploads,
          Rindle.Workers.CleanupOrphans
        ],
        "Data Types": [
          Rindle.Domain.MediaAsset,
          Rindle.Domain.MediaAttachment,
          Rindle.Domain.MediaUploadSession,
          Rindle.Domain.MediaVariant,
          Rindle.Domain.MediaProcessingRun
        ]
      ],
      before_closing_head_tag: &before_closing_head_tag/1
    ]
  end

  # Mermaid CDN injection so ```mermaid fences in guides and @moduledoc blocks
  # render as interactive SVG diagrams in the generated HexDocs HTML.
  # Only the :html target needs this; :epub returns an empty string.
  defp before_closing_head_tag(:html) do
    """
    <script defer src="https://cdn.jsdelivr.net/npm/mermaid@10.2.3/dist/mermaid.min.js"></script>
    <script>
      let initialized = false;
      window.addEventListener("exdoc:loaded", () => {
        if (!initialized) {
          mermaid.initialize({
            startOnLoad: false,
            theme: document.body.className.includes("dark") ? "dark" : "default"
          });
          initialized = true;
        }
        let id = 0;
        for (const codeEl of document.querySelectorAll("pre code.mermaid")) {
          const preEl = codeEl.parentElement;
          const graphDefinition = codeEl.textContent;
          const graphEl = document.createElement("div");
          const graphId = "mermaid-graph-" + id++;
          mermaid.render(graphId, graphDefinition).then(({svg, bindFunctions}) => {
            graphEl.innerHTML = svg;
            bindFunctions?.(graphEl);
            preEl.insertAdjacentElement("afterend", graphEl);
            preEl.remove();
          });
        }
      });
    </script>
    """
  end

  defp before_closing_head_tag(:epub), do: ""

  defp package do
    [
      licenses: ["MIT"],
      links: %{
        "GitHub" => @source_url,
        "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md",
        "Docs" => "https://hexdocs.pm/rindle"
      },
      files:
        ~w(lib priv/repo/migrations priv/static/rindle_admin mix.exs README.md RUNNING.md CHANGELOG.md LICENSE guides)
    ]
  end

  defp aliases do
    [
      "gsd.clean": ["cmd bash scripts/gsd_cleanup.sh"],
      precommit: ["test"],
      refactor_contract: ["cmd bash scripts/maintainer/refactor_contract.sh"],
      credo_quality: ["cmd bash scripts/maintainer/credo_quality.sh"],
      quality_signals: [
        "credo_quality",
        "cmd env MIX_ENV=dev mix doctor --full --raise",
        "refactor_contract"
      ],
      # `mix ci` is the discoverable local approximation of the PR merge gate.
      # Ordered fast-static -> compile -> format -> truthful quality signals ->
      # brandbook drift gates -> the gating unit suite last. It mirrors ONLY the
      # merge-blocking set surfaced by the `CI Summary` gate (ci.yml `quality` +
      # `brandbook-tokens`); it does NOT
      # include push:main/nightly-only lanes (package-consumer-full, adoption-demo
      # -e2e, mux-soak, the broad OTP x Elixir matrix, Dialyzer).
      #
      # MinIO/integration legs are deliberately ABSENT: the final `test` task runs
      # the default-tag suite, which test/test_helper.exs already excludes
      # :integration/:minio/:contract/:adopter from — so `mix ci` runs on a fresh
      # clone WITHOUT a running MinIO and never hard-fails for its absence
      # clone without a running MinIO. The full-parity storage command is documented
      # in CONTRIBUTING.
      # The Playwright browser gallery proof is omitted to keep `mix ci`
      # Elixir-toolchain-only (covered by `brandbook-tokens` in CI +
      # scripts/ci/e2e_local.sh locally).
      ci: [
        "deps.get --check-locked",
        "deps.unlock --check-unused",
        "compile --warnings-as-errors",
        "format --check-formatted",
        "quality_signals",
        "cmd node brandbook/src/tokens-build.mjs",
        "cmd node brandbook/src/admin-css-build.mjs",
        "cmd node brandbook/src/admin-contrast.mjs",
        "cmd node brandbook/src/sync-admin-css.mjs",
        "test"
      ],
      setup: ["deps.get", "ecto.setup"],
      "ecto.setup": ["ecto.create", "ecto.migrate"],
      "ecto.reset": ["ecto.drop", "ecto.setup"],
      test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"]
    ]
  end
end