nixpkgs-suyu/doc/contributing/coding-conventions.chapter.md
2022-11-08 07:30:20 -08:00

22 KiB
Raw Blame History

Coding conventions

Syntax

  • Use 2 spaces of indentation per indentation level in Nix expressions, 4 spaces in shell scripts.

  • Do not use tab characters, i.e. configure your editor to use soft tabs. For instance, use (setq-default indent-tabs-mode nil) in Emacs. Everybody has different tab settings so its asking for trouble.

  • Use lowerCamelCase for variable names, not UpperCamelCase. Note, this rule does not apply to package attribute names, which instead follow the rules in .

  • Function calls with attribute set arguments are written as

    foo {
      arg = ...;
    }
    

    not

    foo
    {
      arg = ...;
    }
    

    Also fine is

    foo { arg = ...; }
    

    if it's a short call.

  • In attribute sets or lists that span multiple lines, the attribute names or list elements should be aligned:

    # A long list.
    list = [
      elem1
      elem2
      elem3
    ];
    
    # A long attribute set.
    attrs = {
      attr1 = short_expr;
      attr2 =
        if true then big_expr else big_expr;
    };
    
    # Combined
    listOfAttrs = [
      {
        attr1 = 3;
        attr2 = "fff";
      }
      {
        attr1 = 5;
        attr2 = "ggg";
      }
    ];
    
  • Short lists or attribute sets can be written on one line:

    # A short list.
    list = [ elem1 elem2 elem3 ];
    
    # A short set.
    attrs = { x = 1280; y = 1024; };
    
  • Breaking in the middle of a function argument can give hard-to-read code, like

    someFunction { x = 1280;
      y = 1024; } otherArg
      yetAnotherArg
    

    (especially if the argument is very large, spanning multiple lines).

    Better:

    someFunction
      { x = 1280; y = 1024; }
      otherArg
      yetAnotherArg
    

    or

    let res = { x = 1280; y = 1024; };
    in someFunction res otherArg yetAnotherArg
    
  • The bodies of functions, asserts, and withs are not indented to prevent a lot of superfluous indentation levels, i.e.

    { arg1, arg2 }:
    assert system == "i686-linux";
    stdenv.mkDerivation { ...
    

    not

    { arg1, arg2 }:
      assert system == "i686-linux";
        stdenv.mkDerivation { ...
    
  • Function formal arguments are written as:

    { arg1, arg2, arg3 }:
    

    but if they don't fit on one line they're written as:

    { arg1, arg2, arg3
    , arg4, ...
    , # Some comment...
      argN
    }:
    
  • Functions should list their expected arguments as precisely as possible. That is, write

    { stdenv, fetchurl, perl }: ...
    

    instead of

    args: with args; ...
    

    or

    { stdenv, fetchurl, perl, ... }: ...
    

    For functions that are truly generic in the number of arguments (such as wrappers around mkDerivation) that have some required arguments, you should write them using an @-pattern:

    { stdenv, doCoverageAnalysis ? false, ... } @ args:
    
    stdenv.mkDerivation (args // {
      ... if doCoverageAnalysis then "bla" else "" ...
    })
    

    instead of

    args:
    
    args.stdenv.mkDerivation (args // {
      ... if args ? doCoverageAnalysis && args.doCoverageAnalysis then "bla" else "" ...
    })
    
  • Unnecessary string conversions should be avoided. Do

    rev = version;
    

    instead of

    rev = "${version}";
    
  • Building lists conditionally should be done with lib.optional(s) instead of using if cond then [ ... ] else null or if cond then [ ... ] else [ ].

    buildInputs = lib.optional stdenv.isDarwin iconv;
    

    instead of

    buildInputs = if stdenv.isDarwin then [ iconv ] else null;
    

    As an exception, an explicit conditional expression with null can be used when fixing a important bug without triggering a mass rebuild. If this is done a follow up pull request should be created to change the code to lib.optional(s).

  • Arguments should be listed in the order they are used, with the exception of lib, which always goes first.

Package naming

The key words must, must not, required, shall, shall not, should, should not, recommended, may, and optional in this section are to be interpreted as described in RFC 2119. Only emphasized words are to be interpreted in this way.

In Nixpkgs, there are generally three different names associated with a package:

  • The name attribute of the derivation (excluding the version part). This is what most users see, in particular when using nix-env.

  • The variable name used for the instantiated package in all-packages.nix, and when passing it as a dependency to other functions. Typically this is called the package attribute name. This is what Nix expression authors see. It can also be used when installing using nix-env -iA.

  • The filename for (the directory containing) the Nix expression.

Most of the time, these are the same. For instance, the package e2fsprogs has a name attribute "e2fsprogs-version", is bound to the variable name e2fsprogs in all-packages.nix, and the Nix expression is in pkgs/os-specific/linux/e2fsprogs/default.nix.

