%% rebar3_erli18n — a publish-ready rebar3 plugin package for erli18n catalog
%% tooling (`rebar3 erli18n extract|merge|check|report`).
%%
%% Distribution model (see README.md). This plugin is a SEPARATE Hex package
%% from the runtime `erli18n` library, pointing plugin -> lib: it declares a
%% real `{deps,[{erli18n,"~> 0.6"}]}` and `{applications,[...,erli18n]}` so it
%% can reuse the published PO read/serialize API — `erli18n_po:parse/1`,
%% `erli18n_po:dump/1`, and `erli18n_po:escape_string/1` — across the package
%% boundary (the same way `rebar3_gpb_plugin` reuses `gpb`). This is what pulls
%% a downstream consumer's `erli18n` checkout onto the plugin's runtime path
%% when the lib is unpublished (verified: WITHOUT this dep, the consumer's
%% `_checkouts/erli18n` is not on the plugin path and `erli18n_po` is
%% `non_existing` at provider-run time). The earlier "build-only, kernel +
%% stdlib, no runtime erli18n dep" claim was false — the providers already make
%% hard `erli18n_po:*` calls — so it is removed.
%%
%% The rebar3 HOST API modules the plugin calls (`providers`, `rebar_state`,
%% `rebar_api`, `rebar_app_info`) are a different matter: they live inside the
%% rebar3 escript and are supplied at plugin-load time. They are NOT a fetchable
%% dependency (rebar3 has no `rebar`/`rebar3` Hex package), so they cannot be
%% declared as deps. Every call into them is confined to the single host seam
%% `rebar3_erli18n_host.erl`; xref resolution for that seam is handled by the
%% scoped `{xref_ignores,...}` block below, NOT by extracting host beams.
{erl_opts, [
debug_info,
warnings_as_errors,
warn_unused_vars,
warn_shadow_vars,
warn_obsolete_guard
]}.
{minimum_otp_vsn, "27"}.
%% Per-app Hex/doc publishing plugins. These are scoped to THIS app (not the
%% umbrella root) on purpose: the package is published from inside this directory
%% (`cd apps/rebar3_erli18n && rebar3 hex publish ...` — see
%% `.github/workflows/release.yml`), and a per-app `rebar3 hex` invocation only
%% has the `hex`/`ex_doc` providers if `rebar3_hex`/`rebar3_ex_doc` are on THIS
%% app's plugin path. Without them, `rebar3 hex build` here aborts with
%% `Command hex not found`.
%%
%% Why publish from the app dir (not `--app NAME` from the umbrella root): the
%% published `requirements` are built from the LOCK, not from `{deps}` —
%% `rebar3_hex_build.erl:369` reads `LockDeps = rebar_state:get(State, {locks,
%% default}, [])`, `rebar3_hex_app:get_deps/1`+`lock_to_dep/2` keep only level-0
%% `{pkg,...}` lock entries (rebar3_hex_app.erl:43-63), and
%% `update_versions/2` (rebar3_hex_build.erl:424-442) iterates over those LOCK
%% entries, using `{deps}` ONLY to override the requirement string. A `{deps}`
%% entry that is NOT in the lock is therefore dropped from `requirements`. The
%% umbrella root `rebar.lock` locks only `telemetry` (the sibling app `erli18n`
%% is path-resolved, never in the root lock), so an `--app rebar3_erli18n` build
%% from the root publishes `requirements = telemetry` and OMITS `erli18n` — the
%% exact bug this layout fixes. Building from THIS app dir resolves `erli18n`
%% from Hex into a per-app `rebar.lock` as a level-0 `{pkg,...}` entry, so
%% `create_package` emits `requirements = {erli18n, "~> 0.6"}` (with `telemetry`
%% correctly demoted to a transitive level-1 lock and excluded). Verified
%% locally against the resolvable `~> 0.4` line: the per-app build produced
%% `rebar.lock` `{<<"erli18n">>,{pkg,...,<<"0.4.0">>},0}` and tarball
%% `requirements = {erli18n, "~> 0.4"}` with no spurious `telemetry`.
%%
%% This mirrors the exemplar: open-telemetry/opentelemetry-erlang publishes each
%% app self-contained (apps/opentelemetry/ carries its own rebar.config /
%% README / CHANGELOG / LICENSE / docs.config and NO per-app lock), its root
%% rebar.lock never lists the sibling `opentelemetry_api`, yet the published
%% `opentelemetry` 1.7.0 lists `requirements = {opentelemetry_api, "~> 1.5.0"}`
%% (hex.pm/api/packages/opentelemetry/releases/1.7.0) — only achievable by
%% resolving the sibling from Hex in a per-app publish context.
{project_plugins, [
{rebar3_hex, "~> 7.0"},
{rebar3_ex_doc, "~> 0.2"}
]}.
%% Real dependency on the runtime library, across the published package
%% boundary. This is the `{deps}` entry whose requirement string
%% `create_package` carries into the published plugin `requirements` —
%% but ONLY because `erli18n` also resolves into the per-app `rebar.lock` as a
%% level-0 `{pkg,...}` entry when the package is built from THIS directory (see
%% the `{project_plugins,...}` rationale above; `update_versions/2` keys on the
%% lock and uses this string to set the requirement —
%% rebar3_hex_build.erl:424-442).
%%
%% Version-sync policy (S0): the two packages keep INDEPENDENT version lines
%% (erli18n 0.6.0, this plugin 0.1.1) and are coupled ONLY by this `~> minor`
%% constraint, which tracks erli18n's CURRENT minor in lockstep with each
%% co-release — exactly the verified exemplar mechanism
%% (open-telemetry/opentelemetry-erlang apps/opentelemetry/rebar.config
%% {opentelemetry_api,"~> 1.5.0"}, where api 1.5.0 / sdk 1.7.0 / exporter 1.10.0
%% are independent versions coupled only by the dependent's `~>` constraint).
%% History: erli18n 0.5.0 first exported `erli18n_po:escape_string/1` as public
%% API (apps/erli18n/src/erli18n_po.erl), which this plugin calls
%% (apps/rebar3_erli18n/src/rebar3_erli18n_po_meta.erl), so the constraint moved
%% `~> 0.4` -> `~> 0.5` to require the line that ships that export; the Phase 5
%% co-release then moves it `~> 0.5` -> `~> 0.6` in lockstep with erli18n's
%% 0.6.0 minor (this plugin 0.1.1 adds no new erli18n API call, but pins the
%% exact library line it is built and tested against). Publish order: erli18n
%% 0.6.0 first, then this plugin — `~> 0.6` only resolves once 0.6.0 is on Hex.
{deps, [{erli18n, "~> 0.6"}]}.
%% Xref host-API resolution, scoped to the host seam.
%%
%% Because `apps/*` is rebar3's default discovery root, the umbrella's
%% `rebar3 xref` scans this plugin's modules. The eight rebar3 host calls the
%% plugin makes — `providers:create/1`, `rebar_state:add_provider/2`,
%% `rebar_state:command_parsed_args/1`, `rebar_state:project_apps/1`,
%% `rebar_state:dir/1`, `rebar_app_info:dir/1`, `rebar_api:info/2`,
%% `rebar_api:console/2` — are ALL confined to `rebar3_erli18n_host.erl` (no
%% provider references them directly). Those four host modules live inside the
%% rebar3 escript and are NOT a fetchable Hex dep, so xref reports each call as
%% an undefined function. We ignore exactly those eight {M,F,A} edges — and
%% nothing else — so `undefined_function_calls`/`undefined_functions` stay
%% ACTIVE for genuine bugs in every other module (including the plugin's own).
%% This scoped ignore is the structural mechanism for the host seam (the plugin
%% receives these host modules from the rebar3 escript at plugin-load time); it
%% is load-bearing (removing it makes exactly these eight calls reappear as
%% undefined) and not over-broad.
%%
%% Rejected alternatives (see README.md "Xref and the rebar3 host API"):
%% (a) declare rebar3 as a build dep — there is no `rebar`/`rebar3` Hex
%% package, so this is impossible, not merely undesirable;
%% (b) extract the host beams out of the running escript onto an extra xref
%% path — the deleted workaround; version-coupled to the local rebar3 and
%% needs a generator escript + a git-ignored beam dir;
%% (c) exclude the whole plugin app from xref — over-broad; it would silence
%% real undefined-call bugs in the plugin's own logic modules.
{xref_ignores, [
{providers, create, 1},
{rebar_state, add_provider, 2},
{rebar_state, command_parsed_args, 1},
{rebar_state, project_apps, 1},
{rebar_state, dir, 1},
{rebar_app_info, dir, 1},
{rebar_api, info, 2},
{rebar_api, console, 2}
]}.
{profiles, [
{test, [
{erl_opts, [nowarn_export_all]}
]}
]}.
%% ExDoc config for the rebar3_erli18n package. Single source of truth for this
%% package's doc options; `scripts/gen_docs.sh rebar3_erli18n` reads it (via
%% scripts/ex_doc_config.escript) and renders HTML from the native
%% `-doc`/`-moduledoc` BEAM chunks (EEP-48). It lives in the app (not the
%% umbrella root) so the published `rebar3_erli18n` package owns its own doc
%% config, exactly as `apps/erli18n/rebar.config` does for the library.
%%
%% The `{extras, ...}` list references ONLY this app's own files (README,
%% CHANGELOG, LICENSE) — all three ship inside the package tarball (matched by
%% rebar3_hex's ?DEFAULT_FILES relative to the app dir).
%%
%% NOTE: as with the library, `rebar3 ex_doc` is unusable directly — the plugin
%% always runs EDoc to generate chunks first, and EDoc's legacy wiki parser
%% throws on a Markdown backtick in an ordinary `%%` comment (an upstream bug).
%% `scripts/gen_docs.sh` skips that vestigial EDoc step (ex_doc reads docs from
%% the BEAM, not from EDoc's chunks), and the release workflow publishes the
%% pre-built `doc/` directory via `rebar3 hex publish docs --doc-dir doc`, which
%% never invokes the crashing doc provider.
{ex_doc, [
{extras, [
"README.md",
"CHANGELOG.md",
"LICENSE"
]},
{main, "readme"},
{source_url, "https://github.com/eagle-head/erli18n"},
{prefix_ref_vsn_with_v, false}
]}.
%% Hex packaging. Wire the doc provider to ExDoc so that
%% `rebar3 hex publish docs` (and any `rebar3 hex build docs`) resolves a
%% provider; the EDoc crash is routed around by publishing `--doc-dir doc`.
{hex, [{doc, #{provider => ex_doc}}]}.