Svelixir.Manifest (svelixir v0.11.0)

Copy Markdown

Reads and writes fireside.exs — the generated lock file (SDD 8.2).

Reading is completely independent of the target's mix.exs, its deps, its _build, and of Mix being started at all. That is the one property worth keeping from Fireside, because mix svelixir.update must run against a project that may not compile — asserted against a target whose mix.exs has a syntax error and one that raises UndefinedFunctionError at load.

read!/1 and write!/2 take a %Svelixir.Target{} and never move the process to another directory. Reading would work either way, but F4's mechanism is live: once the working directory moves, Mix's build-path and compile-path accessors re-expand a relative _build and return a directory that does not exist, while its current-project accessor stays pinned to the scaffolder. The leak is asymmetric, which is why spot-checking the project accessor afterwards concludes nothing leaked.

The prose above is deliberately written without the literal tokens Svelixir.BoundaryGuard bans: the guard is a substring scan over the whole file, comments and docs included, so naming those accessors the obvious way would make lib/ fail its own boundary test.

No timestamp is minted here — generated_at and applied_at are caller-supplied ISO8601-Z strings (D8).

Ordering

Svelixir.Exs owns map KEY order; list order is this module's. encode!/1 sorts components by name and each component's files by path, after rejecting duplicate component names — the uniqueness check has to precede the sort, or two components sharing a name would sort into an arbitrary order instead of raising.

decode!/2 does not re-sort. A file this module wrote is already canonical, and a second sorting call site would be a second place for the rule to drift; the round-trip test asserts read!/1 returns what write!/2 was given, which is what makes that safe.

Deliberate asymmetries

  • config_digest and every file hash are checked for FORMAT only. Nothing here derives a digest from anything: nothing produces one until the generator exists, so a function to compute one would be a guess at a caller that does not exist.
  • schema_version is validated on decode and not on encode. The only producers of a %Svelixir.Manifest{} are decode!/2, which validates it, and the generator, which sets the constant — so an encode-side check would have no file to name in its message and no caller to reach it.
  • Errors raised while decoding name the file; errors raised from encode!/1 do not. validate!/1 is shared by both paths, and encode!/1 has no file to name.

Summary

Functions

Validates already-read fireside.exs source. file names it in errors only.

Validates a manifest and encodes it to deterministic .exs bytes.

The absolute path to target's fireside.exs.

Reads and validates target's fireside.exs.

Validates manifest and writes it to target's fireside.exs.

Types

t()

@type t() :: %Svelixir.Manifest{
  base: :otp | :web,
  components: [Svelixir.Manifest.Component.t()],
  config_digest: String.t(),
  generated_at: String.t(),
  schema_version: 1,
  svelixir_version: String.t()
}

Functions

decode!(source, file)

@spec decode!(binary(), Path.t()) :: t()

Validates already-read fireside.exs source. file names it in errors only.

encode!(manifest)

@spec encode!(t()) :: binary()

Validates a manifest and encodes it to deterministic .exs bytes.

path(target)

@spec path(Svelixir.Target.t()) :: Path.t()

The absolute path to target's fireside.exs.

There is no filename/0 beside it, for the reason Svelixir.Config.path/1 gives: a public accessor over a module attribute has no caller that path/1 does not serve better.

read!(target)

@spec read!(Svelixir.Target.t()) :: t()

Reads and validates target's fireside.exs.

Raises File.Error if the file is absent, Svelixir.Exs.DecodeError if it is not plain data, SyntaxError, TokenMissingError or MismatchedDelimiterError if it does not parse at all, and Svelixir.Manifest.InvalidError if it is plain data of the wrong shape. The target's mix.exs is never consulted, so this works against a project that does not compile.

write!(target, manifest)

@spec write!(Svelixir.Target.t(), t()) :: :ok

Validates manifest and writes it to target's fireside.exs.