at master 5.3 kB view raw
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";