There are a few naming guidelines:

  • The pname attribute should be identical to the upstream package name.

  • The pname and the version attribute must not contain uppercase letters — e.g., "mplayer" instead of "MPlayer"`.

  • The version attribute must start with a digit e.g`"0.3.1rc2".

  • If a package is not a release but a commit from a repository, then the version attribute must be the date of that (fetched) commit. The date must be in "unstable-YYYY-MM-DD" format.

  • Dashes in the package pname should be preserved in new variable names, rather than converted to underscores or camel cased — e.g., http-parser instead of http_parser or httpParser. The hyphenated style is preferred in all three package names.

  • If there are multiple versions of a package, this should be reflected in the variable names in all-packages.nix, e.g. json-c_0_9 and json-c_0_11. If there is an obvious “default” version, make an attribute like json-c = json-c_0_9;. See also

File naming and organisation

Names of files and directories should be in lowercase, with dashes between words — not in camel case. For instance, it should be all-packages.nix, not allPackages.nix or AllPackages.nix.

Hierarchy

Each package should be stored in its own directory somewhere in the pkgs/ tree, i.e. in pkgs/category/subcategory/.../pkgname. Below are some rules for picking the right category for a package. Many packages fall under several categories; what matters is the primary purpose of a package. For example, the libxml2 package builds both a library and some tools; but its a library foremost, so it goes under pkgs/development/libraries.

When in doubt, consider refactoring the pkgs/ tree, e.g. creating new categories or splitting up an existing category.

If its used to support software development:

  • If its a library used by other packages:

    • development/libraries (e.g. libxml2)
  • If its a compiler:

    • development/compilers (e.g. gcc)
  • If its an interpreter:

    • development/interpreters (e.g. guile)
  • If its a (set of) development tool(s):

    • If its a parser generator (including lexers):

      • development/tools/parsing (e.g. bison, flex)
    • If its a build manager:

      • development/tools/build-managers (e.g. gnumake)
    • Else:

      • development/tools/misc (e.g. binutils)
  • Else:

    • development/misc

If its a (set of) tool(s):

(A tool is a relatively small program, especially one intended to be used non-interactively.)

  • If its for networking:

    • tools/networking (e.g. wget)
  • If its for text processing:

    • tools/text (e.g. diffutils)
  • If its a system utility, i.e., something related or essential to the operation of a system:

    • tools/system (e.g. cron)
  • If its an archiver (which may include a compression function):

    • tools/archivers (e.g. zip, tar)
  • If its a compression program:

    • tools/compression (e.g. gzip, bzip2)
  • If its a security-related program:

    • tools/security (e.g. nmap, gnupg)
  • Else:

    • tools/misc

If its a shell:

  • shells (e.g. bash)

If its a server:

  • If its a web server:

    • servers/http (e.g. apache-httpd)
  • If its an implementation of the X Windowing System:

    • servers/x11 (e.g. xorg — this includes the client libraries and programs)
  • Else:

    • servers/misc

If its a desktop environment:

  • desktops (e.g. kde, gnome, enlightenment)

If its a window manager:

  • applications/window-managers (e.g. awesome, stumpwm)

If its an application:

A (typically large) program with a distinct user interface, primarily used interactively.

  • If its a version management system:

    • applications/version-management (e.g. subversion)
  • If its a terminal emulator:

    • applications/terminal-emulators (e.g. alacritty or rxvt or termite)
  • If its a file manager:

    • applications/file-managers (e.g. mc or ranger or pcmanfm)
  • If its for video playback / editing:

    • applications/video (e.g. vlc)
  • If its for graphics viewing / editing:

    • applications/graphics (e.g. gimp)
  • If its for networking:

    • If its a mailreader:

      • applications/networking/mailreaders (e.g. thunderbird)
    • If its a newsreader:

      • applications/networking/newsreaders (e.g. pan)
    • If its a web browser:

      • applications/networking/browsers (e.g. firefox)
    • Else:

      • applications/networking/misc
  • Else:

    • applications/misc

If its data (i.e., does not have a straight-forward executable semantics):

  • If its a font:

    • data/fonts
  • If its an icon theme:

    • data/icons
  • If its related to SGML/XML processing:

    • If its an XML DTD:

      • data/sgml+xml/schemas/xml-dtd (e.g. docbook)
    • If its an XSLT stylesheet:

      (Okay, these are executable...)

      • data/sgml+xml/stylesheets/xslt (e.g. docbook-xsl)
  • If its a theme for a desktop environment, a window manager or a display manager:

    • data/themes

If its a game:

  • games

Else:

  • misc

Versioning

