summary refs log tree commit diff
path: root/maintainers/scripts/nixpkgs-lint.pl
blob: 43fb39413613947105605e460922cd7225dd30c3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
#! /usr/bin/env nix-shell
#! nix-shell -i perl -p perl perlPackages.XMLSimple

use strict;
use List::Util qw(min);
use XML::Simple qw(:strict);
use Getopt::Long qw(:config gnu_getopt);

# Parse the command line.
my $path = "<nixpkgs>";
my $filter = "*";
my $maintainer;

sub showHelp {
    print <<EOF;
Usage: $0 [--package=NAME] [--maintainer=REGEXP] [--file=PATH]

Check Nixpkgs for common errors/problems.

  -p, --package        filter packages by name (default is ‘*’)
  -m, --maintainer     filter packages by maintainer (case-insensitive regexp)
  -f, --file           path to Nixpkgs (default is ‘<nixpkgs>’)

Examples:
  \$ nixpkgs-lint -f /my/nixpkgs -p firefox
  \$ nixpkgs-lint -f /my/nixpkgs -m eelco
EOF
    exit 0;
}

GetOptions("package|p=s" => \$filter,
           "maintainer|m=s" => \$maintainer,
           "file|f=s" => \$path,
           "help" => sub { showHelp() }
    ) or exit 1;

# Evaluate Nixpkgs into an XML representation.
my $xml = `nix-env -f '$path' --arg overlays '[]' -qa '$filter' --xml --meta --drv-path`;
die "$0: evaluation of ‘$path’ failed\n" if $? != 0;

my $info = XMLin($xml, KeyAttr => { 'item' => '+attrPath', 'meta' => 'name' }, ForceArray => 1, SuppressEmpty => '' ) or die "cannot parse XML output";

# Check meta information.
print "=== Package meta information ===\n\n";
my $nrBadNames = 0;
my $nrMissingMaintainers = 0;
my $nrMissingPlatforms = 0;
my $nrMissingDescriptions = 0;
my $nrBadDescriptions = 0;
my $nrMissingLicenses = 0;

foreach my $attr (sort keys %{$info->{item}}) {
    my $pkg = $info->{item}->{$attr};

    my $pkgName = $pkg->{name};
    my $pkgVersion = "";
    if ($pkgName =~ /(.*)(-[0-9].*)$/) {
        $pkgName = $1;
        $pkgVersion = $2;
    }

    # Check the maintainers.
    my @maintainers;
    my $x = $pkg->{meta}->{maintainers};
    if (defined $x && $x->{type} eq "strings") {
        @maintainers = map { $_->{value} } @{$x->{string}};
    } elsif (defined $x->{value}) {
        @maintainers = ($x->{value});
    }

    if (defined $maintainer && scalar(grep { $_ =~ /$maintainer/i } @maintainers) == 0) {
        delete $info->{item}->{$attr};
        next;
    }

    if (scalar @maintainers == 0) {
        print "$attr: Lacks a maintainer\n";
        $nrMissingMaintainers++;
    }

    # Check the platforms.
    if (!defined $pkg->{meta}->{platforms}) {
        print "$attr: Lacks a platform\n";
        $nrMissingPlatforms++;
    }

    # Package names should not be capitalised.
    if ($pkgName =~ /^[A-Z]/) {
        print "$attr: package name ‘$pkgName’ should not be capitalised\n";
        $nrBadNames++;
    }

    if ($pkgVersion eq "") {
        print "$attr: package has no version\n";
        $nrBadNames++;
    }

    # Check the license.
    if (!defined $pkg->{meta}->{license}) {
        print "$attr: Lacks a license\n";
        $nrMissingLicenses++;
    }

    # Check the description.
    my $description = $pkg->{meta}->{description}->{value};
    if (!$description) {
        print "$attr: Lacks a description\n";
        $nrMissingDescriptions++;
    } else {
        my $bad = 0;
        if ($description =~ /^\s/) {
            print "$attr: Description starts with whitespace\n";
            $bad = 1;
        }
        if ($description =~ /\s$/) {
            print "$attr: Description ends with whitespace\n";
            $bad = 1;
        }
        if ($description =~ /\.$/) {
            print "$attr: Description ends with a period\n";
            $bad = 1;
        }
        if (index(lc($description), lc($attr)) != -1) {
            print "$attr: Description contains package name\n";
            $bad = 1;
        }
        $nrBadDescriptions++ if $bad;
    }
}

print "\n";

# Find packages that have the same name.
print "=== Package name collisions ===\n\n";

my %pkgsByName;

foreach my $attr (sort keys %{$info->{item}}) {
    my $pkg = $info->{item}->{$attr};
    #print STDERR "attr = $attr, name = $pkg->{name}\n";
    $pkgsByName{$pkg->{name}} //= [];
    push @{$pkgsByName{$pkg->{name}}}, $pkg;
}

my $nrCollisions = 0;
foreach my $name (sort keys %pkgsByName) {
    my @pkgs = @{$pkgsByName{$name}};

    # Filter attributes that are aliases of each other (e.g. yield the
    # same derivation path).
    my %drvsSeen;
    @pkgs = grep { my $x = $drvsSeen{$_->{drvPath}}; $drvsSeen{$_->{drvPath}} = 1; !defined $x } @pkgs;

    # Filter packages that have a lower priority.
    my $highest = min (map { $_->{meta}->{priority}->{value} // 0 } @pkgs);
    @pkgs = grep { ($_->{meta}->{priority}->{value} // 0) == $highest } @pkgs;

    next if scalar @pkgs == 1;

    $nrCollisions++;
    print "The following attributes evaluate to a package named ‘$name’:\n";
    print "  ", join(", ", map { $_->{attrPath} } @pkgs), "\n\n";
}

print "=== Bottom line ===\n";
print "Number of packages: ", scalar(keys %{$info->{item}}), "\n";
print "Number of bad names: $nrBadNames\n";
print "Number of missing maintainers: $nrMissingMaintainers\n";
print "Number of missing platforms: $nrMissingPlatforms\n";
print "Number of missing licenses: $nrMissingLicenses\n";
print "Number of missing descriptions: $nrMissingDescriptions\n";
print "Number of bad descriptions: $nrBadDescriptions\n";
print "Number of name collisions: $nrCollisions\n";