]> git.pld-linux.org Git - projects/distfiles.git/blob - specparser.pl
(Temporarily) support both scp and ftp
[projects/distfiles.git] / specparser.pl
1 #!/usr/bin/perl
2 #
3 # USAGE: specparser.pl file.spec
4 #
5 # Output:
6 #   stdout:
7 #     list of:
8 #       <md5><space><url>\n
9 #     for given specfile.
10 #     except for lines prefixed with: ERROR:
11 #   exit status:
12 #     0 - normal
13 #     2 - cannot open spec file
14 #
15 use strict;
16 use warnings;
17
18 my %no_source;
19 my $spec;
20 my $base_spec;
21 my @spec;
22 my @sources;
23 my $sources_file;
24 my %patchset;
25
26 sub print_source($$$);
27
28 sub next_spec($)
29 {
30         $spec = shift;
31         @spec = ();
32         $base_spec = $spec;
33         $base_spec =~ s|.*/||;
34 }
35
36 sub error($)
37 {
38         print_once( "ERROR: $base_spec: $_[0]" );
39 }
40
41 sub warning($)
42 {
43         print "ERROR: $base_spec: $_[0]\n";
44 }
45
46 sub trim_spaces($)
47 {
48         my $v = shift;
49
50         $v =~ s/\s+$//;
51         $v =~ s/^\s+//;
52
53         return $v;
54 }
55
56 # expand macros in string
57 sub expand($$) # {{{
58 {
59         my $v = trim_spaces(shift);
60         my $macrotree = shift;
61         my $cnt = 20;
62
63         while ($v =~ /\%\{([^\}]+)\}/) {
64                 my $value;
65                 if (defined $macrotree->{$1}) {
66                         $value = $macrotree->{$1};
67                 } else {
68                         error("undefined macro $1");
69                         $value = "UNDEFINED";
70                         return undef;
71                 }
72                 $v =~ s/\%\{([^\}]+)\}/$value/;
73
74                 return $v if (length $v > 1000 or $cnt-- <= 0)
75         }
76
77         while ($v =~ s/\%\(\s*echo\s+([^\|]+?)\s*\|\s*tr\s*(-d|)\s+([^\)]+?)\s*\)/\@\@tr-me\@\@/) {
78                 my ($what, $d_opt, $how) = ($1, $2, $3);
79                 my ($from, $to) = ($how, "");
80                 ($from, $to) = ($1, $2)
81                         if $how =~ /^([^\s]+)\s+([^\s]+)$/;
82                 if ($d_opt and $to ne "") {
83                         error("tr -d with second string)");
84                 } elsif (($from . $to) =~ /^[a-zA-Z0-9\+_\-\.]+$/) {
85                         if ($d_opt) {
86                                 eval "\$what =~ tr/$from//d;";
87                         } else {
88                                 eval "\$what =~ tr/$from/$to/;";
89                         }
90                 } else {
91                         error("illegal characters in tr string(s) '$from' '$to'");
92                 }
93                 $v =~ s/\@\@tr-me\@\@/$what/;
94
95                 return $v if (length $v > 1000 or $cnt-- <= 0)
96         }
97
98         error("unexpanded macros in $v")
99                 if ($v =~ /\%[^0-9]/);
100
101         return $v;
102 } # }}}
103
104 sub preparse_spec($) # {{{
105 {
106         @spec = ("");
107
108         open(F, "< $_[0]") or die("failed opening: " . $_[0]);
109         while (<F>) {
110                 chomp;
111                 if (/^\s*(\%(description|package|prep|install|pre|post|files)|BuildRoot|URL)/) {
112                         last;
113                 } elsif (/^\s*(\%if.*|\%else|\%endif|\%define\s+.*|Version.*|Name.*)\s*$/) {
114                         $_ = $1;
115                         if ($spec[$#spec] =~ /\%if/) {
116                                 if (/\%else/) {
117                                         next; # don't include empty %if-%else
118                                 } elsif (/\%endif/) {
119                                         # remove empty %if-%endif
120                                         pop @spec;
121                                         next;
122                                 }
123                         }
124                         push @spec, $_;
125                 } elsif (/^NoSource\s*:\s*(\d+)\s*$/i) {
126                         $no_source{ "source" . $1 } = 1;
127                 } elsif (my ($patchset) = /^%patchset_source\s+(.+)$/) {
128                         use Getopt::Long qw(GetOptionsFromString);
129                         my ($f, $s);
130                         my ($ret, $args) = GetOptionsFromString($patchset, 's=s' => \$s, 'f=s' => \$f);
131                         %patchset = (
132                                 pattern => $f,
133                                 filelist => $s,
134                                 start => $args->[0],
135                                 end => $args->[1],
136                         );
137                 }
138         }
139         close(F);
140
141         shift @spec;
142 } # }}}
143
144 # read in 'sources' file
145 sub read_sources_file {
146         my $filename = $_[0] || $sources_file;
147         return () unless $filename and -e $filename;
148
149         my %files;
150
151         open(my $fh, '<', $filename) or die $!;
152         while (<$fh>) {
153                 chomp;
154                 next unless my ($hash, $filename) = /^([a-f0-9]{32})\s+\*?(.+)$/;
155                 $files{$filename} = $hash;
156         }
157         return \%files;
158 }
159
160 sub process_patchset($) {
161         my $macros = shift;
162         my $checksums;
163
164         return unless %patchset;
165
166         # print all files from sources file
167         if ($patchset{filelist}) {
168                 my $prefix = expand($patchset{pattern}, $macros);
169                 my $filelist = expand($patchset{filelist}, $macros);
170                 $checksums = read_sources_file($filelist);
171                 while (my($file, $hash) = each %$checksums) {
172                         my $url = $prefix . $file;
173                         print_source "patchset $file", $hash, $url;
174                 }
175                 return;
176         }
177
178         # parse sources file sequences
179         $checksums = read_sources_file() or return;
180
181         # print out patchset entries which source md5 is present in source file
182         my $start = expand($patchset{start}, $macros);
183         my $end = expand($patchset{end}, $macros);
184         my $pattern = expand($patchset{pattern}, $macros);
185         for (my $i = $start; $i <= $end; $i++) {
186                 my $url = sprintf($pattern, $i);
187                 my ($basename) = $url =~ m{/([^/]+$)};
188                 my $hash = $checksums->{$basename} or next;
189                 print_source "patchset $i", $hash, $url;
190         }
191 }
192
193
194 my $total = 0;
195
196 sub cont($$);
197 sub cont($$) # {{{
198 {
199         my ($spec, $macros) = @_;
200         local $_;
201         while ($_ = shift @{$spec}) {
202                 if (0 <= index $_, '%if') { # if, ifarch, ifos
203
204                         # split spec parsing
205                         my @speccopy = @{$spec};
206                         my %macroscopy = %{$macros};
207                         cont(\@speccopy, \%macroscopy);
208
209                         my $level = 0;
210                         while ($_ = shift @{$spec}) {
211                                 if ($level <= 0 and ($_ eq '%else' or $_ eq '%endif')) {
212                                         last;
213                                 } elsif (0 <= index $_, '%if') {
214                                         $level++;
215                                 } elsif ($_ eq '%endif') {
216                                         $level--;
217                                 }
218                         }
219
220                         # continue parsing
221                 } elsif ($_ eq '%else') {
222
223                         # %else happens only when %if was interpreted
224                         # so skip until %endif
225
226                         my $level = 0;
227                         while ($_ = shift @{$spec}) {
228                                 if ($level <= 0 and $_ eq '%endif') {
229                                         last;
230                                 } elsif (0 <= index $_, '%if') {
231                                         $level++;
232                                 } elsif ($_ eq '%endif') {
233                                         $level--;
234                                 }
235                         }
236
237                 } elsif (/^\s*\%(define|global)\s+([^\s]+)\s+([^\s].*?)\s*$/) {
238                         $macros->{$2} = $3;
239
240                 } elsif (/^Version\s*:\s*(.*?)\s*$/i) {
241                         $macros->{"version"} = $1;
242                 } elsif (/^Name\s*:\s*(.*?)\s*$/i) {
243                         $macros->{"name"} = $1;
244                 } elsif (!/\%endif/) {
245                         warn "unrecognised line: $_\n";
246                 }
247         }
248
249         # so we have macros now, can parse patchset source
250         process_patchset($macros);
251
252         # the end, yuppie !
253         foreach my $s (@sources) {
254                 my $src = expand( $s->[2], $macros );
255                 if (defined $src) {
256                         our %tried;
257                         unless (exists $tried{$src}) {
258                                 print_source( $s->[0], $s->[1], $src );
259                                 $tried{$src} = 1;
260                         }
261                 }
262         }
263
264         if (++$total > 10000) {
265                 error("maximum number of bcond posibilities exceeded");
266                 exit 0;
267         }
268 } # }}}
269
270 sub print_once($)
271 {
272         our %printed;
273         my $l = shift;
274         unless (exists $printed{$l}) {
275                 print $l . "\n";
276                 $printed{$l} = 1;
277         }
278 }
279
280 sub print_source($$$) # {{{
281 {
282         my ($no, $md5, $s) = @_;
283
284         if ($s =~ /^([a-z0-9A-Z;:\=\?&\@\+\~\.,\-\/_]|\%[0-9])+(#\/[a-zA-Z0-9\._-]+)?$/) {
285                 if ($s =~ /^(ftp|http|https):\/\//) {
286                         if ($s =~ /\/$/) {
287                                 error("source $no ($s) is directory");
288                         } else {
289                                 if ($s =~ /:\/\/distfiles\.pld-linux\.org\/src/) {
290                                         $s =~ s|.*/||;
291                                         print_once( "$md5 no-url-copy://$s" );
292                                 } else {
293                                         print_once( "$md5 $s" );
294                                 }
295                         }
296                 } else {
297                         $s =~ s|.*/||;
298                         print_once( "$md5 no-url://$s");
299                 }
300         } else {
301                 error("source $no url $s is ill-formatted");
302         }
303 } # }}}
304
305 sub add_md5_to_print($) # {{{
306 {
307         open(F, "< $_[0]") or die("failed opening: " . $_[0]);
308         my %sourcemap = ();
309         while (<F>) {
310                 chomp;
311                 if (/^((?:Source|Patch)\d+)\s*:\s*(.*)/i) {
312                         my $sourceno = lc $1;
313                         my $source = $2;
314                         # master.dl is outdated currently
315                         # $source =~ s/dl.sourceforge.net/master.dl.sourceforge.net/;
316                         $sourcemap{ $sourceno } = $source;
317                 } elsif (/^\s*#\s*((?:source|patch)\d+)-md5\s*:\s*([a-f0-9]{32})/i) {
318                         my $no = lc $1;
319                         my $md5 = $2;
320                         if (defined $no_source{$no}) {
321                                 error("both NoSource: $no and md5 given");
322                         } elsif (defined $sourcemap{ $no} ) {
323                                 my $source = $sourcemap{ $no };
324                                 push @sources, [$no, $md5, $source];
325                         } else {
326                                 error("source $no not defined (# SourceN-md5: has to be placed just after SourceN:)");
327                         }
328                 } elsif (/^\s*BuildRequires:\s*digest\(%((?:SOURCE|PATCH)\d+)\)\s*=\s*([a-f0-9]{32})/i) {
329                         my $no = lc $1;
330                         my $md5 = $2;
331                         if (defined $no_source{ $no }) {
332                                 error("both NoSource: $no and md5 given");
333                         } elsif (defined $sourcemap{ $no }) {
334                                 my $source = $sourcemap{ $no };
335                                 push @sources, [$no, $md5, $source];
336                         } else {
337                                 error("source $no not defined (# Source digest has to be placed after SourceN:)");
338                         }
339                 }
340         }
341         close(F);
342
343         # find extra sources from detached checksum file
344         my $checksums = read_sources_file();
345         my @mapped = map { $_->[0] } @sources;
346         delete @sourcemap{@mapped};
347         while (my($source, $url) = each %sourcemap) {
348                 my ($basename) = $url =~ m{/([^/]+$)};
349                 next unless defined $basename;
350                 my $hash = $checksums->{$basename} or next;
351                 push @sources, [$source, $hash, $url];
352         }
353 } # }}}
354
355 next_spec(shift);
356 $sources_file = shift if @ARGV;
357 preparse_spec($spec);
358 add_md5_to_print($spec);
359 cont( \@spec, { "nil" => "" } );
360
361 exit(0);
362
363 # vim: ts=4:sw=4:fdm=marker
This page took 0.064055 seconds and 3 git commands to generate.