Composer.Resolver (composer v0.2.0)

Copy Markdown View Source

Resolves references and expands recipes into one flat, acyclic, de-duplicated step list for Composer.Runner.

Resolving a step reference

A bare string in a recipe names a step:

  1. In a recipe, a cookbook part that is one of the recipe's own aliases is first replaced by the real cookbook name ("lab:auth/passkeys" with lab: "composer_lab" is "composer_lab:auth/passkeys"); errors show the reference as written, with the alias noted. Aliases never apply inside an included recipe, nor to references given on the command line. An alias that is the name of an installed cookbook is an error.
  2. "cookbook:path" is looked up exactly. An unknown cookbook is an error that suggests adding the package that ships it.
  3. An unqualified "path" is looked up first in the referring recipe's own cookbook (on the command line, the current project's cookbook).
  4. Otherwise every cookbook is searched. Exactly one match resolves to it; more than one is an :ambiguous error listing every candidate. No match, for a name without a /, is the Hex package of that name ("oban" is "hex:oban") - unless the name is very close to an existing step id, which is reported as a likely typo instead.
  5. "hex:pkg" (or "hex:org.pkg" for a private repo) adds the package to mix.exs and runs its own installer, if it ships one. Its @ source ("hex:pkg@path:../pkg", see Composer.Ref) and the entry's options (argv, only, runtime, a source and git options) are carried into one {:install, package, opts} step, whose opts are canonical (see Composer.Runner). An unqualified name that falls back to Hex keeps its options; one that names a step must not have package options ({:dep_opts_on_step, ...}).

A step in the referring cookbook wins over a Hex package of the same name, so an unqualified name is never ambiguous with hex:.

{:use, ref} names a recipe, "cookbook:group/name". An unqualified name without a / ({:use, "base"}) is the recipe of that name in the same group as the referring recipe; nothing outside the group can match it. Any other reference is resolved like a step reference, except that local is searched too and there is no Hex fallback.

Entries

Every leaf becomes an entry:

%{ref: "core:setup/credo", label: "core:setup/credo",
  step: {:module, Composer.Core.Steps.Setup.Credo},
  origin: "local:phx/app", gates: [:dev_tools], if: nil, meta: %Composer.Step.Meta{}}

origin is the recipe the step came from, gates the if: flags of the includes it was reached through, and if its own flag. step is the internal form the runner applies. An install's ref is always "hex:pkg", whatever its source, so requires: ["hex:pkg"] matches it by name; its label shows the source and installer argv ("hex:my_lib (path ../my_lib)"). alias is the cookbook alias the reference was written with ("lab"), or nil; ref and label always use the real cookbook name.

The result also lists the recipes visited, root first, as %Composer.Recipe{} structs; declared_flags/1 collects their flags.

Rules

  • Include-once. A recipe is expanded at most once per resolution; later includes of it are no-ops. A diamond is not a cycle.
  • No cycles. Re-entering a recipe that is still being expanded is a {:cycle, path} error, with the full cookbook:path chain.
  • De-duplication. A step equal to an earlier one is dropped (first occurrence wins) and reported in duplicates. Equality uses the resolved step, so "setup/credo" and "core:setup/credo" are duplicates. Steps that differ only in their if: are distinct.
  • One install per package. Installs of one package with different options (argv, source or dep options, or none against some) are a {:conflicting_install, package, [{origin, label}, ...]} error, whatever their if: flags: the runner adds one dependency per package. Identical ones are de-duplicated as above.

Flags

flags: :all (the default) follows every gated include, for validation, listing and building the flag schema. flags: keyword follows a gated include only when its flag is true; a skipped include is not marked as expanded, so a later ungated include of it still expands. Leaf if: flags are applied by active/2.

Summary

Types

Cookbooks prepared for lookups, from index/1.

Functions

Drops entries whose own if: flag is off in opts.

Checks each of recipe's own entries on its own, as resolving it would, and returns every error found (one per bad entry), in order: a step or package reference, with the recipe's aliases expanded and its cookbook first, or a {:use, ...} target. An alias that hides an installed cookbook comes first. Included recipes are not followed; see resolve_recipe/3 for what only the whole tree shows (cycles, conflicting installs).

Returns the flags declared (in flags:) by every recipe a resolution visited, root first. With flags: :all, these are all the --flags the root recipe accepts.

Resolves one recipe reference to its qualified id.

Resolves one step reference to its metadata or Hex package, without building an entry. Used by mix composer.step.show.

Returns every flag a resolved recipe understands: the include gates plus the leaf if: flags.

Returns recipe's own {:use, ...} entries as {written, result}, in order, where result is {:ok, recipe_id} or {:error, reason}.

Prepares cookbooks for lookups, for check_entries/2 and included/2 over many recipes.

Resolves recipe ref and expands it.

Resolves step references from the command line into entries, in order.

