Skip to main content

lsupg Progress

I went ahead and changed the lsupg Nix component to calculate upgrade items as described in Nix Container Declarative Package Management.

Bug Fix

When I initially implemented the Nix component, I was unsure of how to parse package names and versions from strings in {{ name }}-{{ version }} format. As a tentative implementation, I split on the dash character and considered all parts from the first one that starts with a digit as the version.

parseNameAndVersion :: BS.ByteString -> (BS.ByteString, BS.ByteString)
parseNameAndVersion
    = bimap (BS8.intercalate "-") (BS8.intercalate "-")
    . break (maybe True (isDigit . fst) . BS8.uncons)
    . BS8.split '-'

Since then, I found two places in the manuals where the correct behavior is specified!

In the Nix manual, the parseDrvName function is described as follows:

Split the string s into a package name and version. The package name is everything up to but not including the first dash followed by a digit, and the version is everything following that dash. The result is returned in a set { name, version }. Thus, builtins.parseDrvName "nix-0.12pre12876" returns { name = "nix"; version = "0.12pre12876"; }.

In the Nixpkgs manual, the coding conventions section on package naming specifies:

The version part of the name attribute must start with a digit (following a dash) — e.g., "hello-0.3.1rc2".

I did not revisit my tentative implementation until today, and I see that it was close but not correct! When splitting on the dash character, the first part is always part of the name, even if it begins with a digit. My corrected implementation, with a different API to match the new code, is as follows:

parseLine :: BS.ByteString -> Either String (Text, Text)
parseLine line =
    maybe (Left $ "error parsing nix line: " ++ TTC.toS line) Right $ do
      -- the first part is a name part even if it begins with a digit
      (namePart, parts) <- uncons $ BS8.split '-' line
      let (nameParts, versionParts) =
            break (maybe True (isDigit . fst) . BS8.uncons) parts
      guard . not $ null versionParts
      let joinParts = TTC.toT . BS8.intercalate "-"
      return (joinParts (namePart : nameParts), joinParts versionParts)

I included a package name starting with a digit (0ad) in the unit tests.

Stack Docker Images

I use the Docker functionality of Stack to build the static executable in an Alpine Linux container. The command automatically pulls and uses a fpco/stack-build image that is tagged with the LTS. This fails when using LTS 18, and I confirmed on Docker Hub that no LTS 18 images have been pushed.

Investigating, it seems that images are now pushed to the commercialhaskell/stackage repository instead! Perhaps I missed a Stack release? Checking the stack repository, I confirmed that I am running the latest tagged release, made over a year ago. I imagine that the next release will fix the issue, but I need to work around it in the meantime.

Consulting the Docker integration documentation, I learned how to configure my stack.yaml to point to the new repository. Setting it to commercialhaskell/stackage did not work because the images are no longer tagged the same way! Instead of having separate images for each point release, there is now a single image per LTS. The following configuration works:

resolver: lts-18.1

packages:
  - .

extra-deps:
  - ttc-1.1.0.1

docker:
  enable: false
  repo: "commercialhaskell/stackage:lts18"

Note that I explicitly disable Docker functionality because I only use Docker to build the static executable. The build command in my Makefile enables Docker, overriding the default in the configuration:

stack build --flag lsupg:static --docker

Demonstration

I created a Docker image to demonstrate the change.

Dockerfile:

FROM nixos/nix:latest

COPY packages.nix /etc/nix/packages.nix
COPY test.nix /tmp/test.nix

RUN nix-env -if "/tmp/test.nix"

packages.nix:

with import <nixpkgs> {}; [
  nix
  python3
]

test.nix:

let
  pkgs = import (builtins.fetchTarball {
    url = "https://github.com/NixOS/nixpkgs/archive/refs/tags/20.09.tar.gz";
  }) {};
in pkgs.python3

Running the nix component using the released version produces the following output:

[lsupg] $ lsupg --docker extremais/lsupg-test-nix:latest nix
nix  python3  3.8.5   3.10.0a5
nix  nix      2.3.11  2.3.12

Running the nix component using a build with the change produces the following output:

[lsupg] build$ ./lsupg --docker extremais/lsupg-test-nix:latest nix
nix  nix      2.3.11  2.3.12
nix  python3  3.8.5   3.8.9

The new build is able to correctly determine available upgrades based on the attribute paths.