What hex.pm accepts as a version, and how a Buildroot version is coerced into one.
Every :nbpr_* package mirrors its Buildroot package's version, and
Buildroot's versions aren't semver. Three shapes of mismatch turn up:
- Too few components —
dnsmasqis2.92,kmodis34. Hex requires exactly three, so these are padded (2.92.0,34.0.0). - Too many components —
libjpeg-turbois3.1.4.1, a post-release fix tagged alongside3.1.4. - A patchlevel suffix — ImageMagick is
7.1.2-26.
The rules
A version reaches hex.pm only if it is exactly MAJOR.MINOR.PATCH, all
three numeric with no leading zeros. Two tempting escapes are both closed:
- Build metadata (
3.1.4+1) is rejected outright — hex.pm answersversion: build number not allowed. It couldn't order anyway:Version.compare("3.1.4+1", "3.1.4")is:eq, since semver ignores build metadata when comparing. - A pre-release (
7.1.2-26) parses, and hex.pm will accept it, but it sorts below the release it was meant to supersede (7.1.2-26 < 7.1.2) and Hex's resolver skips pre-releases unless a requirement names one. Consumers on~> 7.1would never see it.
So the fourth component goes. hex_version/1 drops any trailing numeric
packaging component — a fourth dot-segment, or a -N suffix — and pads
what's short.
What that costs
Dropping is lossy, and the loss is real: when upstream moves only the
component that got dropped (3.1.4.1 → 3.1.4.2, 7.1.2-26 →
7.1.2-27), the Hex version doesn't move, so mix nbpr.releasable sees
nothing to release and no new version can be published. Hex has three
numeric positions and uses all three for ordering; it has no
packaging-revision field the way Debian (-N) or Alpine (-rN) do.
The artefact isn't blocked by this — a tarball is addressed by a cache key over the package version, system, system version and build options, so rebuilding and re-pushing under the same key needs no Hex release. Only a change to a package's Elixir source is stuck, and the way out is to decouple the Hex version from upstream entirely rather than to find a cleverer encoding.
Anything this module doesn't recognise as numeric — 1.2.3-rc1, 2.0_p1
— passes through untouched, so validate/1 names the problem instead of
a coercion silently claiming an upstream release it isn't.
Summary
Functions
Coerces a Buildroot version into the MAJOR.MINOR.PATCH shape hex.pm
requires.
The normalise_version/1 every package's mix.exs carries, as source.
Says whether version can be published to hex.pm, and why not when it
can't.
Functions
Coerces a Buildroot version into the MAJOR.MINOR.PATCH shape hex.pm
requires.
iex> NBPR.Version.hex_version("1.8.2")
"1.8.2"
iex> NBPR.Version.hex_version("2.92")
"2.92.0"
iex> NBPR.Version.hex_version("34")
"34.0.0"
iex> NBPR.Version.hex_version("3.1.4.1")
"3.1.4"
iex> NBPR.Version.hex_version("7.1.2-26")
"7.1.2"
iex> NBPR.Version.hex_version("2.03.31")
"2.3.31"A version this can't read is returned as it came, for validate/1 to
reject:
iex> NBPR.Version.hex_version("1.2.3-rc1")
"1.2.3-rc1"
@spec normalise_version_source() :: String.t()
The normalise_version/1 every package's mix.exs carries, as source.
Mix evaluates a project before its deps exist, so a package can't call
hex_version/1 from its own mix.exs — it has to carry the coercion
inline. mix nbpr.new writes this text into what it generates, and the
workspace test asserts every package still contains it, so the copies stay
mechanical rather than hand-maintained.
Indented two spaces, ready to splice into a module body.
Says whether version can be published to hex.pm, and why not when it
can't.
iex> NBPR.Version.validate("1.8.2")
:ok
iex> NBPR.Version.validate("3.1.4+1")
{:error, "build metadata is rejected by hex.pm (`version: build number not allowed`)"}
iex> NBPR.Version.validate("2.92")
{:error, "not a semantic version — hex.pm needs exactly MAJOR.MINOR.PATCH"}