]> git.vpit.fr Git - perl/modules/CPANPLUS-Dist-Gentoo.git/blob - lib/CPANPLUS/Dist/Gentoo.pm
Make 'ebuild' a normal accessor for C::D::G::Atom objects
[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-0.88/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 $atom = $self->_cpan2portage($name, $version)) {
320    $stat->dist($atom->ebuild);
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 $atom = $self->_cpan2portage(@$_);
517   unless (defined $atom) {
518    $self->_abort(
519     "Couldn't find an appropriate ebuild for $_->[0] in the portage tree"
520    );
521    return;
522   }
523   push @requires, $atom;
524  }
525
526  my $perl = CPANPLUS::Dist::Gentoo::Atom->new(
527   category => 'dev-lang',
528   name     => 'perl',
529  );
530  @requires = CPANPLUS::Dist::Gentoo::Atom->fold($perl, @requires);
531
532  my $d = $stat->header;
533  $d   .= "# Generated by CPANPLUS::Dist::Gentoo version $VERSION\n\n";
534  $d   .= 'MODULE_AUTHOR="' . $stat->author . "\"\ninherit perl-module\n\n";
535  $d   .= 'S="${WORKDIR}/' . $stat->distribution . "\"\n";
536  $d   .= 'DESCRIPTION="' . $stat->desc . "\"\n";
537  $d   .= 'HOMEPAGE="' . $stat->uri . "\"\n";
538  $d   .= 'SRC_URI="' . $stat->src . "\"\n";
539  $d   .= "SLOT=\"0\"\n";
540  $d   .= 'LICENSE="|| ( ' . join(' ', sort @{$stat->license}) . " )\"\n";
541  $d   .= 'KEYWORDS="' . join(' ', sort @{$stat->keywords}) . "\"\n";
542  $d   .= 'RDEPEND="' . join("\n", sort @requires) . "\"\n";
543  $d   .= "DEPEND=\"\${RDEPEND}\"\n";
544  $d   .= "SRC_TEST=\"do\"\n";
545  $d   .= $stat->footer;
546
547  return $d;
548 }
549
550 sub _cpan2portage {
551  my ($self, $name, $version) = @_;
552
553  $name    = CPANPLUS::Dist::Gentoo::Maps::name_c2g($name);
554  $version = CPANPLUS::Dist::Gentoo::Maps::version_c2g($version);
555
556  my @portdirs = ($main_portdir, @{$self->status->portdir_overlay});
557
558  for my $category (qw/virtual perl-core dev-perl perl-gcpan/, CATEGORY) {
559   my $name = ($category eq 'virtual' ? 'perl-' : '') . $name;
560
561   for my $portdir (@portdirs) {
562    my @ebuilds = glob File::Spec->catfile(
563     $portdir,
564     $category,
565     $name,
566     "$name-*.ebuild",
567    ) or next;
568
569    my $last = reduce { $a < $b ? $b : $a } # handles overloading
570                map CPANPLUS::Dist::Gentoo::Atom->new_from_ebuild($_),
571                 @ebuilds;
572    next if defined $version and $last < $version;
573
574    return CPANPLUS::Dist::Gentoo::Atom->new(
575     category => $last->category,
576     name     => $last->name,
577     (defined $version ? (version => $version, range => '>=') : ()),
578     ebuild   => $last->ebuild,
579    );
580   }
581
582  }
583
584  return;
585 }
586
587 sub install {
588  my $self = shift;
589  my $stat = $self->status;
590  my $conf = $self->parent->parent->configure_object;
591
592  my $sudo = $conf->get_program('sudo');
593  my @cmd = ('emerge', '=' . $stat->ebuild_name . '-' . $stat->ebuild_version);
594  unshift @cmd, $sudo if $sudo;
595
596  my $success = $self->_run(\@cmd, 1);
597  $stat->installed($success);
598
599  return $success;
600 }
601
602 sub uninstall {
603  my $self = shift;
604  my $stat = $self->status;
605  my $conf = $self->parent->parent->configure_object;
606
607  my $sudo = $conf->get_program('sudo');
608  my @cmd = ('emerge', '-C', '=' . $stat->ebuild_name . '-' . $stat->ebuild_version);
609  unshift @cmd, $sudo if $sudo;
610
611  my $success = $self->_run(\@cmd, 1);
612  $stat->uninstalled($success);
613
614  return $success;
615 }
616
617 sub _run {
618  my ($self, $cmd, $verbose) = @_;
619  my $stat = $self->status;
620
621  my ($success, $errmsg, $output) = do {
622   local $ENV{PORTDIR_OVERLAY}     = join ' ', @{$stat->portdir_overlay};
623   local $ENV{PORTAGE_RO_DISTDIRS} = $stat->distdir;
624   run command => $cmd, verbose => $verbose;
625  };
626
627  unless ($success) {
628   $self->_abort($errmsg);
629   if (not $verbose and defined $output and $stat->verbose) {
630    my $msg = join '', @$output;
631    1 while chomp $msg;
632    CPANPLUS::Error::error($msg);
633   }
634  }
635
636  return $success;
637 }
638
639 sub _abort {
640  my $self = shift;
641
642  CPANPLUS::Error::error("@_ -- aborting");
643
644  return 0;
645 }
646
647 sub _notify {
648  my $self = shift;
649
650  CPANPLUS::Error::msg("@_");
651
652  return 1;
653 }
654
655 sub _skip { shift->_notify(@_, '-- skipping') }
656
657 =head1 DEPENDENCIES
658
659 Gentoo (L<http://gentoo.org>).
660
661 L<CPANPLUS>, L<IPC::Cmd> (core modules since 5.9.5), L<Parse::CPAN::Meta> (since 5.10.1).
662
663 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).
664
665 =head1 SEE ALSO
666
667 L<cpan2dist>.
668
669 L<CPANPLUS::Dist::Base>, L<CPANPLUS::Dist::Deb>, L<CPANPLUS::Dist::Mdv>.
670
671 =head1 AUTHOR
672
673 Vincent Pit, C<< <perl at profvince.com> >>, L<http://www.profvince.com>.
674
675 You can contact me by mail or on C<irc.perl.org> (vincent).
676
677 =head1 BUGS
678
679 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>.
680 I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
681
682 =head1 SUPPORT
683
684 You can find documentation for this module with the perldoc command.
685
686     perldoc CPANPLUS::Dist::Gentoo
687
688 =head1 ACKNOWLEDGEMENTS
689
690 The module was inspired by L<CPANPLUS::Dist::Deb> and L<CPANPLUS::Dist::Mdv>.
691
692 Kent Fredric, for testing and suggesting improvements.
693
694 =head1 COPYRIGHT & LICENSE
695
696 Copyright 2008-2009 Vincent Pit, all rights reserved.
697
698 This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
699
700 =cut
701
702 1; # End of CPANPLUS::Dist::Gentoo