File Coverage

blib/lib/Youri/Package/RPM/Updater.pm
Criterion Covered Total %
statement 34 36 94.4
branch n/a
condition n/a
subroutine 12 12 100.0
pod n/a
total 46 48 95.8


line stmt bran cond sub pod time code
1             # $Id: Updater.pm 2301 2011-01-22 12:10:08Z guillomovitch $
2              
3             package Youri::Package::RPM::Updater;
4              
5             =head1 NAME
6              
7             Youri::Package::RPM::Updater - Update RPM packages
8              
9             =head1 SYNOPSIS
10              
11             my $updater = Youri::Package::RPM::Updater->new();
12             $updater->update_from_source('foo-1.0-1.src.rpm', '2.0');
13             $updater->update_from_spec('foo.spec', '2.0');
14             $updater->update_from_repository('foo', '2.0');
15              
16             =head1 DESCRIPTION
17              
18             This module updates rpm packages. When given an explicit new version, it
19             updates the spec file, and downloads new sources automatically. When not given
20             a new version, it just updates the spec file.
21              
22             Warning, not every spec file syntax is supported. If you use specific syntax,
23             you'll have to ressort to additional processing with explicit perl expression
24             to evaluate for each line of the spec file.
25              
26             Here is version update algorithm (only used when building a new version):
27              
28             =over
29              
30             =item * find the first definition of version
31              
32             =item * replace it with new value
33              
34             =back
35              
36             Here is release update algorithm:
37              
38             =over
39              
40             =item * find the first definition of release
41              
42             =item * if explicit B parameter given:
43              
44             =over
45              
46             =item * replace value
47              
48             =back
49              
50             =item * otherwise:
51              
52             =over
53              
54             =item * extract any macro occuring in the leftmost part (such as %mkrel)
55              
56             =item * extract any occurence of B option in the rightmost part
57              
58             =item * if a new version is given:
59              
60             =over
61              
62             =item * replace with 1
63              
64             =back
65              
66             =item * otherwise:
67              
68             =over
69              
70             =item * increment by 1
71              
72             =back
73              
74             =back
75              
76             =back
77              
78             In both cases, both direct definition:
79              
80             Version: X
81              
82             or indirect definition:
83              
84             %define version X
85             Version: %{version}
86              
87             are supported. Any more complex one is not.
88              
89             =head1 CONFIGURATION
90              
91             The following YAML-format configuration files are used:
92              
93             =over
94              
95             =item the system configuration file is F
96              
97             =item the user configuration file is F<$HOME/.youri/updater.conf>
98              
99             =back
100              
101             Allowed directives are the same as new method options.
102              
103             =head1 AUTHORS
104              
105             Julien Danjou
106              
107             Michael Scherer
108              
109             Guillaume Rousse
110              
111             =head1 COPYRIGHT AND LICENSE
112              
113             Copyright (c) 2003-2007 Mandriva.
114              
115             Permission to use, copy, modify, and distribute this software and its
116             documentation under the terms of the GNU General Public License is hereby
117             granted. No representations are made about the suitability of this software
118             for any purpose. It is provided "as is" without express or implied warranty.
119             See the GNU General Public License for more details.
120              
121             =cut
122              
123 2     2   51313 use strict;
  2         5  
  2         74  
124 2     2   11 use Cwd;
  2         3  
  2         146  
125 2     2   10 use Carp;
  2         3  
  2         158  
126 2     2   4593 use DateTime;
  2         412730  
  2         79  
127 2     2   26 use File::Basename;
  2         4  
  2         229  
128 2     2   2161 use File::Copy;
  2         10392  
  2         133  
129 2     2   17 use File::Spec;
  2         5  
  2         36  
130 2     2   11 use File::Path;
  2         5  
  2         117  
131 2     2   2680 use File::Temp qw/tempdir/;
  2         48178  
  2         157  
132 2     2   48 use List::MoreUtils qw/none/;
  2         5  
  2         185  
133 2     2   15173 use LWP::UserAgent;
  2         145428  
  2         69  
134 2     2   3895 use SVN::Client;
  0            
  0            