Returns the runner steps of entries.

Resolves every recipe with flags: :all, returning the first error.

Returns advisory warnings for an ordered list of entries

Types

entry()

@type entry() :: %{
  ref: String.t() | nil,
  label: String.t(),
  step: Composer.Runner.step(),
  origin: String.t() | nil,
  gates: [atom()],
  if: atom() | nil,
  alias: String.t() | nil,
  meta: Composer.Step.Meta.t() | nil
}

index()

@opaque index()

Cookbooks prepared for lookups, from index/1.

result()

@type result() :: %{
  root: String.t(),
  steps: [entry()],
  duplicates: [entry()],
  gates: [atom()],
  recipes: [Composer.Recipe.t()]
}

warning()

@type warning() ::
  {:phase_order, entry(), entry()}
  | {:provides_overlap, atom(), [entry()]}
  | {:conflict, entry(), atom(), entry()}
  | {:requires_order, entry(), String.t()}

Functions

active(entries, opts)

@spec active(
  [entry()],
  keyword()
) :: [entry()]

Drops entries whose own if: flag is off in opts.

check_entries(cookbooks, recipe)

@spec check_entries([Composer.Cookbook.t()] | index(), Composer.Recipe.t()) :: [
  term()
]

Checks each of recipe's own entries on its own, as resolving it would, and returns every error found (one per bad entry), in order: a step or package reference, with the recipe's aliases expanded and its cookbook first, or a {:use, ...} target. An alias that hides an installed cookbook comes first. Included recipes are not followed; see resolve_recipe/3 for what only the whole tree shows (cycles, conflicting installs).

cookbooks may be an index from index/1, to check many recipes.

declared_flags(map)

@spec declared_flags(result()) :: keyword(String.t())

Returns the flags declared (in flags:) by every recipe a resolution visited, root first. With flags: :all, these are all the --flags the root recipe accepts.

find_recipe(cookbooks, ref, context)

@spec find_recipe([Composer.Cookbook.t()], String.t(), String.t() | nil) ::
  {:ok, String.t()} | {:error, term()}

Resolves one recipe reference to its qualified id.

find_step(cookbooks, ref, context)

@spec find_step([Composer.Cookbook.t()], String.t(), String.t() | nil) ::
  {:ok, {:step, Composer.Step.Meta.t()} | {:hex, String.t()}} | {:error, term()}

Resolves one step reference to its metadata or Hex package, without building an entry. Used by mix composer.step.show.

flags(entries)

@spec flags(result() | [entry()]) :: [atom()]

Returns every flag a resolved recipe understands: the include gates plus the leaf if: flags.

included(cookbooks, recipe)

@spec included([Composer.Cookbook.t()] | index(), Composer.Recipe.t()) :: [
  {String.t(), {:ok, String.t()} | {:error, term()}}
]

Returns recipe's own {:use, ...} entries as {written, result}, in order, where result is {:ok, recipe_id} or {:error, reason}.

cookbooks may be an index from index/1.

index(index)

@spec index([Composer.Cookbook.t()] | index()) :: index()

Prepares cookbooks for lookups, for check_entries/2 and included/2 over many recipes.

resolve_recipe(cookbooks, ref, opts \\ [])

@spec resolve_recipe([Composer.Cookbook.t()], String.t(), keyword()) ::
  {:ok, result()} | {:error, term()}

Resolves recipe ref and expands it.

Options

  • :flags - :all (default) or a keyword of flag values
  • :context - the cookbook name an unqualified ref is looked up in first, normally the current project's cookbook

resolve_steps(cookbooks, refs, opts \\ [])

@spec resolve_steps([Composer.Cookbook.t()], [String.t()], keyword()) ::
  {:ok, [entry()]} | {:error, term()}

Resolves step references from the command line into entries, in order.

Options

  • :context - as for resolve_recipe/3
  • :argv - arguments passed to every step (from --); for a Hex package, its installer's arguments

steps(entries)

@spec steps([entry()]) :: [Composer.Runner.step()]

Returns the runner steps of entries.

validate_all(cookbooks, opts \\ [])

@spec validate_all(
  [Composer.Cookbook.t()],
  keyword()
) :: :ok | {:error, term()}

Resolves every recipe with flags: :all, returning the first error.

Pass only: [cookbook_name] to check some cookbooks' recipes only; references are still resolved against all of cookbooks.

warnings(entries)

@spec warnings([entry()]) :: [warning()]

Returns advisory warnings for an ordered list of entries:

  • {:phase_order, entry, earlier} - entry's phase comes before the phase of earlier, a step listed ahead of it
  • {:provides_overlap, capability, entries} - several steps provide the same capability
  • {:conflict, entry, capability, other} - entry conflicts with a capability that other provides
  • {:requires_order, entry, ref} - entry requires step ref, which is not listed earlier