]> git.vpit.fr Git - perl/modules/CPANPLUS-Dist-Gentoo.git/blob - lib/CPANPLUS/Dist/Gentoo.pm
a6b773407e4320fa3c55c3e342bc6709e636f526
[perl/modules/CPANPLUS-Dist-Gentoo.git] / lib / CPANPLUS / Dist / Gentoo.pm
1 package CPANPLUS::Dist::Gentoo;
2
3 use strict;
4 use warnings;
5
6 use Cwd qw/abs_path/;
7 use List::Util qw/reduce/;
8 use File::Copy ();
9 use File::Path ();
10 use File::Spec;
11
12 use IPC::Cmd qw/run can_run/;
13 use Parse::CPAN::Meta ();
14
15 use CPANPLUS::Error ();
16
17 use base qw/CPANPLUS::Dist::Base/;
18
19 use CPANPLUS::Dist::Gentoo::Maps;
20
21 =head1 NAME
22
23 CPANPLUS::Dist::Gentoo - CPANPLUS backend generating Gentoo ebuilds.
24
25 =head1 VERSION
26
27 Version 0.08
28
29 =cut
30
31 our $VERSION = '0.08';
32
33 =head1 SYNOPSIS
34
35     cpan2dist --format=CPANPLUS::Dist::Gentoo \
36               --dist-opts overlay=/usr/local/portage \
37               --dist-opts distdir=/usr/portage/distfiles \
38               --dist-opts manifest=yes \
39               --dist-opts keywords=x86 \
40               --dist-opts header="# Copyright 1999-2008 Gentoo Foundation" \
41               --dist-opts footer="# End" \
42               Any::Module You::Like
43
44 =head1 DESCRPITON
45
46 This module is a CPANPLUS backend that recursively generates Gentoo ebuilds for a given package in the specified overlay (defaults to F</usr/local/portage>), updates the manifest, and even emerges it (together with its dependencies) if the user requires it.
47 You need write permissions on the directory where Gentoo fetches its source files (usually F</usr/portage/distfiles>).
48 The valid C<KEYWORDS> for the generated ebuilds are by default those given in C<ACCEPT_KEYWORDS>, but you can specify your own with the C<keywords> dist-option.
49
50 The generated ebuilds are placed into the C<perl-gcpanp> category.
51 They favour depending on a C<virtual>, on C<perl-core>, C<dev-perl> or C<perl-gcpan> (in that order) rather than C<perl-gcpanp>.
52
53 =head1 INSTALLATION
54
55 Before installing this module, you should append C<perl-gcpanp> to your F</etc/portage/categories> file.
56
57 You have two ways for installing this module :
58
59 =over 4
60
61 =item *
62
63 Use the perl overlay located at L<http://git.overlays.gentoo.org/gitweb/?p=proj/perl-overlay.git>.
64 It contains an ebuild for L<CPANPLUS::Dist::Gentoo>.
65
66 =item *
67
68 Bootstrap an ebuild for L<CPANPLUS::Dist::Gentoo> using itself.
69 Note that if your Gentoo system C<perl> is C<5.8.x>, L<CPANPLUS> and its dependencies are not installed and not even available in the main portage tree.
70 So you need to bootstrap them as well.
71
72 First, fetch tarballs for L<CPANPLUS> and L<CPANPLUS::Dist::Gentoo> :
73
74     $ cd /tmp
75     $ wget http://search.cpan.org/CPAN/authors/id/K/KA/KANE/CPANPLUS-0.88.tar.gz
76     $ wget http://search.cpan.org/CPAN/authors/id/V/VP/VPIT/CPANPLUS-Dist-Gentoo-0.08.tar.gz
77
78 Log in as root and unpack them in e.g. your home directory :
79
80     # cd
81     # tar xzf /tmp/CPANPLUS-0.88.tar.gz
82     # tar xzf /tmp/CPANPLUS-Dist-Gentoo-0.08.tar.gz
83
84 Set up environment variables so that the toolchain is temporarily available :
85
86     # export OLDPATH=$PATH
87     # export PATH=/root/CPANPLUS-Dist-Gentoo-0.08/bin:$PATH
88     # export PERL5LIB=/root/CPANPLUS-Dist-Gentoo-0.08/blib/lib:/root/CPANPLUS-0.88/lib:/root/CPANPLUS-0.88/inc/bundle
89
90 Make sure you don't have an old C<.cpanplus> configuration visible :
91
92     # [ -d /root/.cpanplus ] && mv /root/.cpanplus{,.bak}
93
94 Bootstrap L<CPANPLUS> :
95
96     # cd /root/CPANPLUS-Dist-Gentoo-0.08
97     # samples/g-cpanp CPANPLUS
98
99 Reset the environment :
100
101     # export PATH=$OLDPATH
102     # unset PERL5LIB OLDPATH
103
104 Emerge L<CPANPLUS> with the ebuilds you've just generated :
105
106     # emerge -tv CPANPLUS
107
108 As of september 2009, C<podlators> and C<ExtUtils-MakeMaker> may fail to emerge due to collisions.
109 You can work around this by disabling the C<protect-owned> C<FEATURE> for them :
110
111     # FEATURES="-protect-owned" emerge podlators
112     # FEATURES="-protect-owned" emerge ExtUtils-MakeMaker
113
114 You may need to run each of these commands two times for them to succeed.
115
116 At this point, you can bootstrap L<CPANPLUS::Dist::Gentoo> using the system L<CPANPLUS> :
117
118     # PERL5LIB=/root/CPANPLUS-Dist-Gentoo-0.08/blib/lib samples/g-cpanp CPANPLUS::Dist::Gentoo
119     # emerge -tv CPANPLUS-Dist-Gentoo
120
121 =back
122
123 =head1 METHODS
124
125 This module inherits all the methods from L<CPANPLUS::Dist::Base>.
126 Please refer to its documentation for precise information on what's done at each step.
127
128 =cut
129
130 use constant CATEGORY => 'perl-gcpanp';
131
132 my $overlays;
133 my $default_keywords;
134 my $default_distdir;
135 my $main_portdir;
136
137 my %forced;
138
139 my $unquote = sub {
140  my $s = shift;
141  $s =~ s/^["']*//;
142  $s =~ s/["']*$//;
143  return $s;
144 };
145
146 my $format_available;
147
148 sub format_available {
149  return $format_available if defined $format_available;
150
151  for my $prog (qw/emerge ebuild/) {
152   unless (can_run($prog)) {
153    __PACKAGE__->_abort("$prog is required to write ebuilds");
154    return $format_available = 0;
155   }
156  }
157
158  if (IPC::Cmd->can_capture_buffer) {
159   my $buffers;
160   my ($success, $errmsg) = run command => [ qw/emerge --info/ ],
161                                verbose => 0,
162                                buffer  => \$buffers;
163   if ($success) {
164    if ($buffers =~ /^PORTDIR_OVERLAY=(.*)$/m) {
165     $overlays = [ map abs_path($_), split ' ', $unquote->($1) ];
166    }
167    if ($buffers =~ /^ACCEPT_KEYWORDS=(.*)$/m) {
168     $default_keywords = [ split ' ', $unquote->($1) ];
169    }
170    if ($buffers =~ /^DISTDIR=(.*)$/m) {
171     $default_distdir = abs_path($unquote->($1));
172    }
173    if ($buffers =~ /^PORTDIR=(.*)$/m) {
174     $main_portdir = abs_path($unquote->($1));
175    }
176   } else {
177    __PACKAGE__->_abort($errmsg);
178   }
179  }
180
181  $default_keywords = [ 'x86' ] unless defined $default_keywords;
182  $default_distdir  = '/usr/portage/distfiles' unless defined $default_distdir;
183
184  return $format_available = 1;
185 }
186
187 sub init {
188  my ($self) = @_;
189  my $stat = $self->status;
190  my $conf = $self->parent->parent->configure_object;
191
192  $stat->mk_accessors(qw/name version author distribution desc uri src license
193                         fetched_arch requires
194                         ebuild_name ebuild_version ebuild_dir ebuild_file
195                         portdir_overlay
196                         overlay distdir keywords do_manifest header footer
197                         force verbose/);
198
199  $stat->force($conf->get_conf('force'));
200  $stat->verbose($conf->get_conf('verbose'));
201
202  return 1;
203 }
204
205 sub prepare {
206  my $self = shift;
207  my $mod  = $self->parent;
208  my $stat = $self->status;
209  my $int  = $mod->parent;
210  my $conf = $int->configure_object;
211
212  my %opts = @_;
213
214  my $OK   = sub { $stat->prepared(1); 1 };
215  my $FAIL = sub { $stat->prepared(0); $self->_abort(@_) if @_; 0 };
216  my $SKIP = sub { $stat->prepared(1); $stat->created(1); $self->_skip(@_) if @_; 1 };
217
218  my $keywords = delete $opts{keywords};
219  if (defined $keywords) {
220   $keywords = [ split ' ', $keywords ];
221  } else {
222   $keywords = $default_keywords;
223  }
224  $stat->keywords($keywords);
225
226  my $manifest = delete $opts{manifest};
227  $manifest = 1 unless defined $manifest;
228  $manifest = 0 if $manifest =~ /^\s*no?\s*$/i;
229  $stat->do_manifest($manifest);
230
231  my $header = delete $opts{header};
232  if (defined $header) {
233   1 while chomp $header;
234   $header .= "\n\n";
235  } else {
236   my $year = (localtime)[5] + 1900;
237   $header = <<"  DEF_HEADER";
238 # Copyright 1999-$year Gentoo Foundation
239 # Distributed under the terms of the GNU General Public License v2
240 # \$Header: \$
241   DEF_HEADER
242  }
243  $stat->header($header);
244
245  my $footer = delete $opts{footer};
246  if (defined $footer) {
247   $footer = "\n" . $footer;
248  } else {
249   $footer = '';
250  }
251  $stat->footer($footer);
252
253  my $overlay = delete $opts{overlay};
254  $overlay = (defined $overlay) ? abs_path $overlay : '/usr/local/portage';
255  $stat->overlay($overlay);
256
257  my $distdir = delete $opts{distdir};
258  $distdir = (defined $distdir) ? abs_path $distdir : $default_distdir;
259  $stat->distdir($distdir);
260
261  return $FAIL->("distdir isn't writable") if $stat->do_manifest && !-w $distdir;
262
263  $stat->fetched_arch($mod->status->fetch);
264
265  my $cur = File::Spec->curdir();
266  my $portdir_overlay;
267  for (@$overlays) {
268   if ($_ eq $overlay or File::Spec->abs2rel($overlay, $_) eq $cur) {
269    $portdir_overlay = [ @$overlays ];
270    last;
271   }
272  }
273  $portdir_overlay = [ @$overlays, $overlay ] unless $portdir_overlay;
274  $stat->portdir_overlay($portdir_overlay);
275
276  my $name = $mod->package_name;
277  $stat->name($name);
278
279  my $version = $mod->package_version;
280  $stat->version($version);
281
282  my $author = $mod->author->cpanid;
283  $stat->author($author);
284
285  $stat->distribution($name . '-' . $version);
286
287  $stat->ebuild_version(CPANPLUS::Dist::Gentoo::Maps::version_c2g($version));
288
289  $stat->ebuild_name(CPANPLUS::Dist::Gentoo::Maps::name_c2g($name));
290
291  $stat->ebuild_dir(File::Spec->catdir(
292   $stat->overlay,
293   CATEGORY,
294   $stat->ebuild_name,
295  ));
296
297  my $file = File::Spec->catfile(
298   $stat->ebuild_dir,
299   $stat->ebuild_name . '-' . $stat->ebuild_version . '.ebuild',
300  );
301  $stat->ebuild_file($file);
302
303  if ($stat->force) {
304   # Always generate an ebuild in our category when forcing
305   if ($forced{$file}) {
306    $stat->dist($file);
307    return $SKIP->('Ebuild already forced for', $stat->distribution);
308   }
309   ++$forced{$file};
310   if (-e $file) {
311    unless (-w $file) {
312     $stat->dist($file);
313     return $SKIP->("Can't force rewriting of $file");
314    }
315    1 while unlink $file;
316   }
317  } else {
318   if (my @match = $self->_cpan2portage($name, $version)) {
319    $stat->dist($match[1]);
320    return $SKIP->('Ebuild already generated for', $stat->distribution);
321   }
322  }
323
324  $stat->prepared(0);
325
326  $self->SUPER::prepare(%opts);
327
328  return $FAIL->() unless $stat->prepared;
329
330  my $desc = $mod->description;
331  ($desc = $name) =~ s/-+/::/g unless $desc;
332  $stat->desc($desc);
333
334  $stat->uri('http://search.cpan.org/dist/' . $name);
335
336  $author =~ /^(.)(.)/ or return $FAIL->('Wrong author name');
337  $stat->src("mirror://cpan/modules/by-authors/id/$1/$1$2/$author/" . $mod->package);
338
339  $stat->license($self->intuit_license);
340
341  my $prereqs = $mod->status->requires;
342  my @requires;
343  for my $prereq (sort keys %$prereqs) {
344   next if $prereq =~ /^perl(?:-|\z)/;
345   my $obj = $int->module_tree($prereq);
346   next unless $obj; # Not in the module tree (e.g. Config)
347   next if $obj->package_is_perl_core;
348   {
349    my $version;
350    if ($prereqs->{$prereq}) {
351     if ($obj->installed_version && $obj->installed_version < $obj->version) {
352      $version = $obj->installed_version;
353     } else {
354      $version = $obj->package_version;
355     }
356    }
357    push @requires, [ $obj->package_name, $version ];
358   }
359  }
360  $stat->requires(\@requires);
361
362  return $OK->();
363 }
364
365 =head2 C<intuit_license>
366
367 Returns an array reference to a list of Gentoo licences identifiers under which the current distribution is released.
368
369 =cut
370
371 my %dslip_license = (
372  p => 'perl',
373  g => 'gpl',
374  l => 'lgpl',
375  b => 'bsd',
376  a => 'artistic',
377  2 => 'artistic_2',
378 );
379
380 sub intuit_license {
381  my $self = shift;
382  my $mod  = $self->parent;
383
384  my $dslip = $mod->dslip;
385  if (defined $dslip and $dslip =~ /\S{4}(\S)/) {
386   my @licenses = CPANPLUS::Dist::Gentoo::Maps::license_c2g($dslip_license{$1});
387   return \@licenses if @licenses;
388  }
389
390  my $extract_dir = $mod->status->extract;
391
392  for my $meta_file (qw/META.json META.yml/) {
393   my $meta = eval {
394    Parse::CPAN::Meta::LoadFile(File::Spec->catdir(
395     $extract_dir,
396     $meta_file,
397    ));
398   } or next;
399   my $license = $meta->{license};
400   if (defined $license) {
401    my @licenses = CPANPLUS::Dist::Gentoo::Maps::license_c2g($license);
402    return \@licenses if @licenses;
403   }
404  }
405
406  return [ CPANPLUS::Dist::Gentoo::Maps::license_c2g('perl') ];
407 }
408
409 sub create {
410  my $self = shift;
411  my $stat = $self->status;
412
413  my $file;
414
415  my $OK   = sub {
416   $stat->created(1);
417   $stat->dist($file) if defined $file;
418   1;
419  };
420
421  my $FAIL = sub {
422   $stat->created(0);
423   $stat->dist(undef);
424   $self->_abort(@_) if @_;
425   if (defined $file and -f $file) {
426    1 while unlink $file;
427   }
428   0;
429  };
430
431  unless ($stat->prepared) {
432   return $FAIL->(
433    'Can\'t create', $stat->distribution, 'since it was never prepared'
434   );
435  }
436
437  if ($stat->created) {
438   $self->_skip($stat->distribution, 'was already created');
439   $file = $stat->dist; # Keep the existing one.
440   return $OK->();
441  }
442
443  my $dir = $stat->ebuild_dir;
444  unless (-d $dir) {
445   eval { File::Path::mkpath($dir) };
446   return $FAIL->("mkpath($dir): $@") if $@;
447  }
448
449  $file = $stat->ebuild_file;
450
451  # Create a placeholder ebuild to prevent recursion with circular dependencies.
452  {
453   open my $eb, '>', $file or return $FAIL->("open($file): $!");
454   print $eb "PLACEHOLDER\n";
455  }
456
457  $stat->created(0);
458  $stat->dist(undef);
459
460  $self->SUPER::create(@_);
461
462  return $FAIL->() unless $stat->created;
463
464  {
465   open my $eb, '>', $file or return $FAIL->("open($file): $!");
466   my $source = $self->ebuild_source;
467   return $FAIL->() unless defined $source;
468   print $eb $source;
469  }
470
471  return $FAIL->() if $stat->do_manifest and not $self->update_manifest;
472
473  return $OK->();
474 }
475
476 =head2 C<update_manifest>
477
478 Updates the F<Manifest> file for the ebuild associated to the current dist object.
479
480 =cut
481
482 sub update_manifest {
483  my $self = shift;
484  my $stat = $self->status;
485
486  my $file = $stat->ebuild_file;
487  unless (defined $file and -e $file) {
488   return $self->_abort('The ebuild file is invalid or does not exist');
489  }
490
491  unless (File::Copy::copy($stat->fetched_arch => $stat->distdir)) {
492   return $self->_abort("Couldn\'t copy the distribution file to distdir ($!)");
493  }
494
495  $self->_notify('Adding Manifest entry for', $stat->distribution);
496
497  return $self->_run([ 'ebuild', $file, 'manifest' ], 0);
498 }
499
500 =head2 C<ebuild_source>
501
502 Returns the source of the ebuild for the current dist object, or C<undef> when one of the dependencies couldn't be mapped to an existing ebuild.
503
504 =cut
505
506 sub ebuild_source {
507  my $self = shift;
508  my $stat = $self->status;
509
510  # We must resolve the deps now and not inside prepare because _cpan2portage
511  # has to see the ebuilds already generated for the dependencies of the current
512  # dist.
513  my @requires;
514  for (@{$stat->requires}) {
515   my $dep = $self->_cpan2portage(@$_);
516   unless (defined $dep) {
517    $self->_abort(
518     "Couldn't find an appropriate ebuild for $_->[0] in the portage tree"
519    );
520    return;
521   }
522   push @requires, $dep;
523  }
524
525  @requires = do { my %seen; sort grep !$seen{$_}++, 'dev-lang/perl',@requires };
526
527  my $d = $stat->header;
528  $d   .= "# Generated by CPANPLUS::Dist::Gentoo version $VERSION\n\n";
529  $d   .= 'MODULE_AUTHOR="' . $stat->author . "\"\ninherit perl-module\n\n";
530  $d   .= 'S="${WORKDIR}/' . $stat->distribution . "\"\n";
531  $d   .= 'DESCRIPTION="' . $stat->desc . "\"\n";
532  $d   .= 'HOMEPAGE="' . $stat->uri . "\"\n";
533  $d   .= 'SRC_URI="' . $stat->src . "\"\n";
534  $d   .= "SLOT=\"0\"\n";
535  $d   .= 'LICENSE="|| ( ' . join(' ', sort @{$stat->license}) . " )\"\n";
536  $d   .= 'KEYWORDS="' . join(' ', sort @{$stat->keywords}) . "\"\n";
537  $d   .= 'RDEPEND="' . join("\n", @requires) . "\"\n";
538  $d   .= "DEPEND=\"\${RDEPEND}\"\n";
539  $d   .= "SRC_TEST=\"do\"\n";
540  $d   .= $stat->footer;
541
542  return $d;
543 }
544
545 sub _cpan2portage {
546  my ($self, $name, $version) = @_;
547
548  $name = CPANPLUS::Dist::Gentoo::Maps::name_c2g($name);
549  my $ver;
550  $ver = CPANPLUS::Dist::Gentoo::Maps::version_c2g($version) if defined $version;
551
552  my @portdirs = ($main_portdir, @{$self->status->portdir_overlay});
553
554  for my $category (qw/virtual perl-core dev-perl perl-gcpan/, CATEGORY) {
555   my $atom = ($category eq 'virtual' ? 'perl-' : '') . $name;
556
557   for my $portdir (@portdirs) {
558    my @ebuilds = glob File::Spec->catfile(
559     $portdir,
560     $category,
561     $atom,
562     "$atom-*.ebuild",
563    ) or next;
564
565    my $last = reduce {
566     CPANPLUS::Dist::Gentoo::Maps::version_gcmp($b->[1], $a->[1]) >= 0 ? $b : $a
567    } map [ $_, /\Q$atom\E-v?([\d._pr-]+).*?\.ebuild$/ ? $1 : 0 ], @ebuilds;
568
569    my $dep;
570    if (defined $ver) { # implies that $version is defined
571     next unless
572               CPANPLUS::Dist::Gentoo::Maps::version_gcmp($last->[1], $ver) >= 0;
573     $dep = ">=$category/$atom-$ver";
574    } else {
575     $dep = "$category/$atom";
576    }
577
578    return wantarray ? ($dep, $last->[0]) : $dep;
579   }
580
581  }
582
583  return;
584 }
585
586 sub install {
587  my $self = shift;
588  my $stat = $self->status;
589  my $conf = $self->parent->parent->configure_object;
590
591  my $sudo = $conf->get_program('sudo');
592  my @cmd = ('emerge', '=' . $stat->ebuild_name . '-' . $stat->ebuild_version);
593  unshift @cmd, $sudo if $sudo;
594
595  my $success = $self->_run(\@cmd, 1);
596  $stat->installed($success);
597
598  return $success;
599 }
600
601 sub uninstall {
602  my $self = shift;
603  my $stat = $self->status;
604  my $conf = $self->parent->parent->configure_object;
605
606  my $sudo = $conf->get_program('sudo');
607  my @cmd = ('emerge', '-C', '=' . $stat->ebuild_name . '-' . $stat->ebuild_version);
608  unshift @cmd, $sudo if $sudo;
609
610  my $success = $self->_run(\@cmd, 1);
611  $stat->uninstalled($success);
612
613  return $success;
614 }
615
616 sub _run {
617  my ($self, $cmd, $verbose) = @_;
618  my $stat = $self->status;
619
620  my ($success, $errmsg, $output) = do {
621   local $ENV{PORTDIR_OVERLAY}     = join ' ', @{$stat->portdir_overlay};
622   local $ENV{PORTAGE_RO_DISTDIRS} = $stat->distdir;
623   run command => $cmd, verbose => $verbose;
624  };
625
626  unless ($success) {
627   $self->_abort($errmsg);
628   if (not $verbose and defined $output and $stat->verbose) {
629    my $msg = join '', @$output;
630    1 while chomp $msg;
631    CPANPLUS::Error::error($msg);
632   }
633  }
634
635  return $success;
636 }
637
638 sub _abort {
639  my $self = shift;
640
641  CPANPLUS::Error::error("@_ -- aborting");
642
643  return 0;
644 }
645
646 sub _notify {
647  my $self = shift;
648
649  CPANPLUS::Error::msg("@_");
650
651  return 1;
652 }
653
654 sub _skip { shift->_notify(@_, '-- skipping') }
655
656 =head1 DEPENDENCIES
657
658 Gentoo (L<http://gentoo.org>).
659
660 L<CPANPLUS>, L<IPC::Cmd> (core modules since 5.9.5), L<Parse::CPAN::Meta> (since 5.10.1).
661
662 L<Cwd>, L<Carp> (since perl 5), L<File::Path> (5.001), L<File::Copy> (5.002), L<File::Spec> (5.00405), L<List::Util> (5.007003).
663
664 =head1 SEE ALSO
665
666 L<cpan2dist>.
667
668 L<CPANPLUS::Dist::Base>, L<CPANPLUS::Dist::Deb>, L<CPANPLUS::Dist::Mdv>.
669
670 =head1 AUTHOR
671
672 Vincent Pit, C<< <perl at profvince.com> >>, L<http://www.profvince.com>.
673
674 You can contact me by mail or on C<irc.perl.org> (vincent).
675
676 =head1 BUGS
677
678 Please report any bugs or feature requests to C<bug-cpanplus-dist-gentoo at rt.cpan.org>, or through the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=CPANPLUS-Dist-Gentoo>.
679 I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
680
681 =head1 SUPPORT
682
683 You can find documentation for this module with the perldoc command.
684
685     perldoc CPANPLUS::Dist::Gentoo
686
687 =head1 ACKNOWLEDGEMENTS
688
689 The module was inspired by L<CPANPLUS::Dist::Deb> and L<CPANPLUS::Dist::Mdv>.
690
691 Kent Fredric, for testing and suggesting improvements.
692
693 =head1 COPYRIGHT & LICENSE
694
695 Copyright 2008-2009 Vincent Pit, all rights reserved.
696
697 This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
698
699 =cut
700
701 1; # End of CPANPLUS::Dist::Gentoo