Because every version of a package in Nixpkgs creates a potential maintenance burden, old versions of a package should not be kept unless there is a good reason to do so. For instance, Nixpkgs contains several versions of GCC because other packages dont build with the latest version of GCC. Other examples are having both the latest stable and latest pre-release version of a package, or to keep several major releases of an application that differ significantly in functionality.

If there is only one version of a package, its Nix expression should be named e2fsprogs/default.nix. If there are multiple versions, this should be reflected in the filename, e.g. e2fsprogs/1.41.8.nix and e2fsprogs/1.41.9.nix. The version in the filename should leave out unnecessary detail. For instance, if we keep the latest Firefox 2.0.x and 3.5.x versions in Nixpkgs, they should be named firefox/2.0.nix and firefox/3.5.nix, respectively (which, at a given point, might contain versions 2.0.0.20 and 3.5.4). If a version requires many auxiliary files, you can use a subdirectory for each version, e.g. firefox/2.0/default.nix and firefox/3.5/default.nix.

All versions of a package must be included in all-packages.nix to make sure that they evaluate correctly.

Fetching Sources

There are multiple ways to fetch a package source in nixpkgs. The general guideline is that you should package reproducible sources with a high degree of availability. Right now there is only one fetcher which has mirroring support and that is fetchurl. Note that you should also prefer protocols which have a corresponding proxy environment variable.

You can find many source fetch helpers in pkgs/build-support/fetch*.

In the file pkgs/top-level/all-packages.nix you can find fetch helpers, these have names on the form fetchFrom*. The intention of these are to provide snapshot fetches but using the same api as some of the version controlled fetchers from pkgs/build-support/. As an example going from bad to good:

  • Bad: Uses git:// which won't be proxied.

    src = fetchgit {
      url = "git://github.com/NixOS/nix.git";
      rev = "1f795f9f44607cc5bec70d1300150bfefcef2aae";
      sha256 = "1cw5fszffl5pkpa6s6wjnkiv6lm5k618s32sp60kvmvpy7a2v9kg";
    }
    
  • Better: This is ok, but an archive fetch will still be faster.

    src = fetchgit {
      url = "https://github.com/NixOS/nix.git";
      rev = "1f795f9f44607cc5bec70d1300150bfefcef2aae";
      sha256 = "1cw5fszffl5pkpa6s6wjnkiv6lm5k618s32sp60kvmvpy7a2v9kg";
    }
    
  • Best: Fetches a snapshot archive and you get the rev you want.

    src = fetchFromGitHub {
      owner = "NixOS";
      repo = "nix";
      rev = "1f795f9f44607cc5bec70d1300150bfefcef2aae";
      sha256 = "1i2yxndxb6yc9l6c99pypbd92lfq5aac4klq7y2v93c9qvx2cgpc";
    }
    

When fetching from GitHub, commits must always be referenced by their full commit hash. This is because GitHub shares commit hashes among all forks and returns 404 Not Found when a short commit hash is ambiguous. It already happens for some short, 6-character commit hashes in nixpkgs. It is a practical vector for a denial-of-service attack by pushing large amounts of auto generated commits into forks and was already demonstrated against GitHub Actions Beta.

Find the value to put as sha256 by running nix-shell -p nix-prefetch-github --run "nix-prefetch-github --rev 1f795f9f44607cc5bec70d1300150bfefcef2aae NixOS nix".

Obtaining source hash

