Merge pull request #10768 from rycee/refactor/nix-generate-from-cpan

nix-generate-from-cpan: large refactor

+390 -99
+7 -5
maintainers/scripts/nix-generate-from-cpan.nix
··· 1 { stdenv, makeWrapper, perl, perlPackages }: 2 3 stdenv.mkDerivation { 4 - name = "nix-generate-from-cpan-1"; 5 6 - buildInputs = [ makeWrapper perl perlPackages.YAMLLibYAML perlPackages.JSON perlPackages.CPANPLUS ]; 7 8 - unpackPhase = "true"; 9 - buildPhase = "true"; 10 11 installPhase = 12 '' 13 mkdir -p $out/bin 14 cp ${./nix-generate-from-cpan.pl} $out/bin/nix-generate-from-cpan 15 wrapProgram $out/bin/nix-generate-from-cpan --set PERL5LIB $PERL5LIB 16 ''; 17 18 meta = { 19 - maintainers = [ stdenv.lib.maintainers.eelco ]; 20 description = "Utility to generate a Nix expression for a Perl package from CPAN"; 21 }; 22 }
··· 1 { stdenv, makeWrapper, perl, perlPackages }: 2 3 stdenv.mkDerivation { 4 + name = "nix-generate-from-cpan-2"; 5 6 + buildInputs = with perlPackages; [ 7 + makeWrapper perl CPANMeta GetoptLongDescriptive CPANPLUS Readonly Log4Perl 8 + ]; 9 10 + phases = [ "installPhase" ]; 11 12 installPhase = 13 '' 14 mkdir -p $out/bin 15 cp ${./nix-generate-from-cpan.pl} $out/bin/nix-generate-from-cpan 16 + patchShebangs $out/bin/nix-generate-from-cpan 17 wrapProgram $out/bin/nix-generate-from-cpan --set PERL5LIB $PERL5LIB 18 ''; 19 20 meta = { 21 + maintainers = with stdenv.lib.maintainers; [ eelco rycee ]; 22 description = "Utility to generate a Nix expression for a Perl package from CPAN"; 23 }; 24 }
+383 -94
maintainers/scripts/nix-generate-from-cpan.pl
··· 1 - #! /run/current-system/sw/bin/perl -w 2 3 use strict; 4 - use CPANPLUS::Backend; 5 - use YAML::XS; 6 - use JSON; 7 8 - my $module_name = $ARGV[0]; 9 - die "syntax: $0 <MODULE-NAME>\n" unless defined $module_name; 10 11 - my $cb = CPANPLUS::Backend->new; 12 13 - my @modules = $cb->search(type => "name", allow => [$module_name]); 14 - die "module $module_name not found\n" if scalar @modules == 0; 15 - die "multiple packages that match module $module_name\n" if scalar @modules > 1; 16 - my $module = $modules[0]; 17 18 sub pkg_to_attr { 19 - my ($pkg_name) = @_; 20 - my $attr_name = $pkg_name; 21 - $attr_name =~ s/-\d.*//; # strip version 22 - return "LWP" if $attr_name eq "libwww-perl"; 23 - $attr_name =~ s/-//g; 24 - return $attr_name; 25 } 26 27 sub get_pkg_name { 28 my ($module) = @_; 29 - my $pkg_name = $module->package; 30 - $pkg_name =~ s/\.tar.*//; 31 - $pkg_name =~ s/\.zip//; 32 - return $pkg_name; 33 } 34 35 - my $pkg_name = get_pkg_name $module; 36 - my $attr_name = pkg_to_attr $pkg_name; 37 38 - print STDERR "attribute name: ", $attr_name, "\n"; 39 - print STDERR "module: ", $module->module, "\n"; 40 - print STDERR "version: ", $module->version, "\n"; 41 - print STDERR "package: ", $module->package, , " (", $pkg_name, ", ", $attr_name, ")\n"; 42 - print STDERR "path: ", $module->path, "\n"; 43 - 44 - my $tar_path = $module->fetch(); 45 - print STDERR "downloaded to: $tar_path\n"; 46 - print STDERR "sha-256: ", $module->status->checksum_value, "\n"; 47 - 48 - my $pkg_path = $module->extract(); 49 - print STDERR "unpacked to: $pkg_path\n"; 50 51 - my $meta; 52 - if (-e "$pkg_path/META.yml") { 53 - eval { 54 - $meta = YAML::XS::LoadFile("$pkg_path/META.yml"); 55 - }; 56 - if ($@) { 57 - system("iconv -f windows-1252 -t utf-8 '$pkg_path/META.yml' > '$pkg_path/META.yml.tmp'"); 58 - $meta = YAML::XS::LoadFile("$pkg_path/META.yml.tmp"); 59 } 60 - } elsif (-e "$pkg_path/META.json") { 61 - local $/; 62 - open(my $fh, '<', "$pkg_path/META.json") or die; 63 - $meta = decode_json(<$fh>); 64 - } else { 65 - warn "package has no META.yml or META.json\n"; 66 } 67 - 68 - print STDERR "metadata: ", encode_json($meta), "\n" if defined $meta; 69 70 # Map a module to the attribute corresponding to its package 71 # (e.g. HTML::HeadParser will be mapped to HTMLParser, because that 72 # module is in the HTML-Parser package). 73 sub module_to_pkg { 74 - my ($module_name) = @_; 75 - my @modules = $cb->search(type => "name", allow => [$module_name]); 76 - if (scalar @modules == 0) { 77 # Fallback. 78 $module_name =~ s/:://g; 79 return $module_name; 80 } 81 - my $module = $modules[0]; 82 - my $attr_name = pkg_to_attr(get_pkg_name $module); 83 - print STDERR "mapped dep $module_name to $attr_name\n"; 84 return $attr_name; 85 } 86 87 sub get_deps { 88 - my ($type) = @_; 89 - my $deps; 90 - if (defined $meta->{prereqs}) { 91 - die "unimplemented"; 92 - } elsif ($type eq "runtime") { 93 - $deps = $meta->{requires}; 94 - } elsif ($type eq "configure") { 95 - $deps = $meta->{configure_requires}; 96 - } elsif ($type eq "build") { 97 - $deps = $meta->{build_requires}; 98 - } 99 my @res; 100 - foreach my $n (keys %{$deps}) { 101 next if $n eq "perl"; 102 # Hacky way to figure out if this module is part of Perl. 103 - if ($n !~ /^JSON/ && $n !~ /^YAML/ && $n !~ /^Module::Pluggable/) { 104 eval "use $n;"; 105 - if (!$@) { 106 - print STDERR "skipping Perl-builtin module $n\n"; 107 next; 108 } 109 } 110 - push @res, module_to_pkg($n); 111 } 112 return @res; 113 } 114 115 sub uniq { 116 - return keys %{{ map { $_ => 1 } @_ }}; 117 } 118 119 - my @build_deps = sort(uniq(get_deps("configure"), get_deps("build"), get_deps("test"))); 120 - print STDERR "build deps: @build_deps\n"; 121 122 - my @runtime_deps = sort(uniq(get_deps("runtime"))); 123 - print STDERR "runtime deps: @runtime_deps\n"; 124 125 - my $homepage = $meta->{resources}->{homepage}; 126 - print STDERR "homepage: $homepage\n" if defined $homepage; 127 128 - my $description = $meta->{abstract}; 129 - if (defined $description) { 130 - $description = uc(substr($description, 0, 1)) . substr($description, 1); # capitalise first letter 131 - $description =~ s/\.$//; # remove period at the end 132 $description =~ s/\s*$//; 133 $description =~ s/^\s*//; 134 - print STDERR "description: $description\n"; 135 } 136 137 - my $license = $meta->{license}; 138 - if (defined $license) { 139 - $license = "perl5" if $license eq "perl_5"; 140 - print STDERR "license: $license\n"; 141 - } 142 143 - my $build_fun = -e "$pkg_path/Build.PL" && ! -e "$pkg_path/Makefile.PL" ? "buildPerlModule" : "buildPerlPackage"; 144 145 print STDERR "===\n"; 146 147 print <<EOF; 148 - $attr_name = $build_fun { 149 name = "$pkg_name"; 150 src = fetchurl { 151 - url = mirror://cpan/${\$module->path}/${\$module->package}; 152 sha256 = "${\$module->status->checksum_value}"; 153 }; 154 EOF ··· 168 description = "$description"; 169 EOF 170 print <<EOF if defined $license; 171 - license = "$license"; 172 EOF 173 print <<EOF; 174 };
··· 1 + #!/usr/bin/env perl 2 3 + use utf8; 4 use strict; 5 + use warnings; 6 + 7 + use CPAN::Meta(); 8 + use CPANPLUS::Backend(); 9 + use Getopt::Long::Descriptive qw( describe_options ); 10 + use JSON::PP qw( encode_json ); 11 + use Log::Log4perl qw(:easy); 12 + use Readonly(); 13 + 14 + # Readonly hash that maps CPAN style license strings to information 15 + # necessary to generate a Nixpkgs style license attribute. 16 + Readonly::Hash my %LICENSE_MAP => ( 17 + 18 + # The Perl 5 License (Artistic 1 & GPL 1 or later). 19 + perl_5 => { 20 + licenses => [qw( artistic1 gpl1Plus )] 21 + }, 22 + 23 + # GNU Affero General Public License, Version 3. 24 + agpl_3 => { 25 + licenses => [qw( agpl3Plus )], 26 + amb => 1 27 + }, 28 + 29 + # Apache Software License, Version 1.1. 30 + apache_1_1 => { 31 + licenses => ["Apache License 1.1"], 32 + in_set => 0 33 + }, 34 + 35 + # Apache License, Version 2.0. 36 + apache_2_0 => { 37 + licenses => [qw( asl20 )] 38 + }, 39 + 40 + # Artistic License, (Version 1). 41 + artistic_1 => { 42 + licenses => [qw( artistic1 )] 43 + }, 44 + 45 + # Artistic License, Version 2.0. 46 + artistic_2 => { 47 + licenses => [qw( artistic2 )] 48 + }, 49 + 50 + # BSD License (three-clause). 51 + bsd => { 52 + licenses => [qw( bsd3 )], 53 + amb => 1 54 + }, 55 + 56 + # FreeBSD License (two-clause). 57 + freebsd => { 58 + licenses => [qw( bsd2 )] 59 + }, 60 + 61 + # GNU Free Documentation License, Version 1.2. 62 + gfdl_1_2 => { 63 + licenses => [qw( fdl12 )] 64 + }, 65 + 66 + # GNU Free Documentation License, Version 1.3. 67 + gfdl_1_3 => { 68 + licenses => [qw( fdl13 )] 69 + }, 70 + 71 + # GNU General Public License, Version 1. 72 + gpl_1 => { 73 + licenses => [qw( gpl1Plus )], 74 + amb => 1 75 + }, 76 + 77 + # GNU General Public License, Version 2. Note, we will interpret 78 + # "gpl" alone as GPL v2+. 79 + gpl_2 => { 80 + licenses => [qw( gpl2Plus )], 81 + amb => 1 82 + }, 83 + 84 + # GNU General Public License, Version 3. 85 + gpl_3 => { 86 + licenses => [qw( gpl3Plus )], 87 + amb => 1 88 + }, 89 + 90 + # GNU Lesser General Public License, Version 2.1. Note, we will 91 + # interpret "gpl" alone as LGPL v2.1+. 92 + lgpl_2_1 => { 93 + licenses => [qw( lgpl21Plus )], 94 + amb => 1 95 + }, 96 + 97 + # GNU Lesser General Public License, Version 3.0. 98 + lgpl_3_0 => { 99 + licenses => [qw( lgpl3Plus )], 100 + amb => 1 101 + }, 102 + 103 + # MIT (aka X11) License. 104 + mit => { 105 + licenses => [qw( mit )] 106 + }, 107 + 108 + # Mozilla Public License, Version 1.0. 109 + mozilla_1_0 => { 110 + licenses => [qw( mpl10 )] 111 + }, 112 + 113 + # Mozilla Public License, Version 1.1. 114 + mozilla_1_1 => { 115 + licenses => [qw( mpl11 )] 116 + }, 117 + 118 + # OpenSSL License. 119 + openssl => { 120 + licenses => [qw( openssl )] 121 + }, 122 + 123 + # Q Public License, Version 1.0. 124 + qpl_1_0 => { 125 + licenses => [qw( qpl )] 126 + }, 127 + 128 + # Original SSLeay License. 129 + ssleay => { 130 + licenses => ["Original SSLeay License"], 131 + in_set => 0 132 + }, 133 + 134 + # Sun Internet Standards Source License (SISSL). 135 + sun => { 136 + licenses => ["Sun Industry Standards Source License v1.1"], 137 + in_set => 0 138 + }, 139 + 140 + # zlib License. 141 + zlib => { 142 + licenses => [qw( zlib )] 143 + }, 144 + 145 + # Other Open Source Initiative (OSI) approved license. 146 + open_source => { 147 + licenses => [qw( free )], 148 + amb => 1 149 + }, 150 + 151 + # Requires special permission from copyright holder. 152 + restricted => { 153 + licenses => [qw( unfree )], 154 + amb => 1 155 + }, 156 + 157 + # Not an OSI approved license, but not restricted. Note, we 158 + # currently map this to unfreeRedistributable, which is a 159 + # conservative choice. 160 + unrestricted => { 161 + licenses => [qw( unfreeRedistributable )], 162 + amb => 1 163 + }, 164 + 165 + # License not provided in metadata. 166 + unknown => { 167 + licenses => [qw( unknown )], 168 + amb => 1 169 + } 170 + ); 171 + 172 + sub handle_opts { 173 + my ( $opt, $usage ) = describe_options( 174 + 'usage: $0 %o MODULE', 175 + [ 'maintainer|m=s', 'the package maintainer' ], 176 + [ 'debug|d', 'enable debug output' ], 177 + [ 'help', 'print usage message and exit' ] 178 + ); 179 + 180 + if ( $opt->help ) { 181 + print $usage->text; 182 + exit; 183 + } 184 185 + my $module_name = $ARGV[0]; 186 187 + if ( !defined $module_name ) { 188 + print STDERR "Missing module name\n"; 189 + print STDERR $usage->text; 190 + exit 1; 191 + } 192 193 + return ( $opt, $module_name ); 194 + } 195 + 196 + # Takes a Perl package attribute name and returns 1 if the name cannot 197 + # be referred to as a bareword. This typically happens if the package 198 + # name is a reserved Nix keyword. 199 + sub is_reserved { 200 + my ($pkg) = @_; 201 + 202 + return $pkg =~ /^(?: assert | 203 + else | 204 + if | 205 + import | 206 + in | 207 + inherit | 208 + let | 209 + rec | 210 + then | 211 + while | 212 + with )$/x; 213 + } 214 215 sub pkg_to_attr { 216 + my ($module) = @_; 217 + my $attr_name = $module->package_name; 218 + if ( $attr_name eq "libwww-perl" ) { 219 + return "LWP"; 220 + } 221 + else { 222 + $attr_name =~ s/-//g; 223 + return $attr_name; 224 + } 225 } 226 227 sub get_pkg_name { 228 my ($module) = @_; 229 + return $module->package_name . '-' . $module->package_version; 230 } 231 232 + sub read_meta { 233 + my ($pkg_path) = @_; 234 235 + my $yaml_path = "$pkg_path/META.yml"; 236 + my $json_path = "$pkg_path/META.json"; 237 + my $meta; 238 239 + if ( -r $json_path ) { 240 + $meta = CPAN::Meta->load_file($json_path); 241 } 242 + elsif ( -r $yaml_path ) { 243 + $meta = CPAN::Meta->load_file($yaml_path); 244 + } 245 + else { 246 + WARN("package has no META.yml or META.json"); 247 + } 248 + 249 + return $meta; 250 } 251 252 # Map a module to the attribute corresponding to its package 253 # (e.g. HTML::HeadParser will be mapped to HTMLParser, because that 254 # module is in the HTML-Parser package). 255 sub module_to_pkg { 256 + my ( $cb, $module_name ) = @_; 257 + my @modules = $cb->search( type => "name", allow => [$module_name] ); 258 + if ( scalar @modules == 0 ) { 259 + 260 # Fallback. 261 $module_name =~ s/:://g; 262 return $module_name; 263 } 264 + my $module = $modules[0]; 265 + my $attr_name = pkg_to_attr($module); 266 + DEBUG("mapped dep $module_name to $attr_name"); 267 return $attr_name; 268 } 269 270 sub get_deps { 271 + my ( $cb, $meta, $type ) = @_; 272 + 273 + return if !defined $meta; 274 + 275 + my $prereqs = $meta->effective_prereqs; 276 + my $deps = $prereqs->requirements_for( $type, "requires" ); 277 my @res; 278 + foreach my $n ( $deps->required_modules ) { 279 next if $n eq "perl"; 280 + 281 # Hacky way to figure out if this module is part of Perl. 282 + if ( $n !~ /^JSON/ && $n !~ /^YAML/ && $n !~ /^Module::Pluggable/ ) { 283 eval "use $n;"; 284 + if ( !$@ ) { 285 + DEBUG("skipping Perl-builtin module $n"); 286 next; 287 } 288 } 289 + 290 + my $pkg = module_to_pkg( $cb, $n ); 291 + 292 + # If the package name is reserved then we need to refer to it 293 + # through the "self" variable. 294 + $pkg = "self.\"$pkg\"" if is_reserved($pkg); 295 + 296 + push @res, $pkg; 297 } 298 return @res; 299 } 300 301 sub uniq { 302 + return keys %{ { map { $_ => 1 } @_ } }; 303 } 304 305 + sub render_license { 306 + my ($cpan_license) = @_; 307 + 308 + return if !defined $cpan_license; 309 + 310 + my $licenses; 311 312 + # If the license is ambiguous then we'll print an extra warning. 313 + # For example, "gpl_2" is ambiguous since it may refer to exactly 314 + # "GPL v2" or to "GPL v2 or later". 315 + my $amb = 0; 316 317 + # Whether the license is available inside `stdenv.lib.licenses`. 318 + my $in_set = 1; 319 320 + my $nix_license = $LICENSE_MAP{$cpan_license}; 321 + if ( !$nix_license ) { 322 + WARN("Unknown license: $cpan_license"); 323 + $licenses = [$cpan_license]; 324 + $in_set = 0; 325 + } 326 + else { 327 + $licenses = $nix_license->{licenses}; 328 + $amb = $nix_license->{amb}; 329 + $in_set = !$nix_license->{in_set}; 330 + } 331 + 332 + my $license_line; 333 + 334 + if ( @$licenses == 0 ) { 335 + 336 + # Avoid defining the license line. 337 + } 338 + elsif ($in_set) { 339 + my $lic = 'stdenv.lib.licenses'; 340 + if ( @$licenses == 1 ) { 341 + $license_line = "$lic.$licenses->[0]"; 342 + } 343 + else { 344 + $license_line = "with $lic; [ " . join( ' ', @$licenses ) . " ]"; 345 + } 346 + } 347 + else { 348 + if ( @$licenses == 1 ) { 349 + $license_line = $licenses->[0]; 350 + } 351 + else { 352 + $license_line = '[ ' . join( ' ', @$licenses ) . ' ]'; 353 + } 354 + } 355 + 356 + INFO("license: $cpan_license"); 357 + WARN("License '$cpan_license' is ambiguous, please verify") if $amb; 358 + 359 + return $license_line; 360 + } 361 + 362 + my ( $opt, $module_name ) = handle_opts(); 363 + 364 + Log::Log4perl->easy_init( 365 + { 366 + level => $opt->debug ? $DEBUG : $INFO, 367 + layout => '%m%n' 368 + } 369 + ); 370 + 371 + my $cb = CPANPLUS::Backend->new; 372 + 373 + my @modules = $cb->search( type => "name", allow => [$module_name] ); 374 + die "module $module_name not found\n" if scalar @modules == 0; 375 + die "multiple packages that match module $module_name\n" if scalar @modules > 1; 376 + my $module = $modules[0]; 377 + 378 + my $pkg_name = get_pkg_name $module; 379 + my $attr_name = pkg_to_attr $module; 380 + 381 + INFO( "attribute name: ", $attr_name ); 382 + INFO( "module: ", $module->module ); 383 + INFO( "version: ", $module->version ); 384 + INFO( "package: ", $module->package, " (", $pkg_name, ", ", $attr_name, ")" ); 385 + INFO( "path: ", $module->path ); 386 + 387 + my $tar_path = $module->fetch(); 388 + INFO( "downloaded to: ", $tar_path ); 389 + INFO( "sha-256: ", $module->status->checksum_value ); 390 + 391 + my $pkg_path = $module->extract(); 392 + INFO( "unpacked to: ", $pkg_path ); 393 + 394 + my $meta = read_meta($pkg_path); 395 + 396 + DEBUG( "metadata: ", encode_json( $meta->as_struct ) ) if defined $meta; 397 + 398 + my @build_deps = sort( uniq( 399 + get_deps( $cb, $meta, "configure" ), 400 + get_deps( $cb, $meta, "build" ), 401 + get_deps( $cb, $meta, "test" ) 402 + ) ); 403 + INFO("build deps: @build_deps"); 404 + 405 + my @runtime_deps = sort( uniq( get_deps( $cb, $meta, "runtime" ) ) ); 406 + INFO("runtime deps: @runtime_deps"); 407 + 408 + my $homepage = $meta ? $meta->resources->{homepage} : undef; 409 + INFO("homepage: $homepage") if defined $homepage; 410 + 411 + my $description = $meta ? $meta->abstract : undef; 412 + if ( defined $description ) { 413 + $description = uc( substr( $description, 0, 1 ) ) 414 + . substr( $description, 1 ); # capitalise first letter 415 + $description =~ s/\.$//; # remove period at the end 416 $description =~ s/\s*$//; 417 $description =~ s/^\s*//; 418 + $description =~ s/\n+/ /; # Replace new lines by space. 419 + INFO("description: $description"); 420 } 421 422 + #print(Data::Dumper::Dumper($meta->licenses) . "\n"); 423 + my $license = $meta ? render_license( $meta->licenses ) : undef; 424 + 425 + INFO( "RSS feed: https://metacpan.org/feed/distribution/", 426 + $module->package_name ); 427 428 + my $build_fun = -e "$pkg_path/Build.PL" 429 + && !-e "$pkg_path/Makefile.PL" ? "buildPerlModule" : "buildPerlPackage"; 430 431 print STDERR "===\n"; 432 433 print <<EOF; 434 + "$attr_name" = $build_fun rec { 435 name = "$pkg_name"; 436 src = fetchurl { 437 + url = "mirror://cpan/${\$module->path}/\${name}.${\$module->package_extension}"; 438 sha256 = "${\$module->status->checksum_value}"; 439 }; 440 EOF ··· 454 description = "$description"; 455 EOF 456 print <<EOF if defined $license; 457 + license = $license; 458 + EOF 459 + print <<EOF if $opt->maintainer; 460 + maintainers = [ maintainers.${\$opt->maintainer} ]; 461 EOF 462 print <<EOF; 463 };