NBPR.Version (NBPR v0.4.0)

Copy Markdown View Source

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 componentsdnsmasq is 2.92, kmod is 34. Hex requires exactly three, so these are padded (2.92.0, 34.0.0).
  • Too many componentslibjpeg-turbo is 3.1.4.1, a post-release fix tagged alongside 3.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 answers version: 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.1 would 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.13.1.4.2, 7.1.2-267.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

hex_version(upstream)

@spec hex_version(String.t()) :: String.t()

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"

normalise_version_source()

@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.

validate(version)

@spec validate(String.t()) :: :ok | {:error, String.t()}

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"}