Preferred source hash type is sha256. There are several ways to get it.

  1. Prefetch URL (with nix-prefetch-XXX URL, where XXX is one of url, git, hg, cvs, bzr, svn). Hash is printed to stdout.

  2. Prefetch by package source (with nix-prefetch-url '<nixpkgs>' -A PACKAGE.src, where PACKAGE is package attribute name). Hash is printed to stdout.

    This works well when you've upgraded existing package version and want to find out new hash, but is useless if package can't be accessed by attribute or package has multiple sources (.srcs, architecture-dependent sources, etc).

  3. Upstream provided hash: use it when upstream provides sha256 or sha512 (when upstream provides md5, don't use it, compute sha256 instead).

    A little nuance is that nix-prefetch-* tools produce hash encoded with base32, but upstream usually provides hexadecimal (base16) encoding. Fetchers understand both formats. Nixpkgs does not standardize on any one format.

    You can convert between formats with nix-hash, for example:

    $ nix-hash --type sha256 --to-base32 HASH
    
  4. Extracting hash from local source tarball can be done with sha256sum. Use nix-prefetch-url file:///path/to/tarball if you want base32 hash.

  5. Fake hash: set the hash to one of

    • ""
    • lib.fakeHash
    • lib.fakeSha256
    • lib.fakeSha512

    in the package expression, attempt build and extract correct hash from error messages.

    ::: {.warning} You must use one of these four fake hashes and not some arbitrarily-chosen hash.

    See . :::

    This is last resort method when reconstructing source URL is non-trivial and nix-prefetch-url -A isnt applicable (for example, one of kodi dependencies). The easiest way then would be replace hash with a fake one and rebuild. Nix build will fail and error message will contain desired hash.

Obtaining hashes securely

Let's say Man-in-the-Middle (MITM) sits close to your network. Then instead of fetching source you can fetch malware, and instead of source hash you get hash of malware. Here are security considerations for this scenario:

  • http:// URLs are not secure to prefetch hash from;

  • hashes from upstream (in method 3) should be obtained via secure protocol;

  • https:// URLs are secure in methods 1, 2, 3;

  • https:// URLs are secure in method 5 only if you use one of the listed fake hashes. If you use any other hash, fetchurl will pass --insecure to curl and may then degrade to HTTP in case of TLS certificate expiration.

Patches

Patches available online should be retrieved using fetchpatch.

patches = [
  (fetchpatch {
    name = "fix-check-for-using-shared-freetype-lib.patch";
    url = "http://git.ghostscript.com/?p=ghostpdl.git;a=patch;h=8f5d285";
    sha256 = "1f0k043rng7f0rfl9hhb89qzvvksqmkrikmm38p61yfx51l325xr";
  })
];

Otherwise, you can add a .patch file to the nixpkgs repository. In the interest of keeping our maintenance burden to a minimum, only patches that are unique to nixpkgs should be added in this way.

If a patch is available online but does not cleanly apply, it can be modified in some fixed ways by using additional optional arguments for fetchpatch. Check for details.

patches = [ ./0001-changes.patch ];

If you do need to do create this sort of patch file, one way to do so is with git:

  1. Move to the root directory of the source code you're patching.

    $ cd the/program/source
    
  2. If a git repository is not already present, create one and stage all of the source files.

    $ git init
    $ git add .
    
  3. Edit some files to make whatever changes need to be included in the patch.

  4. Use git to create a diff, and pipe the output to a patch file:

    $ git diff -a > nixpkgs/pkgs/the/package/0001-changes.patch
    

Package tests

Tests are important to ensure quality and make reviews and automatic updates easy.

The following types of tests exists:

  • NixOS module tests, which spawn one or more NixOS VMs. They exercise both NixOS modules and the packaged programs used within them. For example, a NixOS module test can start a web server VM running the nginx module, and a client VM running curl or a graphical firefox, and test that they can talk to each other and display the correct content.
  • Nix package tests are a lightweight alternative to NixOS module tests. They should be used to create simple integration tests for packages, but cannot test NixOS services, and some programs with graphical user interfaces may also be difficult to test with them.
  • The checkPhase of a package, which should execute the unit tests that are included in the source code of a package.

Here in the nixpkgs manual we describe mostly package tests; for module tests head over to the corresponding section in the NixOS manual.

Writing inline package tests

For very simple tests, they can be written inline:

{ , yq-go }:

buildGoModule rec {
  

  passthru.tests = {
    simple = runCommand "${pname}-test" {} ''
      echo "test: 1" | ${yq-go}/bin/yq eval -j > $out
      [ "$(cat $out | tr -d $'\n ')" = '{"test":1}' ]
    '';
  };
}

Writing larger package tests

This is an example using the phoronix-test-suite package with the current best practices.

Add the tests in passthru.tests to the package definition like this:

{ stdenv, lib, fetchurl, callPackage }:

stdenv.mkDerivation {
  

  passthru.tests = {
    simple-execution = callPackage ./tests.nix { };
  };

  meta = {  };
}

Create tests.nix in the package directory:

{ runCommand, phoronix-test-suite }:

let
  inherit (phoronix-test-suite) pname version;
in

runCommand "${pname}-tests" { meta.timeout = 60; }
  ''
    # automatic initial setup to prevent interactive questions
    ${phoronix-test-suite}/bin/phoronix-test-suite enterprise-setup >/dev/null
    # get version of installed program and compare with package version
    if [[ `${phoronix-test-suite}/bin/phoronix-test-suite version` != *"${version}"*  ]]; then
      echo "Error: program version does not match package version"
      exit 1
    fi
    # run dummy command
    ${phoronix-test-suite}/bin/phoronix-test-suite dummy_module.dummy-command >/dev/null
    # needed for Nix to register the command as successful
    touch $out
  ''

Running package tests

You can run these tests with:

$ cd path/to/nixpkgs
$ nix-build -A phoronix-test-suite.tests

Examples of package tests

Here are examples of package tests:

Linking NixOS module tests to a package

Like package tests as shown above, NixOS module tests can also be linked to a package, so that the tests can be easily run when changing the related package.

For example, assuming we're packaging nginx, we can link its module test via passthru.tests:

{ stdenv, lib, nixosTests }:

stdenv.mkDerivation {
  ...

  passthru.tests = {
    nginx = nixosTests.nginx;
  };

  ...
}