135             use Readonly;
136             use YAML::AppConfig;
137             use Youri::Package::RPM 0.002;
138             use version; our $VERSION = qv('0.6.0');
139             use feature qw/switch/;
140              
141             # default values
142             Readonly::Scalar my $defaults => <<'EOF';
143             ---
144             srpm_dirs:
145              
146             timeout: 10
147              
148             agent: youri-package-updater/VERSION
149              
150             url_rewrite_rules:
151             -
152             from: http://(.*)\.(?:sourceforge|sf)\.net/?(.*)
153             to: http://prdownloads.sourceforge.net/$1/$2
154             -
155             from: https?://gna.org/projects/([^/]*)/(.*)'
156             to: http://download.gna.org/$1/$2
157             -
158             from: http://(.*)\.berlios.de/(.*)
159             to: http://download.berlios.de/$1/$2
160             -
161             from: https?://savannah.nongnu.org/projects/([^/]*)/(.*)
162             to: http://savannah.nongnu.org/download/$1/$2
163             -
164             from: https?://savannah.gnu.org/projects/([^/]*)/(.*)
165             to: http://savannah.gnu.org/download/$1/$2
166             -
167             from: http://search.cpan.org/dist/([^-]+)-.*
168             to: http://www.cpan.org/modules/by-module/$1/
169              
170             archive_content_types:
171             tar:
172             - application/x-tar
173             gz:
174             - application/x-tar
175             - application/x-gz
176             - application/x-gzip
177             tgz:
178             - application/x-tar
179             - application/x-gz
180             - application/x-gzip
181             bz2:
182             - application/x-tar
183             - application/x-bz2
184             - application/x-bzip
185             - application/x-bzip2
186             tbz2:
187             - application/x-tar
188             - application/x-bz2
189             - application/x-bzip
190             - application/x-bzip2
191             zip:
192             - application/x-gzip
193             lzma:
194             - application/x-tar
195             - application/x-lzma
196             _all:
197             - application/x-download
198             - application/octet-stream
199             - application/empty
200              
201             alternate_extensions:
202             - tar.gz
203             - tgz
204             - zip
205              
206             sourceforge_mirrors:
207             - ovh
208             - mesh
209             - switch
210              
211             new_version_message: New version %%VERSION
212              
213             new_release_message: Rebuild
214             EOF
215              
216             my $wrapper_class = Youri::Package::RPM->get_wrapper_class();
217              
218             =head1 CLASS METHODS
219              
220             =head2 new(%options)
221              
222             Creates and returns a new MDV::RPM::Updater object.
223              
224             Available options:
225              
226             =over
227              
228             =item verbose $level
229              
230             verbosity level (default: 0).
231              
232             =item check_new_version
233              
234             check new version is really new before updating spec file (default: true).
235              
236             =item topdir $topdir
237              
238             rpm top-level directory (default: rpm %_topdir macro).
239              
240             =item sourcedir $sourcedir
241              
242             rpm source directory (default: rpm %_sourcedir macro).
243              
244             =item release_suffix $suffix
245              
246             suffix appended to numerical value in release tag. (default: none).
247              
248             =item srpm_dirs $dirs
249              
250             list of directories containing source packages (default: empty).
251              
252             =item timeout $timeout
253              
254             timeout for file downloads (default: 10)
255              
256             =item agent $agent
257              
258             user agent for file downloads (default: youri-package-updater/$VERSION)
259              
260             =item alternate_extensions $extensions
261              
262             alternate extensions to try when downloading source fails (default: tar.gz,
263             tgz, zip)
264              
265             =item sourceforge_mirrors $mirrors
266              
267             mirrors to try when downloading files hosted on sourceforge (default: ovh,
268             mesh, switch)
269              
270             =item url_rewrite_rules $rules
271              
272             list of rewrite rules to apply on source tag value for computing source URL
273             when the source is a local file, as hashes of two regexeps
274              
275             =item archive_content_types $types
276              
277             hash of lists of accepted content types when downloading archive files, indexed
278             by archive extension
279              
280             =item new_version_message
281              
282             changelog message for new version (default: New version %%VERSION)
283              
284             =item new_release_message
285              
286             changelog message for new release (default: Rebuild)
287              
288             =back
289              
290             =cut
291              
292             sub new {
293             my ($class, %options) = @_;
294              
295             # force internal rpmlib configuration
296             my ($topdir, $sourcedir);
297             if ($options{topdir}) {
298             $topdir = File::Spec->rel2abs($options{topdir});
299             $wrapper_class->add_macro("_topdir $topdir");
300             } else {
301             $topdir = $wrapper_class->expand_macro('%_topdir');
302             }
303             if ($options{sourcedir}) {
304             $sourcedir = File::Spec->rel2abs($options{sourcedir});
305             $wrapper_class->add_macro("_sourcedir $sourcedir");
306             } else {
307             $sourcedir = $wrapper_class->expand_macro('%_sourcedir');
308             }
309              
310             my $config = YAML::AppConfig->new(string => $defaults);
311             $config->merge(file => '/etc/youri/updater.conf')
312             if -r '/etc/youri/updater.conf';
313             $config->merge(file => "$ENV{HOME}/.youri/updater.conf")
314             if -r "$ENV{HOME}/.youri/updater.conf";
315            
316             my $self = bless {
317             _topdir => $topdir,
318             _sourcedir => $sourcedir,
319             _verbose => $options{verbose} // 0,
320             _check_new_version => $options{check_new_version} // 1,
321             _release_suffix => $options{release_suffix} // undef,
322             _timeout => $options{timeout} //
323             $config->get('timeout'),
324             _agent => $options{agent} //
325             $config->get('agent'),
326             _srpm_dirs => $options{srpm_dirs} //
327             $config->get('srpm_dirs'),
328             _alternate_extensions => $options{alternate_extensions} //
329             $config->get('alternate_extensions'),
330             _sourceforge_mirrors => $options{sourceforge_mirrors} //
331             $config->get('sourceforge_mirrors'),
332             _new_version_message => $options{new_version_message} //
333             $config->get('new_version_message'),
334             _new_release_message => $options{new_release_message} //
335             $config->get('new_release_message'),
336             _url_rewrite_rules => $options{url_rewrite_rules} //
337             $config->get('url_rewrite_rules'),
338             _archive_content_types => $options{archive_content_types} //
339             $config->get('archive_content_types'),
340             }, $class;
341              
342             $self->{_agent} =~ s/VERSION/$VERSION/;
343              
344             return $self;
345             }
346              
347             =head1 INSTANCE METHODS
348              
349             =head2 update_from_repository($name, $version, %options)
350              
351             Update package with name $name to version $version.
352              
353             Available options:
354              
355             =over
356              
357             =item release => $release
358              
359             Force package release, instead of computing it.
360              
361             =item download true/false
362              
363             download new sources (default: true).
364              
365             =item update_revision true/false
366              
367             update spec file revision (release/history) (default: true).
368              
369             =item update_changelog true/false
370              
371             update spec file changelog (default: true).
372              
373             =item spec_line_callback $callback
374              
375             callback to execute as filter for each spec file line (default: none).
376              
377             =item spec_line_expression $expression
378              
379             perl expression (or list of expressions) to evaluate for each spec file line
380             (default: none). Takes precedence over previous option.
381              
382             =item changelog_entries $entries
383              
384             list of changelog entries (default: empty).
385              
386             =back
387              
388             =cut
389              
390             sub update_from_repository {
391             my ($self, $name, $new_version, %options) = @_;
392             croak "Not a class method" unless ref $self;
393             my $src_file;
394              
395             if ($self->{_srpm_dirs}) {
396             foreach my $srpm_dir (@{$self->{_srpm_dirs}}) {
397             $src_file = $self->_find_source_package($srpm_dir, $name);
398             last if $src_file;
399             }
400             }
401              
402             croak "No source available for package $name, aborting" unless $src_file;
403              
404             $self->update_from_source($src_file, $new_version, %options);
405             }
406              
407             =head2 update_from_source($source, $version, %options)
408              
409             Update package with source file $source to version $version.
410              
411             See update_from_repository() for available options.
412              
413             =cut
414              
415             sub update_from_source {
416             my ($self, $src_file, $new_version, %options) = @_;
417             croak "Not a class method" unless ref $self;
418              
419             $wrapper_class->set_verbosity(0);
420             my ($spec_file) = $wrapper_class->install_srpm($src_file);
421              
422             croak "Unable to install source package $src_file, aborting"
423             unless $spec_file;
424              
425             $self->update_from_spec($spec_file, $new_version, %options);
426             }
427              
428             =head2 update_from_spec($spec, $version, %options)
429              
430             Update package with spec file $spec to version $version.
431              
432             See update_from_repository() for available options.
433              
434             =cut
435              
436             sub update_from_spec {
437             my ($self, $spec_file, $new_version, %options) = @_;
438             croak "Not a class method" unless ref $self;
439              
440             $options{download} = 1 unless defined $options{download};
441             $options{update_revision} = 1 unless defined $options{update_revision};
442             $options{update_changelog} = 1 unless defined $options{update_changelog};
443              
444             my $spec = $wrapper_class->new_spec($spec_file, force => 1)
445             or croak "Unable to parse spec $spec_file\n";
446              
447             $self->_update_spec($spec_file, $spec, $new_version, %options) if
448             $options{update_revision} ||
449             $options{update_changelog} ||
450             $options{spec_line_callback} ||
451             $options{spec_line_expression};
452              
453             $spec = $wrapper_class->new_spec($spec_file, force => 1)
454             or croak "Unable to parse updated spec file $spec_file\n";
455              
456             $self->_download_sources($spec, $new_version, %options) if
457             $new_version &&
458             $options{download};
459             }
460              
461             sub _update_spec {
462             my ($self, $spec_file, $spec, $new_version, %options) = @_;
463              
464             my $header = $spec->srcheader();
465              
466             # return if old version >= new version
467             my $old_version = $header->tag('version');
468             return if $options{check_new_version} &&
469             $new_version &&
470             RPM4::rpmvercmp($old_version, $new_version) >= 0;
471              
472             my $new_release = $options{release} || '';
473             my $epoch = $header->tag('epoch');
474              
475             if ($options{spec_line_expression}) {
476             $options{spec_line_callback} =
477             _get_callback($options{spec_line_expression});
478             }
479              
480             open(my $in, '<', $spec_file)
481             or croak "Unable to open file $spec_file: $!";
482              
483             my $content;
484             my ($version_updated, $release_updated, $changelog_updated);
485             while (my $line = <$in>) {
486             if ($options{update_revision} && # update required
487             $new_version && # version change needed
488             !$version_updated # not already done
489             ) {
490             my ($directive, $spacing, $value) =
491             _get_new_version($line, $new_version);
492             if ($directive && $value) {
493             $line = $directive . $spacing . $value . "\n";
494             $new_version = $value;
495             $version_updated = 1;
496             }
497             }
498              
499             if ($options{update_revision} && # update required
500             !$release_updated # not already done
501             ) {
502             my ($directive, $spacing, $value) =
503             _get_new_release($line, $new_version, $new_release, $self->{_release_suffix});
504             if ($directive && $value) {
505             $line = $directive . $spacing . $value . "\n";
506             $new_release = $value;
507             $release_updated = 1;
508             }
509             }
510              
511             # apply global and local callbacks if any
512             $line = $options{spec_line_callback}->($line)
513             if $options{spec_line_callback};
514              
515             $content .= $line;
516              
517             if ($options{update_changelog} &&
518             !$changelog_updated && # not already done
519             $line =~ /^\%changelog/
520             ) {
521             # skip until first changelog entry, as requested for bug #21389
522             while ($line = <$in>) {
523             last if $line =~ /^\*/;
524             $content .= $line;
525             }
526              
527             my @entries =
528             $options{changelog_entries} ? @{$options{changelog_entries}} :
529             $new_version ? $self->{_new_version_message} :
530             $self->{_new_release_message} ;
531             foreach my $entry (@entries) {
532             $entry =~ s/\%\%VERSION/$new_version/g;
533             }
534              
535             my $title = $wrapper_class->expand_macro(
536             DateTime->now()->strftime('%a %b %d %Y') .
537             ' ' .
538             $self->_get_packager() .
539             ' ' .
540             ($epoch ? $epoch . ':' : '') .
541             ($new_version ? $new_version : $old_version) .
542             '-' .
543             $new_release
544             );
545              
546             $content .= "* $title\n";
547             foreach my $entry (@entries) {
548             $content .= "- $entry\n";
549             }
550             $content .= "\n";
551              
552             # don't forget kept line
553             $content .= $line;
554              
555             # just to skip test for next lines
556             $changelog_updated = 1;
557             }
558             }
559             close($in);
560              
561             open(my $out, '>', $spec_file)
562             or croak "Unable to open file $spec_file: $!";
563             print $out $content;
564             close($out);
565             }
566              
567             sub _download_sources {
568             my ($self, $spec, $new_version, %options) = @_;
569              
570             foreach my $source ($self->_get_sources($spec, $new_version)) {
571             my $found;
572              
573             if ($source->{url} =~ m!http://prdownloads.sourceforge.net!) {
574             # if content is hosted on source forge, attempt to download
575             # from all configured mirrors
576             foreach my $mirror (@{$self->{_sourceforge_mirrors}}) {
577             my $sf_url = $source->{url};
578             $sf_url =~ s!prdownloads.sourceforge.net!$mirror.dl.sourceforge.net/sourceforge!;
579             $found = $self->_fetch_tarball($sf_url);
580             last if $found;
581             }
582             } else {
583             $found = $self->_fetch($source->{url});
584             }
585              
586             croak "Unable to download source: $source->{url}" unless $found;
587              
588             # recompress source if neeeded
589             _bzme($found) if $source->{bzme};
590             }
591              
592             }
593              
594             sub _fetch {
595             my ($self, $url) = @_;
596             # if you add a handler here, do not forget to add it to the body of build()
597             return $self->_fetch_tarball($url) if $url =~ m!^(ftp|https?)://!;
598             return $self->_fetch_svn($url) if $url =~ m!^svns?://!;
599             }
600              
601             sub _fetch_svn {
602             my ($self, $url) = @_;
603             my ($basename, $repos);
604              
605             $basename = basename($url);
606             ($repos = $url) =~ s|/$basename$||;
607             $repos =~ s/^svn/http/;
608             croak "Cannot extract revision number from the name."
609             if $basename !~ /^(.*)-([^-]*rev)(\d\d*).tar.bz2$/;
610             my ($name, $prefix, $release) = ($1, $2, $3);
611              
612             # extract repository in a temp directory
613             my $dir = tempdir(CLEANUP => 1);
614             my $archive = "$name-$prefix$release";
615             my $svn = SVN::Client->new();
616             $svn->export($repos, "$dir/$archive", $release);
617              
618             # archive and compress result
619             my $result = system("tar -cjf $archive.tar.bz2 -C $dir $archive");
620             croak("Error during archive creation: $?\n")
621             unless $result == 0;
622             }
623              
624             sub _fetch_tarball {
625             my ($self, $url) = @_;
626              
627             my $agent = LWP::UserAgent->new();
628             $agent->env_proxy();
629             $agent->timeout($self->{_timeout});
630             $agent->agent($self->{_agent});
631              
632             my $file = $self->_fetch_potential_tarball($agent, $url);
633              
634             # Mandriva policy implies to recompress sources, so if the one that was
635             # just looked for was missing, check with other formats
636             if (!$file and $url =~ /\.tar\.bz2$/) {
637             foreach my $extension (@{$self->{_alternate_extensions}}) {
638             my $alternate_url = $url;
639             $alternate_url =~ s/\.tar\.bz2$/.$extension/;
640             $file = $self->_fetch_potential_tarball($agent, $alternate_url);
641             if ($file) {
642             $file = _bzme($file);
643             last;
644             }
645             }
646             }
647              
648             return $file;
649             }
650              
651             sub _fetch_potential_tarball {
652             my ($self, $agent, $url) = @_;
653              
654             my $filename = basename($url);
655             my $dest = "$self->{_sourcedir}/$filename";
656              
657             # don't attempt to download file if already present
658             return $dest if -f $dest;
659              
660             print "attempting to download $url\n" if $self->{_verbose};
661             my $response = $agent->mirror($url, $dest);
662             if ($response->is_success()) {
663             print "response: OK\n" if $self->{_verbose} > 1;
664             my ($extension) = $filename =~ /\.(\w+)$/;
665             if ($self->{_archive_content_types}->{$extension}) {
666             # check content type for archives
667             my $type = $response->header('Content-Type');
668             print "checking content-type $type: " if $self->{_verbose} > 1;
669             if (
670             none { $type eq $_ }
671             @{$self->{_archive_content_types}->{$extension}},
672             @{$self->{_archive_content_types}->{_all}}
673             ) {
674             # wrong type
675             print "NOK\n" if $self->{_verbose} > 1;
676             unlink $dest;
677             return;
678             } else {
679             print "OK\n" if $self->{_verbose} > 1;
680             }
681             }
682             return $dest;
683             } else {
684             print "response: NOK\n" if $self->{_verbose} > 1;
685             return;
686             }
687             }
688              
689              
690             sub _get_packager {
691             my ($self) = @_;
692             my $packager = $wrapper_class->expand_macro('%packager');
693             if ($packager eq '%packager') {
694             my $login = (getpwuid($<))[0];
695             $packager = $ENV{EMAIL} ? "$login <$ENV{EMAIL}>" : $login;
696             }
697             return $packager;
698             }
699              
700              
701             sub _find_source_package {
702             my ($self, $dir, $name) = @_;
703              
704             my $file;
705             opendir(my $DIR, $dir) or croak "Unable to open $dir: $!";
706             while (my $entry = readdir($DIR)) {
707             if ($entry =~ /^\Q$name\E-[^-]+-[^-]+\.src.rpm$/) {
708             $file = "$dir/$entry";
709             last;
710             }
711             }
712             closedir($DIR);
713             return $file;
714             }
715              
716             sub _get_sources {
717             my ($self, $spec, $version) = @_;
718              
719             my $header = $spec->srcheader();
720             my $name = $header->tag('name');
721              
722             my @sources;
723              
724             # special cases: ignore sources defined in the spec file
725             if ($name =~ /^perl-(\S+)/) {
726             # source URL in the spec file can not be trusted, as it
727             # change for each release, so try to use CPAN metabase DB
728             my $cpan_name = $1;
729             $cpan_name =~ s/-/::/g;
730              
731             # ignore spec file URL, as it changes between releases
732             my ($cpan_url, $cpan_version) = _get_cpan_package_info(
733             $cpan_name
734             );
735              
736             if ($cpan_url && $cpan_version && $cpan_version eq $version) {
737             # use the result if available
738             my $source = ($spec->sources_url())[0];
739             @sources = ( { url => $cpan_url, bzme => $source =~ /\.tar\.bz2$/ } );
740             }
741             }
742              
743             return @sources if @sources;
744              
745             # default case: extract all sources defined with an URL in the spec file
746             @sources =
747             map { _fix_source($_, $version) }
748             map { { url => $_, bzme => 0 } }
749             grep { /(?:ftp|svns?|https?):\/\/\S+/ }
750             $spec->sources_url();
751              
752             return @sources if @sources;
753              
754             # fallback case: try a single source, with URL deduced from package URL
755              
756             print "No remote sources were found, fall back on URL tag ...\n"
757             if $self->{_verbose};
758              
759             my $url = $header->tag('url');
760              
761             foreach my $rule (@{$self->{_url_rewrite_rules}}) {
762             # curiously, we need two level of quoting-evaluation here :(
763             if ($url =~ s!$rule->{from}!qq(qq($rule->{to}))!ee) {
764             last;
765             }
766             }
767              
768             my $source = ($spec->sources_url())[0];
769             @sources = ( { url => $url . '/' . $source, bzme => 0 } );
770              
771             return @sources;
772             }
773              
774             sub _get_callback {
775             my ($expressions) = @_;
776              
777             my ($code, $sub);;
778             $code .= '$sub = sub {';
779             $code .= '$_ = $_[0];';
780             foreach my $expression (
781             ref $expressions eq 'ARRAY' ?
782             @{$expressions} : $expressions
783             ) {
784             $code .= $expression;
785             $code .= ";\n" unless $expression =~ /;$/;
786             }
787             $code .= 'return $_;';
788             $code .= '}';
789             ## no critic ProhibitStringyEva
790             eval $code;
791             ## use critic
792             warn "unable to compile given expression into code $code, skipping"
793             if $@;
794              
795             return $sub;
796             }
797              
798             sub _bzme {
799             my ($file) = @_;
800              
801             system("bzme -f -F $file >/dev/null 2>&1");
802             $file =~ s/\.(?:tar\.gz|tgz|zip)$/.tar.bz2/;
803              
804             return $file;
805             }
806              
807             sub _get_new_version {
808             my ($line, $new_version) = @_;
809              
810             return unless $line =~ /^
811             (
812             %define \s+ # macro
813             (?:
814             version
815             |
816             upstream_version
817             )
818             |
819             (?i)Version: # tag
820             )
821             (\s+) # spacing
822             (\S+(?: \s+ \S+)*) # value
823             /ox;
824              
825             my ($directive, $spacing, $value) = ($1, $2, $3);
826              
827             if ($new_version) {
828             $value = $new_version;
829             }
830              
831             return ($directive, $spacing, $value);
832             }
833             sub _get_new_release {
834             my ($line, $new_version, $new_release, $release_suffix) = @_;
835              
836             return unless $line =~ /^
837             (
838             %define \s+ # macro
839             (?:
840             rel
841             |
842             release
843             )
844             |
845             (?i)Release: # tag
846             )
847             (\s+) # spacing
848             (\S+(?: \s+ \S+)*) # value
849             /ox;
850              
851             my ($directive, $spacing, $value) = ($1, $2, $3);
852              
853             if ($new_release) {
854             $value = $new_release;
855             } else {
856             if ($value =~ /^% (\w+) (\s+) (\S+) $/x) {
857             my ($macro_name, $macro_spacing, $macro_value) = ($1, $2, $3);
858             $macro_value = _get_new_release_number($macro_value, $new_version, $release_suffix);
859             $value = '%' . $macro_name . $macro_spacing . $macro_value;
860             } elsif ($value =~ /^% { (\w+) (\s+) (\S+) } $/x) {
861             my ($macro_name, $macro_spacing, $macro_value) = ($1, $2, $3);
862             $macro_value = _get_new_release_number($macro_value, $new_version, $release_suffix);
863             $value = '%{' . $macro_name . $macro_spacing . $macro_value . '}';
864             } else {
865             $value = _get_new_release_number($value, $new_version, $release_suffix);
866             }
867             }
868              
869             return ($directive, $spacing, $value);
870             }
871              
872             sub _get_new_release_number {
873             my ($value, $new_version, $release_suffix) = @_;
874              
875             my ($prefix, $number, $suffix);
876             if ($new_version) {
877             $number = 1;
878             } else {
879             # optional suffix from configuration
880             $release_suffix = $release_suffix ?
881             quotemeta($release_suffix) : '';
882             ($prefix, $number, $suffix) =
883             $value =~ /^(.*?)(\d+)($release_suffix)?$/;
884              
885             croak "Unable to extract release number from value '$value'"
886             unless $number;
887              
888             $number++;
889             }
890              
891             return
892             ($prefix ? $prefix : "") .
893             $number .
894             ($suffix ? $suffix : "");
895              
896             }
897              
898             sub _fix_source {
899             my ($source, $version) = @_;
900              
901             given ($source->{url}) {
902             when (m!ftp.gnome.org/pub/GNOME/sources/!) {
903             # the last part of the path should match current
904             # major and minor version numbers:
905             # ftp://ftp.gnome.org/pub/GNOME/sources/ORbit2/2.10/ORbit2-2.10.0.tar.bz2
906             my ($major, $minor) = split('\.', $version);
907             $source->{url} =~ m!(.+)/([^/]+)$!;
908             my ($path, $file) = ($1, $2);
909             if ($path =~ m!/(\d+)\.(\d+)$!) {
910             # expected format found
911             if ($1 != $major || $2 != $minor) {
912             # but not corresponding to the current version
913             $path =~ s!\d+\.\d+$!$major.$minor!;
914             }
915             } else {
916             $path .= "/$major.$minor";
917             }
918             $source->{url} = "$path/$file";
919             }
920             when (m!\w+\.(perl|cpan)\.org/!) {
921             # force http
922             $source->{url} =~ s!ftp://ftp\.(perl|cpan)\.org/pub/CPAN!http://www.cpan.org!;
923             # force .tar.gz
924             $source->{bzme} = 1
925             if $source->{url} =~ s!\.tar\.bz2$!.tar.gz!;
926             }
927             when (m!download.pear.php.net/!) {
928             # PEAR: force tgz
929             $source->{bzme} = 1
930             if $source->{url} =~ s!\.tar\.bz2$!.tgz!;
931             }
932             }
933              
934             return $source;
935             }
936              
937             sub _get_cpan_package_info {
938             my ($name) = @_;
939              
940             my $agent = LWP::UserAgent->new();
941             $agent->env_proxy();
942              
943             my $response = $agent->get(
944             "http://cpanmetadb.appspot.com/v1.0/package/$name"
945             );
946              
947             return unless $response->is_success();
948              
949             my $conf = YAML::AppConfig->new(
950             string => $response->decoded_content()
951             );
952              
953             return unless $conf->get('distfile');
954              
955             my $url =
956             "http://search.cpan.org/CPAN/authors/id/" . $conf->get('distfile');
957             my $version = $conf->get('version');
958              
959             return ($url, $version);
960             }
961              
962             1;