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_digestand every filehashare 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_versionis validated on decode and not on encode. The only producers of a%Svelixir.Manifest{}aredecode!/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!/1do not.validate!/1is shared by both paths, andencode!/1has 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
@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
Validates already-read fireside.exs source. file names it in errors only.
Validates a manifest and encodes it to deterministic .exs bytes.
@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.
@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.
@spec write!(Svelixir.Target.t(), t()) :: :ok
Validates manifest and writes it to target's fireside.exs.