]> git.vpit.fr Git - perl/modules/CPANPLUS-Dist-Gentoo.git/blob - lib/CPANPLUS/Dist/Gentoo.pm
Introduce C::D::G::{Atom,Version}
[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::Atom;
20 use CPANPLUS::Dist::Gentoo::Maps;
21
22 =head1 NAME
23
24 CPANPLUS::Dist::Gentoo - CPANPLUS backend generating Gentoo ebuilds.
25
26 =head1 VERSION
27
28 Version 0.08
29
30 =cut
31
32 our $VERSION = '0.08';
33
34 =head1 SYNOPSIS
35
36     cpan2dist --format=CPANPLUS::Dist::Gentoo \
37               --dist-opts overlay=/usr/local/portage \
38               --dist-opts distdir=/usr/portage/distfiles \
39               --dist-opts manifest=yes \
40               --dist-opts keywords=x86 \
41               --dist-opts header="# Copyright 1999-2008 Gentoo Foundation" \
42               --dist-opts footer="# End" \
43               Any::Module You::Like
44
45 =head1 DESCRPITON
46
47 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.
48 You need write permissions on the directory where Gentoo fetches its source files (usually F</usr/portage/distfiles>).
49 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.
50
51 The generated ebuilds are placed into the C<perl-gcpanp> category.
52 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>.
53
54 =head1 INSTALLATION
55
56 Before installing this module, you should append C<perl-gcpanp> to your F</etc/portage/categories> file.
57
58 You have two ways for installing this module :
59
60 =over 4
61
62 =item *
63
64 Use the perl overlay located at L<http://git.overlays.gentoo.org/gitweb/?p=proj/perl-overlay.git>.
65 It contains an ebuild for L<CPANPLUS::Dist::Gentoo>.
66
67 =item *
68
69 Bootstrap an ebuild for L<CPANPLUS::Dist::Gentoo> using itself.
70 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.
71 So you need to bootstrap them as well.
72
73 First, fetch tarballs for L<CPANPLUS> and L<CPANPLUS::Dist::Gentoo> :
74
75     $ cd /tmp
76     $ wget http://search.cpan.org/CPAN/authors/id/K/KA/KANE/CPANPLUS-0.88.tar.gz
77     $ wget http://search.cpan.org/CPAN/authors/id/V/VP/VPIT/CPANPLUS-Dist-Gentoo-0.08.tar.gz
78
79 Log in as root and unpack them in e.g. your home directory :
80
81     # cd
82     # tar xzf /tmp/CPANPLUS-0.88.tar.gz
83     # tar xzf /tmp/CPANPLUS-Dist-Gentoo-0.08.tar.gz
84
85 Set up environment variables so that the toolchain is temporarily available :
86
87     # export OLDPATH=$PATH
88     # export PATH=/root/CPANPLUS-Dist-Gentoo-0.08/bin:$PATH
89     # export PERL5LIB=/root/CPANPLUS-Dist-Gentoo-0.08/blib/lib:/root/CPANPLUS-0.88/lib:/root/CPANPLUS-0.88/inc/bundle
90
91 Make sure you don't have an old C<.cpanplus> configuration visible :
92
93     # [ -d /root/.cpanplus ] && mv /root/.cpanplus{,.bak}
94
95 Bootstrap L<CPANPLUS> :
96
97     # cd /root/CPANPLUS-Dist-Gentoo-0.08
98     # samples/g-cpanp CPANPLUS
99
100 Reset the environment :
101
102     # export PATH=$OLDPATH
103     # unset PERL5LIB OLDPATH
104
105 Emerge L<CPANPLUS> with the ebuilds you've just generated :
106
107     # emerge -tv CPANPLUS
108
109 As of september 2009, C<podlators> and C<ExtUtils-MakeMaker> may fail to emerge due to collisions.
110 You can work around this by disabling the C<protect-owned> C<FEATURE> for them :
111
112     # FEATURES="-protect-owned" emerge podlators
113     # FEATURES="-protect-owned" emerge ExtUtils-MakeMaker
114
115 You may need to run each of these commands two times for them to succeed.
116
117 At this point, you can bootstrap L<CPANPLUS::Dist::Gentoo> using the system L<CPANPLUS> :
118
119     # PERL5LIB=/root/CPANPLUS-Dist-Gentoo-0.08/blib/lib samples/g-cpanp CPANPLUS::Dist::Gentoo
120     # emerge -tv CPANPLUS-Dist-Gentoo
121
122 =back
123
124 =head1 METHODS
125
126 This module inherits all the methods from L<CPANPLUS::Dist::Base>.
127 Please refer to its documentation for precise information on what's done at each step.
128
129 =cut
130
131 use constant CATEGORY => 'perl-gcpanp';
132
133 my $overlays;
134 my $default_keywords;
135 my $default_distdir;
136 my $main_portdir;
137
138 my %forced;
139
140 my $unquote = sub {
141  my $s = shift;
142  $s =~ s/^["']*//;
143  $s =~ s/["']*$//;
144  return $s;
145 };
146
147 my $format_available;
148
149 sub format_available {
150  return $format_available if defined $format_available;
151
152  for my $prog (qw/emerge ebuild/) {
153   unless (can_run($prog)) {
154    __PACKAGE__->_abort("$prog is required to write ebuilds");
155    return $format_available = 0;
156   }
157  }
158
159  if (IPC::Cmd->can_capture_buffer) {
160   my $buffers;
161   my ($success, $errmsg) = run command => [ qw/emerge --info/ ],
162                                verbose => 0,
163                                buffer  => \$buffers;
164   if ($success) {
165    if ($buffers =~ /^PORTDIR_OVERLAY=(.*)$/m) {
166     $overlays = [ map abs_path($_), split ' ', $unquote->($1) ];
167    }
168    if ($buffers =~ /^ACCEPT_KEYWORDS=(.*)$/m) {
169     $default_keywords = [ split ' ', $unquote->($1) ];
170    }
171    if ($buffers =~ /^DISTDIR=(.*)$/m) {
172     $default_distdir = abs_path($unquote->($1));
173    }
174    if ($buffers =~ /^PORTDIR=(.*)$/m) {
175     $main_portdir = abs_path($unquote->($1));
176    }
177   } else {
178    __PACKAGE__->_abort($errmsg);
179   }
180  }
181
182  $default_keywords = [ 'x86' ] unless defined $default_keywords;
183  $default_distdir  = '/usr/portage/distfiles' unless defined $default_distdir;
184
185  return $format_available = 1;
186 }
187
188 sub init {
189  my ($self) = @_;
190  my $stat = $self->status;
191  my $conf = $self->parent->parent->configure_object;
192
193  $stat->mk_accessors(qw/name version author distribution desc uri src license
194                         fetched_arch requires
195                         ebuild_name ebuild_version ebuild_dir ebuild_file
196                         portdir_overlay
197                         overlay distdir keywords do_manifest header footer
198                         force verbose/);
199
200  $stat->force($conf->get_conf('force'));
201  $stat->verbose($conf->get_conf('verbose'));
202
203  return 1;
204 }
205
206 sub prepare {
207  my $self = shift;
208  my $mod  = $self->parent;
209  my $stat = $self->status;
210  my $int  = $mod->parent;
211  my $conf = $int->configure_object;
212
213  my %opts = @_;
214
215  my $OK   = sub { $stat->prepared(1); 1 };
216  my $FAIL = sub { $stat->prepared(0); $self->_abort(@_) if @_; 0 };
217  my $SKIP = sub { $stat->prepared(1); $stat->created(1); $self->_skip(@_) if @_; 1 };
218
219  my $keywords = delete $opts{keywords};
220  if (defined $keywords) {
221   $keywords = [ split ' ', $keywords ];
222  } else {
223   $keywords = $default_keywords;
224  }
225  $stat->keywords($keywords);
226
227  my $manifest = delete $opts{manifest};
228  $manifest = 1 unless defined $manifest;
229  $manifest = 0 if $manifest =~ /^\s*no?\s*$/i;
230  $stat->do_manifest($manifest);
231
232  my $header = delete $opts{header};
233  if (defined $header) {
234   1 while chomp $header;
235   $header .= "\n\n";
236  } else {
237   my $year = (localtime)[5] + 1900;
238   $header = <<"  DEF_HEADER";
239 # Copyright 1999-$year Gentoo Foundation
240 # Distributed under the terms of the GNU General Public License v2
241 # \$Header: \$
242   DEF_HEADER
243  }
244  $stat->header($header);
245
246  my $footer = delete $opts{footer};
247  if (defined $footer) {
248   $footer = "\n" . $footer;
249  } else {
250   $footer = '';
251  }
252  $stat->footer($footer);
253
254  my $overlay = delete $opts{overlay};
255  $overlay = (defined $overlay) ? abs_path $overlay : '/usr/local/portage';
256  $stat->overlay($overlay);
257
258  my $distdir = delete $opts{distdir};
259  $distdir = (defined $distdir) ? abs_path $distdir : $default_distdir;
260  $stat->distdir($distdir);
261
262  return $FAIL->("distdir isn't writable") if $stat->do_manifest && !-w $distdir;
263
264  $stat->fetched_arch($mod->status->fetch);
265
266  my $cur = File::Spec->curdir();
267  my $portdir_overlay;
268  for (@$overlays) {
269   if ($_ eq $overlay or File::Spec->abs2rel($overlay, $_) eq $cur) {
270    $portdir_overlay = [ @$overlays ];
271    last;
272   }
273  }
274  $portdir_overlay = [ @$overlays, $overlay ] unless $portdir_overlay;
275  $stat->portdir_overlay($portdir_overlay);
276
277  my $name = $mod->package_name;
278  $stat->name($name);
279
280  my $version = $mod->package_version;
281  $stat->version($version);
282
283  my $author = $mod->author->cpanid;
284  $stat->author($author);
285
286  $stat->distribution($name . '-' . $version);
287
288  $stat->ebuild_version(CPANPLUS::Dist::Gentoo::Maps::version_c2g($version));
289
290  $stat->ebuild_name(CPANPLUS::Dist::Gentoo::Maps::name_c2g($name));
291
292  $stat->ebuild_dir(File::Spec->catdir(
293   $stat->overlay,
294   CATEGORY,
295   $stat->ebuild_name,
296  ));
297
298  my $file = File::Spec->catfile(
299   $stat->ebuild_dir,
300   $stat->ebuild_name . '-' . $stat->ebuild_version . '.ebuild',
301  );
302  $stat->ebuild_file($file);
303
304  if ($stat->force) {
305   # Always generate an ebuild in our category when forcing
306   if ($forced{$file}) {
307    $stat->dist($file);
308    return $SKIP->('Ebuild already forced for', $stat->distribution);
309   }
310   ++$forced{$file};
311   if (-e $file) {
312    unless (-w $file) {
313     $stat->dist($file);
314     return $SKIP->("Can't force rewriting of $file");
315    }
316    1 while unlink $file;
317   }
318  } else {
319   if (my @match = $self->_cpan2portage($name, $version)) {
320    $stat->dist($match[1]);
321    return $SKIP->('Ebuild already generated for', $stat->distribution);
322   }
323  }
324
325  $stat->prepared(0);
326
327  $self->SUPER::prepare(%opts);
328
329  return $FAIL->() unless $stat->prepared;
330
331  my $desc = $mod->description;
332  ($desc = $name) =~ s/-+/::/g unless $desc;
333  $stat->desc($desc);
334
335  $stat->uri('http://search.cpan.org/dist/' . $name);
336
337  $author =~ /^(.)(.)/ or return $FAIL->('Wrong author name');
338  $stat->src("mirror://cpan/modules/by-authors/id/$1/$1$2/$author/" . $mod->package);
339
340  $stat->license($self->intuit_license);
341
342  my $prereqs = $mod->status->requires;
343  my @requires;
344  for my $prereq (sort keys %$prereqs) {
345   next if $prereq =~ /^perl(?:-|\z)/;
346   my $obj = $int->module_tree($prereq);
347   next unless $obj; # Not in the module tree (e.g. Config)
348   next if $obj->package_is_perl_core;
349   {
350    my $version;
351    if ($prereqs->{$prereq}) {
352     if ($obj->installed_version && $obj->installed_version < $obj->version) {
353      $version = $obj->installed_version;
354     } else {
355      $version = $obj->package_version;
356     }
357    }
358    push @requires, [ $obj->package_name, $version ];
359   }
360  }
361  $stat->requires(\@requires);
362
363  return $OK->();
364 }
365
366 =head2 C<intuit_license>
367
368 Returns an array reference to a list of Gentoo licences identifiers under which the current distribution is released.
369
370 =cut
371
372 my %dslip_license = (
373  p => 'perl',
374  g => 'gpl',
375  l => 'lgpl',
376  b => 'bsd',
377  a => 'artistic',
378  2 => 'artistic_2',
379 );
380
381 sub intuit_license {
382  my $self = shift;
383  my $mod  = $self->parent;
384
385  my $dslip = $mod->dslip;
386  if (defined $dslip and $dslip =~ /\S{4}(\S)/) {
387   my @licenses = CPANPLUS::Dist::Gentoo::Maps::license_c2g($dslip_license{$1});
388   return \@licenses if @licenses;
389  }
390
391  my $extract_dir = $mod->status->extract;
392
393  for my $meta_file (qw/META.json META.yml/) {
394   my $meta = eval {
395    Parse::CPAN::Meta::LoadFile(File::Spec->catdir(
396     $extract_dir,
397     $meta_file,
398    ));
399   } or next;
400   my $license = $meta->{license};
401   if (defined $license) {
402    my @licenses = CPANPLUS::Dist::Gentoo::Maps::license_c2g($license);
403    return \@licenses if @licenses;
404   }
405  }
406
407  return [ CPANPLUS::Dist::Gentoo::Maps::license_c2g('perl') ];
408 }
409
410 sub create {
411  my $self = shift;
412  my $stat = $self->status;
413
414  my $file;
415
416  my $OK   = sub {
417   $stat->created(1);
418   $stat->dist($file) if defined $file;
419   1;
420  };
421
422  my $FAIL = sub {
423   $stat->created(0);
424   $stat->dist(undef);
425   $self->_abort(@_) if @_;
426   if (defined $file and -f $file) {
427    1 while unlink $file;
428   }
429   0;
430  };
431
432  unless ($stat->prepared) {
433   return $FAIL->(
434    'Can\'t create', $stat->distribution, 'since it was never prepared'
435   );
436  }
437
438  if ($stat->created) {
439   $self->_skip($stat->distribution, 'was already created');
440   $file = $stat->dist; # Keep the existing one.
441   return $OK->();
442  }
443
444  my $dir = $stat->ebuild_dir;
445  unless (-d $dir) {
446   eval { File::Path::mkpath($dir) };
447   return $FAIL->("mkpath($dir): $@") if $@;
448  }
449
450  $file = $stat->ebuild_file;
451
452  # Create a placeholder ebuild to prevent recursion with circular dependencies.
453  {
454   open my $eb, '>', $file or return $FAIL->("open($file): $!");
455   print $eb "PLACEHOLDER\n";
456  }
457
458  $stat->created(0);
459  $stat->dist(undef);
460
461  $self->SUPER::create(@_);
462
463  return $FAIL->() unless $stat->created;
464
465  {
466   open my $eb, '>', $file or return $FAIL->("open($file): $!");
467   my $source = $self->ebuild_source;
468   return $FAIL->() unless defined $source;
469   print $eb $source;
470  }
471
472  return $FAIL->() if $stat->do_manifest and not $self->update_manifest;
473
474  return $OK->();
475 }
476
477 =head2 C<update_manifest>
478
479 Updates the F<Manifest> file for the ebuild associated to the current dist object.
480
481 =cut
482
483 sub update_manifest {
484  my $self = shift;
485  my $stat = $self->status;
486
487  my $file = $stat->ebuild_file;
488  unless (defined $file and -e $file) {
489   return $self->_abort('The ebuild file is invalid or does not exist');
490  }
491
492  unless (File::Copy::copy($stat->fetched_arch => $stat->distdir)) {
493   return $self->_abort("Couldn\'t copy the distribution file to distdir ($!)");
494  }
495
496  $self->_notify('Adding Manifest entry for', $stat->distribution);
497
498  return $self->_run([ 'ebuild', $file, 'manifest' ], 0);
499 }
500
501 =head2 C<ebuild_source>
502
503 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.
504
505 =cut
506
507 sub ebuild_source {
508  my $self = shift;
509  my $stat = $self->status;
510
511  # We must resolve the deps now and not inside prepare because _cpan2portage
512  # has to see the ebuilds already generated for the dependencies of the current
513  # dist.
514  my @requires;
515  for (@{$stat->requires}) {
516   my $dep = $self->_cpan2portage(@$_);
517   unless (defined $dep) {
518    $self->_abort(
519     "Couldn't find an appropriate ebuild for $_->[0] in the portage tree"
520    );
521    return;
522   }
523   push @requires, $dep;
524  }
525
526  @requires = do { my %seen; sort grep !$seen{$_}++, 'dev-lang/perl',@requires };
527
528  my $d = $stat->header;
529  $d   .= "# Generated by CPANPLUS::Dist::Gentoo version $VERSION\n\n";
530  $d   .= 'MODULE_AUTHOR="' . $stat->author . "\"\ninherit perl-module\n\n";
531  $d   .= 'S="${WORKDIR}/' . $stat->distribution . "\"\n";
532  $d   .= 'DESCRIPTION="' . $stat->desc . "\"\n";
533  $d   .= 'HOMEPAGE="' . $stat->uri . "\"\n";
534  $d   .= 'SRC_URI="' . $stat->src . "\"\n";
535  $d   .= "SLOT=\"0\"\n";
536  $d   .= 'LICENSE="|| ( ' . join(' ', sort @{$stat->license}) . " )\"\n";
537  $d   .= 'KEYWORDS="' . join(' ', sort @{$stat->keywords}) . "\"\n";
538  $d   .= 'RDEPEND="' . join("\n", @requires) . "\"\n";
539  $d   .= "DEPEND=\"\${RDEPEND}\"\n";
540  $d   .= "SRC_TEST=\"do\"\n";
541  $d   .= $stat->footer;
542
543  return $d;
544 }
545
546 sub _cpan2portage {
547  my ($self, $name, $version) = @_;
548
549  $name = CPANPLUS::Dist::Gentoo::Maps::name_c2g($name);
550  my $ver;
551  $ver = CPANPLUS::Dist::Gentoo::Maps::version_c2g($version) if defined $version;
552
553  my @portdirs = ($main_portdir, @{$self->status->portdir_overlay});
554
555  for my $category (qw/virtual perl-core dev-perl perl-gcpan/, CATEGORY) {
556   my $atom = ($category eq 'virtual' ? 'perl-' : '') . $name;
557
558   for my $portdir (@portdirs) {
559    my @ebuilds = glob File::Spec->catfile(
560     $portdir,
561     $category,
562     $atom,
563     "$atom-*.ebuild",
564    ) or next;
565
566    my $last = reduce { $a->[1] < $b->[1] ? $b : $a }
567                map [ $_, CPANPLUS::Dist::Gentoo::Atom->new(ebuild => $_) ],
568                 @ebuilds;
569
570    my $dep;
571    if (defined $ver) { # implies that $version is defined
572     next if $last < $ver;
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