at 18.09-beta 12 kB view raw
1#!/usr/bin/env perl 2 3use utf8; 4use strict; 5use warnings; 6 7use CPAN::Meta(); 8use CPANPLUS::Backend(); 9use Getopt::Long::Descriptive qw( describe_options ); 10use JSON::PP qw( encode_json ); 11use Log::Log4perl qw(:easy); 12use Readonly(); 13 14# Readonly hash that maps CPAN style license strings to information 15# necessary to generate a Nixpkgs style license attribute. 16Readonly::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 172sub 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. 199sub 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 215sub 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 227sub get_pkg_name { 228 my ($module) = @_; 229 return $module->package_name . '-' . $module->package_version; 230} 231 232sub 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). 255sub 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 270sub 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 # Figure out whether the module is a core module by attempting 282 # to `use` the module in a pure Perl interpreter and checking 283 # whether it succeeded. Note, $^X is a magic variable holding 284 # the path to the running Perl interpreter. 285 if ( system("env -i $^X -M$n -e1 >/dev/null 2>&1") == 0 ) { 286 DEBUG("skipping Perl-builtin module $n"); 287 next; 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 301sub uniq { 302 return keys %{ { map { $_ => 1 } @_ } }; 303} 304 305sub 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 362my ( $opt, $module_name ) = handle_opts(); 363 364Log::Log4perl->easy_init( 365 { 366 level => $opt->debug ? $DEBUG : $INFO, 367 layout => '%m%n' 368 } 369); 370 371my $cb = CPANPLUS::Backend->new; 372 373my @modules = $cb->search( type => "name", allow => [$module_name] ); 374die "module $module_name not found\n" if scalar @modules == 0; 375die "multiple packages that match module $module_name\n" if scalar @modules > 1; 376my $module = $modules[0]; 377 378my $pkg_name = get_pkg_name $module; 379my $attr_name = pkg_to_attr $module; 380 381INFO( "attribute name: ", $attr_name ); 382INFO( "module: ", $module->module ); 383INFO( "version: ", $module->version ); 384INFO( "package: ", $module->package, " (", $pkg_name, ", ", $attr_name, ")" ); 385INFO( "path: ", $module->path ); 386 387my $tar_path = $module->fetch(); 388INFO( "downloaded to: ", $tar_path ); 389INFO( "sha-256: ", $module->status->checksum_value ); 390 391my $pkg_path = $module->extract(); 392INFO( "unpacked to: ", $pkg_path ); 393 394my $meta = read_meta($pkg_path); 395 396DEBUG( "metadata: ", encode_json( $meta->as_struct ) ) if defined $meta; 397 398my @runtime_deps = sort( uniq( get_deps( $cb, $meta, "runtime" ) ) ); 399INFO("runtime deps: @runtime_deps"); 400 401my @build_deps = sort( uniq( 402 get_deps( $cb, $meta, "configure" ), 403 get_deps( $cb, $meta, "build" ), 404 get_deps( $cb, $meta, "test" ) 405) ); 406 407# Filter out runtime dependencies since those are already handled. 408my %in_runtime_deps = map { $_ => 1 } @runtime_deps; 409@build_deps = grep { not $in_runtime_deps{$_} } @build_deps; 410 411INFO("build deps: @build_deps"); 412 413my $homepage = $meta ? $meta->resources->{homepage} : undef; 414INFO("homepage: $homepage") if defined $homepage; 415 416my $description = $meta ? $meta->abstract : undef; 417if ( defined $description ) { 418 $description = uc( substr( $description, 0, 1 ) ) 419 . substr( $description, 1 ); # capitalise first letter 420 $description =~ s/\.$//; # remove period at the end 421 $description =~ s/\s*$//; 422 $description =~ s/^\s*//; 423 $description =~ s/\n+/ /; # Replace new lines by space. 424 INFO("description: $description"); 425} 426 427#print(Data::Dumper::Dumper($meta->licenses) . "\n"); 428my $license = $meta ? render_license( $meta->licenses ) : undef; 429 430INFO( "RSS feed: https://metacpan.org/feed/distribution/", 431 $module->package_name ); 432 433my $build_fun = -e "$pkg_path/Build.PL" 434 && !-e "$pkg_path/Makefile.PL" ? "buildPerlModule" : "buildPerlPackage"; 435 436print STDERR "===\n"; 437 438print <<EOF; 439 ${\(is_reserved($attr_name) ? "\"$attr_name\"" : $attr_name)} = $build_fun rec { 440 name = "$pkg_name"; 441 src = fetchurl { 442 url = "mirror://cpan/${\$module->path}/\${name}.${\$module->package_extension}"; 443 sha256 = "${\$module->status->checksum_value}"; 444 }; 445EOF 446print <<EOF if scalar @build_deps > 0; 447 buildInputs = [ @build_deps ]; 448EOF 449print <<EOF if scalar @runtime_deps > 0; 450 propagatedBuildInputs = [ @runtime_deps ]; 451EOF 452print <<EOF; 453 meta = { 454EOF 455print <<EOF if defined $homepage; 456 homepage = $homepage; 457EOF 458print <<EOF if defined $description && $description ne "Unknown"; 459 description = "$description"; 460EOF 461print <<EOF if defined $license; 462 license = $license; 463EOF 464print <<EOF if $opt->maintainer; 465 maintainers = [ maintainers.${\$opt->maintainer} ]; 466EOF 467print <<EOF; 468 }; 469 }; 470EOF