1#! /usr/bin/env nix-shell
2#! nix-shell -i perl -p perl perlPackages.XMLSimple
3
4use strict;
5use List::Util qw(min);
6use XML::Simple qw(:strict);
7use Getopt::Long qw(:config gnu_getopt);
8
9# Parse the command line.
10my $path = "<nixpkgs>";
11my $filter = "*";
12my $maintainer;
13
14sub showHelp {
15 print <<EOF;
16Usage: $0 [--package=NAME] [--maintainer=REGEXP] [--file=PATH]
17
18Check Nixpkgs for common errors/problems.
19
20 -p, --package filter packages by name (default is ‘*’)
21 -m, --maintainer filter packages by maintainer (case-insensitive regexp)
22 -f, --file path to Nixpkgs (default is ‘<nixpkgs>’)
23
24Examples:
25 \$ nixpkgs-lint -f /my/nixpkgs -p firefox
26 \$ nixpkgs-lint -f /my/nixpkgs -m eelco
27EOF
28 exit 0;
29}
30
31GetOptions("package|p=s" => \$filter,
32 "maintainer|m=s" => \$maintainer,
33 "file|f=s" => \$path,
34 "help" => sub { showHelp() }
35 ) or exit 1;
36
37# Evaluate Nixpkgs into an XML representation.
38my $xml = `nix-env -f '$path' --arg overlays '[]' -qa '$filter' --xml --meta --drv-path`;
39die "$0: evaluation of ‘$path’ failed\n" if $? != 0;
40
41my $info = XMLin($xml, KeyAttr => { 'item' => '+attrPath', 'meta' => 'name' }, ForceArray => 1, SuppressEmpty => '' ) or die "cannot parse XML output";
42
43# Check meta information.
44print "=== Package meta information ===\n\n";
45my $nrBadNames = 0;
46my $nrMissingMaintainers = 0;
47my $nrMissingPlatforms = 0;
48my $nrMissingDescriptions = 0;
49my $nrBadDescriptions = 0;
50my $nrMissingLicenses = 0;
51
52foreach my $attr (sort keys %{$info->{item}}) {
53 my $pkg = $info->{item}->{$attr};
54
55 my $pkgName = $pkg->{name};
56 my $pkgVersion = "";
57 if ($pkgName =~ /(.*)(-[0-9].*)$/) {
58 $pkgName = $1;
59 $pkgVersion = $2;
60 }
61
62 # Check the maintainers.
63 my @maintainers;
64 my $x = $pkg->{meta}->{maintainers};
65 if (defined $x && $x->{type} eq "strings") {
66 @maintainers = map { $_->{value} } @{$x->{string}};
67 } elsif (defined $x->{value}) {
68 @maintainers = ($x->{value});
69 }
70
71 if (defined $maintainer && scalar(grep { $_ =~ /$maintainer/i } @maintainers) == 0) {
72 delete $info->{item}->{$attr};
73 next;
74 }
75
76 if (scalar @maintainers == 0) {
77 print "$attr: Lacks a maintainer\n";
78 $nrMissingMaintainers++;
79 }
80
81 # Check the platforms.
82 if (!defined $pkg->{meta}->{platforms}) {
83 print "$attr: Lacks a platform\n";
84 $nrMissingPlatforms++;
85 }
86
87 # Package names should not be capitalised.
88 if ($pkgName =~ /^[A-Z]/) {
89 print "$attr: package name ‘$pkgName’ should not be capitalised\n";
90 $nrBadNames++;
91 }
92
93 if ($pkgVersion eq "") {
94 print "$attr: package has no version\n";
95 $nrBadNames++;
96 }
97
98 # Check the license.
99 if (!defined $pkg->{meta}->{license}) {
100 print "$attr: Lacks a license\n";
101 $nrMissingLicenses++;
102 }
103
104 # Check the description.
105 my $description = $pkg->{meta}->{description}->{value};
106 if (!$description) {
107 print "$attr: Lacks a description\n";
108 $nrMissingDescriptions++;
109 } else {
110 my $bad = 0;
111 if ($description =~ /^\s/) {
112 print "$attr: Description starts with whitespace\n";
113 $bad = 1;
114 }
115 if ($description =~ /\s$/) {
116 print "$attr: Description ends with whitespace\n";
117 $bad = 1;
118 }
119 if ($description =~ /\.$/) {
120 print "$attr: Description ends with a period\n";
121 $bad = 1;
122 }
123 if (index(lc($description), lc($attr)) != -1) {
124 print "$attr: Description contains package name\n";
125 $bad = 1;
126 }
127 $nrBadDescriptions++ if $bad;
128 }
129}
130
131print "\n";
132
133# Find packages that have the same name.
134print "=== Package name collisions ===\n\n";
135
136my %pkgsByName;
137
138foreach my $attr (sort keys %{$info->{item}}) {
139 my $pkg = $info->{item}->{$attr};
140 #print STDERR "attr = $attr, name = $pkg->{name}\n";
141 $pkgsByName{$pkg->{name}} //= [];
142 push @{$pkgsByName{$pkg->{name}}}, $pkg;
143}
144
145my $nrCollisions = 0;
146foreach my $name (sort keys %pkgsByName) {
147 my @pkgs = @{$pkgsByName{$name}};
148
149 # Filter attributes that are aliases of each other (e.g. yield the
150 # same derivation path).
151 my %drvsSeen;
152 @pkgs = grep { my $x = $drvsSeen{$_->{drvPath}}; $drvsSeen{$_->{drvPath}} = 1; !defined $x } @pkgs;
153
154 # Filter packages that have a lower priority.
155 my $highest = min (map { $_->{meta}->{priority}->{value} // 0 } @pkgs);
156 @pkgs = grep { ($_->{meta}->{priority}->{value} // 0) == $highest } @pkgs;
157
158 next if scalar @pkgs == 1;
159
160 $nrCollisions++;
161 print "The following attributes evaluate to a package named ‘$name’:\n";
162 print " ", join(", ", map { $_->{attrPath} } @pkgs), "\n\n";
163}
164
165print "=== Bottom line ===\n";
166print "Number of packages: ", scalar(keys %{$info->{item}}), "\n";
167print "Number of bad names: $nrBadNames\n";
168print "Number of missing maintainers: $nrMissingMaintainers\n";
169print "Number of missing platforms: $nrMissingPlatforms\n";
170print "Number of missing licenses: $nrMissingLicenses\n";
171print "Number of missing descriptions: $nrMissingDescriptions\n";
172print "Number of bad descriptions: $nrBadDescriptions\n";
173print "Number of name collisions: $nrCollisions\n";