1 | #!/usr/bin/perl |
2 | use Getopt::Std; |
3 | use FindBin; |
4 | use Cwd; |
5 | use lib "$FindBin::Bin"; |
6 | use metadata; |
7 | use warnings; |
8 | use strict; |
9 | use Cwd 'abs_path'; |
10 | |
11 | chdir "$FindBin::Bin/.."; |
12 | $ENV{TOPDIR}=getcwd(); |
13 | $ENV{GIT_CONFIG_PARAMETERS}="'core.autocrlf=false'"; |
14 | |
15 | my $mk=`which gmake 2>/dev/null`; # select the right 'make' program |
16 | chomp($mk); # trim trailing newline |
17 | $mk or $mk = "make"; # default to 'make' |
18 | |
19 | # check version of make |
20 | my @mkver = split /\s+/, `$mk -v`, 4; |
21 | my $valid_mk = 1; |
22 | $mkver[0] =~ /^GNU/ or $valid_mk = 0; |
23 | $mkver[1] =~ /^Make/ or $valid_mk = 0; |
24 | $mkver[2] >= "3.81" or $valid_mk = 0; |
25 | $valid_mk or die "Unsupported version of make found: $mk\n"; |
26 | |
27 | my @feeds; |
28 | my %build_packages; |
29 | my %installed; |
30 | my %feed_cache; |
31 | |
32 | my $feed_package = {}; |
33 | my $feed_src = {}; |
34 | |
35 | sub parse_config() { |
36 | my $line = 0; |
37 | my %name; |
38 | |
39 | open FEEDS, "feeds.conf" or |
40 | open FEEDS, "feeds.conf.default" or |
41 | die "Unable to open feeds configuration"; |
42 | while (<FEEDS>) { |
43 | chomp; |
44 | s/#.+$//; |
45 | next unless /\S/; |
46 | my @line = split /\s+/, $_, 3; |
47 | my @src; |
48 | $line++; |
49 | |
50 | my $valid = 1; |
51 | $line[0] =~ /^src-\w+$/ or $valid = 0; |
52 | $line[1] =~ /^\w+$/ or $valid = 0; |
53 | @src = split /\s+/, $line[2]; |
54 | $valid or die "Syntax error in feeds.conf, line: $line\n"; |
55 | |
56 | $name{$line[1]} and die "Duplicate feed name '$line[1]', line: $line\n"; |
57 | $name{$line[1]} = 1; |
58 | |
59 | push @feeds, [$line[0], $line[1], \@src]; |
60 | } |
61 | close FEEDS; |
62 | } |
63 | |
64 | sub update_location($$) |
65 | { |
66 | my $name = shift; |
67 | my $url = shift; |
68 | my $old_url; |
69 | |
70 | -d "./feeds/$name.tmp" or mkdir "./feeds/$name.tmp" or return 1; |
71 | |
72 | if( open LOC, "< ./feeds/$name.tmp/location" ) |
73 | { |
74 | chomp($old_url = readline LOC); |
75 | close LOC; |
76 | } |
77 | |
78 | if( !$old_url || $old_url ne $url ) |
79 | { |
80 | if( open LOC, "> ./feeds/$name.tmp/location" ) |
81 | { |
82 | print LOC $url, "\n"; |
83 | close LOC; |
84 | } |
85 | return $old_url ? 1 : 0; |
86 | } |
87 | |
88 | return 0; |
89 | } |
90 | |
91 | sub update_index($) |
92 | { |
93 | my $name = shift; |
94 | |
95 | -d "./feeds/$name.tmp" or mkdir "./feeds/$name.tmp" or return 1; |
96 | -d "./feeds/$name.tmp/info" or mkdir "./feeds/$name.tmp/info" or return 1; |
97 | |
98 | system("$mk -s prepare-mk OPENWRT_BUILD= TMP_DIR=\"$ENV{TOPDIR}/feeds/$name.tmp\""); |
99 | system("$mk -s -f include/scan.mk IS_TTY=1 SCAN_TARGET=\"packageinfo\" SCAN_DIR=\"feeds/$name\" SCAN_NAME=\"package\" SCAN_DEPS=\"$ENV{TOPDIR}/include/package*.mk\" SCAN_DEPTH=5 SCAN_EXTRA=\"\" TMP_DIR=\"$ENV{TOPDIR}/feeds/$name.tmp\""); |
100 | system("ln -sf $name.tmp/.packageinfo ./feeds/$name.index"); |
101 | |
102 | return 0; |
103 | } |
104 | |
105 | my %update_method = ( |
106 | 'src-svn' => { |
107 | 'init' => "svn checkout '%s' '%s'", |
108 | 'update' => "svn update", |
109 | 'controldir' => ".svn", |
110 | 'revision' => "svn info | grep 'Revision' | cut -d ' ' -f 2 | tr -d '\n'"}, |
111 | 'src-cpy' => { |
112 | 'init' => "cp -Rf '%s' '%s'", |
113 | 'update' => "", |
114 | 'revision' => "echo -n 'local'"}, |
115 | 'src-link' => { |
116 | 'init' => "ln -s '%s' '%s'", |
117 | 'update' => "", |
118 | 'revision' => "echo -n 'local'"}, |
119 | 'src-git' => { |
120 | 'init' => "git clone --depth 1 '%s' '%s'", |
121 | 'init_branch' => "git clone --depth 1 --branch '%s' '%s' '%s'", |
122 | 'update' => "git pull", |
123 | 'controldir' => ".git", |
124 | 'revision' => "git show --abbrev-commit HEAD | head -n 1 | cut -d ' ' -f 2 | tr -d '\n'"}, |
125 | 'src-gitsvn' => { |
126 | 'init' => "git svn clone -r HEAD '%s' '%s'", |
127 | 'update' => "git svn rebase", |
128 | 'controldir' => ".git", |
129 | 'revision' => "git show --abbrev-commit HEAD | head -n 1 | cut -d ' ' -f 2 | tr -d '\n'"}, |
130 | 'src-bzr' => { |
131 | 'init' => "bzr checkout --lightweight '%s' '%s'", |
132 | 'update' => "bzr update", |
133 | 'controldir' => ".bzr"}, |
134 | 'src-hg' => { |
135 | 'init' => "hg clone '%s' '%s'", |
136 | 'update' => "hg pull --update", |
137 | 'controldir' => ".hg"}, |
138 | 'src-darcs' => { |
139 | 'init' => "darcs get '%s' '%s'", |
140 | 'update' => "darcs pull -a", |
141 | 'controldir' => "_darcs"}, |
142 | ); |
143 | |
144 | # src-git: pull broken |
145 | # src-cpy: broken if `basename $src` != $name |
146 | |
147 | sub update_feed_via($$$$) { |
148 | my $type = shift; |
149 | my $name = shift; |
150 | my $src = shift; |
151 | my $relocate = shift; |
152 | |
153 | my $m = $update_method{$type}; |
154 | my $localpath = "./feeds/$name"; |
155 | my $safepath = $localpath; |
156 | $safepath =~ s/'/'\\''/; |
157 | my ($base, $branch) = split(/;/, $src, 2); |
158 | |
159 | if( $relocate || !$m->{'update'} || !-d "$localpath/$m->{'controldir'}" ) { |
160 | system("rm -rf '$safepath'"); |
161 | if ($m->{'init_branch'} and $branch) { |
162 | system(sprintf($m->{'init_branch'}, $branch, $base, $safepath)) == 0 or return 1; |
163 | } else { |
164 | system(sprintf($m->{'init'}, $src, $safepath)) == 0 or return 1; |
165 | } |
166 | } else { |
167 | system("cd '$safepath'; $m->{'update'}") == 0 or return 1; |
168 | } |
169 | |
170 | return 0; |
171 | } |
172 | |
173 | sub get_feed($) { |
174 | my $feed = shift; |
175 | |
176 | if (!defined($feed_cache{$feed})) { |
177 | my $file = "./feeds/$feed.index"; |
178 | |
179 | clear_packages(); |
180 | -f $file or do { |
181 | print "Ignoring feed '$feed' - index missing\n"; |
182 | return; |
183 | }; |
184 | parse_package_metadata($file) or return; |
185 | $feed_cache{$feed} = [ { %package }, { %srcpackage } ]; |
186 | } |
187 | |
188 | $feed_package = $feed_cache{$feed}->[0]; |
189 | $feed_src = $feed_cache{$feed}->[1]; |
190 | return $feed_cache{$feed}->[0]; |
191 | } |
192 | |
193 | sub get_installed() { |
194 | system("$mk -s prepare-tmpinfo OPENWRT_BUILD="); |
195 | clear_packages(); |
196 | parse_package_metadata("./tmp/.packageinfo"); |
197 | %installed = %package; |
198 | } |
199 | |
200 | sub search_feed { |
201 | my $feed = shift; |
202 | my @substr = @_; |
203 | my $display; |
204 | |
205 | return unless @substr > 0; |
206 | get_feed($feed); |
207 | foreach my $name (sort { lc($a) cmp lc($b) } keys %$feed_package) { |
208 | my $pkg = $feed_package->{$name}; |
209 | my $substr; |
210 | my $pkgmatch = 1; |
211 | |
212 | next if $pkg->{vdepends}; |
213 | foreach my $substr (@substr) { |
214 | my $match; |
215 | foreach my $key (qw(name title description src)) { |
216 | $pkg->{$key} and $substr and $pkg->{$key} =~ m/$substr/i and $match = 1; |
217 | } |
218 | $match or undef $pkgmatch; |
219 | }; |
220 | $pkgmatch and do { |
221 | $display or do { |
222 | print "Search results in feed '$feed':\n"; |
223 | $display = 1; |
224 | }; |
225 | printf "\%-25s\t\%s\n", $pkg->{name}, $pkg->{title}; |
226 | }; |
227 | } |
228 | return 0; |
229 | } |
230 | |
231 | sub search { |
232 | my %opts; |
233 | |
234 | getopt('r:', \%opts); |
235 | foreach my $feed (@feeds) { |
236 | search_feed($feed->[1], @ARGV) if (!defined($opts{r}) or $opts{r} eq $feed->[1]); |
237 | } |
238 | } |
239 | |
240 | sub list_feed { |
241 | my $feed = shift; |
242 | |
243 | get_feed($feed); |
244 | foreach my $name (sort { lc($a) cmp lc($b) } keys %$feed_package) { |
245 | my $pkg = $feed_package->{$name}; |
246 | next if $pkg->{vdepends}; |
247 | if($pkg->{name}) { |
248 | printf "\%-32s\t\%s\n", $pkg->{name}, $pkg->{title}; |
249 | } |
250 | } |
251 | |
252 | return 0; |
253 | } |
254 | |
255 | sub list { |
256 | my %opts; |
257 | |
258 | getopts('r:d:sh', \%opts); |
259 | if ($opts{h}) { |
260 | usage(); |
261 | return 0; |
262 | } |
263 | if ($opts{s}) { |
264 | foreach my $feed (@feeds) { |
265 | my $localpath = "./feeds/$feed->[1]"; |
266 | my $m = $update_method{$feed->[0]}; |
267 | my $revision; |
268 | if( !$m->{'revision'} ) { |
269 | $revision = "X"; |
270 | } |
271 | elsif( $m->{'controldir'} && -d "$localpath/$m->{'controldir'}" ) { |
272 | $revision = `cd '$localpath'; $m->{'revision'}`; |
273 | } |
274 | else { |
275 | $revision = "local"; |
276 | } |
277 | if ($opts{d}) { |
278 | printf "%s%s%s%s%s%s%s\n", $feed->[1], $opts{d}, $feed->[0], $opts{d}, $revision, $opts{d}, join(", ", @{$feed->[2]}); |
279 | } |
280 | else { |
281 | printf "\%-8s \%-8s \%-8s \%s\n", $feed->[1], $feed->[0], $revision, join(", ", @{$feed->[2]}); |
282 | } |
283 | } |
284 | return 0; |
285 | } |
286 | foreach my $feed (@feeds) { |
287 | list_feed($feed->[1], @ARGV) if (!defined($opts{r}) or $opts{r} eq $feed->[1]); |
288 | } |
289 | return 0; |
290 | } |
291 | |
292 | sub install_generic() { |
293 | my $feed = shift; |
294 | my $pkg = shift; |
295 | my $path = $pkg->{makefile}; |
296 | |
297 | if($path) { |
298 | $path =~ s/\/Makefile$//; |
299 | |
300 | -d "./package/feeds" or mkdir "./package/feeds"; |
301 | -d "./package/feeds/$feed->[1]" or mkdir "./package/feeds/$feed->[1]"; |
302 | system("ln -sf ../../../$path ./package/feeds/$feed->[1]/"); |
303 | } else { |
304 | warn "Package is not valid\n"; |
305 | return 1; |
306 | } |
307 | |
308 | return 0; |
309 | } |
310 | |
311 | my %install_method = ( |
312 | 'src-svn' => \&install_generic, |
313 | 'src-cpy' => \&install_generic, |
314 | 'src-link' => \&install_generic, |
315 | 'src-git' => \&install_generic, |
316 | 'src-gitsvn' => \&install_generic, |
317 | 'src-bzr' => \&install_generic, |
318 | 'src-hg' => \&install_generic, |
319 | 'src-darcs' => \&install_generic, |
320 | ); |
321 | |
322 | my %feed; |
323 | |
324 | sub lookup_package($$) { |
325 | my $feed = shift; |
326 | my $package = shift; |
327 | |
328 | foreach my $feed ($feed, @feeds) { |
329 | next unless $feed->[1]; |
330 | next unless $feed{$feed->[1]}; |
331 | $feed{$feed->[1]}->{$package} and return $feed; |
332 | } |
333 | return; |
334 | } |
335 | |
336 | sub install_package { |
337 | my $feed = shift; |
338 | my $name = shift; |
339 | my $ret = 0; |
340 | |
341 | $feed = lookup_package($feed, $name); |
342 | $feed or do { |
343 | $installed{$name} and return 0; |
344 | # TODO: check if it's already installed within ./package directory |
345 | $feed_src->{$name} or -d "./package/$name" or warn "WARNING: No feed for package '$name' found, maybe it's already part of the standard packages?\n"; |
346 | return 0; |
347 | }; |
348 | |
349 | # switch to the metadata for the selected feed |
350 | get_feed($feed->[1]); |
351 | |
352 | my $pkg = $feed{$feed->[1]}->{$name} or return 1; |
353 | $pkg->{name} or do { |
354 | $installed{$name} and return 0; |
355 | # TODO: check if this is an alias package, maybe it's known by another name |
356 | warn "WARNING: Package '$name' is not available in feed $feed->[1].\n"; |
357 | return 0; |
358 | }; |
359 | my $src = $pkg->{src}; |
360 | my $type = $feed->[0]; |
361 | $src or $src = $name; |
362 | |
363 | # previously installed packages set the runtime package |
364 | # newly installed packages set the source package |
365 | $installed{$src} and return 0; |
366 | |
367 | # check previously installed packages |
368 | $installed{$name} and return 0; |
369 | $installed{$src} = 1; |
370 | warn "Installing package '$src'\n"; |
371 | |
372 | $install_method{$type} or do { |
373 | warn "Unknown installation method: '$type'\n"; |
374 | return 1; |
375 | }; |
376 | |
377 | &{$install_method{$type}}($feed, $pkg) == 0 or do { |
378 | warn "failed.\n"; |
379 | return 1; |
380 | }; |
381 | |
382 | # install all dependencies referenced from the source package |
383 | foreach my $vpkg (@{$feed_src->{$src}}) { |
384 | foreach my $dep (@{$vpkg->{depends}}, @{$vpkg->{builddepends}}, @{$vpkg->{"builddepends/host"}}) { |
385 | next if $dep =~ /@/; |
386 | $dep =~ s/^\+//; |
387 | $dep =~ s/^.+://; |
388 | $dep =~ s/\/.+$//; |
389 | next unless $dep; |
390 | install_package($feed, $dep) == 0 or $ret = 1; |
391 | } |
392 | } |
393 | |
394 | return $ret; |
395 | } |
396 | |
397 | sub refresh_config { |
398 | my $default = shift; |
399 | |
400 | # workaround for timestamp check |
401 | system("rm -f tmp/.packageinfo"); |
402 | |
403 | # refresh the config |
404 | if ($default) { |
405 | system("$mk oldconfig CONFDEFAULT=\"$default\" Config.in >/dev/null 2>/dev/null"); |
406 | } else { |
407 | system("$mk defconfig Config.in >/dev/null 2>/dev/null"); |
408 | } |
409 | } |
410 | |
411 | sub install { |
412 | my $name; |
413 | my %opts; |
414 | my $feed; |
415 | my $ret = 0; |
416 | |
417 | getopts('ap:d:h', \%opts); |
418 | |
419 | if ($opts{h}) { |
420 | usage(); |
421 | return 0; |
422 | } |
423 | |
424 | get_installed(); |
425 | |
426 | foreach my $f (@feeds) { |
427 | # index all feeds |
428 | $feed{$f->[1]} = get_feed($f->[1]); |
429 | |
430 | # look up the preferred feed |
431 | $opts{p} and $f->[1] eq $opts{p} and $feed = $f; |
432 | } |
433 | |
434 | if($opts{a}) { |
435 | foreach my $f (@feeds) { |
436 | if (!defined($opts{p}) or $opts{p} eq $f->[1]) { |
437 | printf "Installing all packages from feed %s.\n", $f->[1]; |
438 | get_feed($f->[1]); |
439 | foreach my $name (sort { lc($a) cmp lc($b) } keys %$feed_package) { |
440 | my $p = $feed_package->{$name}; |
441 | next if $p->{vdepends}; |
442 | if( $p->{name} ) { |
443 | install_package($feed, $p->{name}) == 0 or $ret = 1; |
444 | get_feed($f->[1]); |
445 | } |
446 | } |
447 | } |
448 | } |
449 | } else { |
450 | while ($name = shift @ARGV) { |
451 | install_package($feed, $name) == 0 or $ret = 1; |
452 | } |
453 | } |
454 | |
455 | # workaround for timestamp check |
456 | |
457 | # set the defaults |
458 | if ($opts{d} and $opts{d} =~ /^[ymn]$/) { |
459 | refresh_config($opts{d}); |
460 | } |
461 | |
462 | return $ret; |
463 | } |
464 | |
465 | sub uninstall { |
466 | my %opts; |
467 | my $name; |
468 | my $uninstall; |
469 | |
470 | getopts('ah', \%opts); |
471 | |
472 | if ($opts{h}) { |
473 | usage(); |
474 | return 0; |
475 | } |
476 | |
477 | if ($opts{a}) { |
478 | system("rm -rvf ./package/feeds"); |
479 | $uninstall = 1; |
480 | } else { |
481 | if($#ARGV == -1) { |
482 | warn "WARNING: no package to uninstall\n"; |
483 | return 0; |
484 | } |
485 | get_installed(); |
486 | while ($name = shift @ARGV) { |
487 | my $pkg = $installed{$name}; |
488 | $pkg or do { |
489 | warn "WARNING: $name not installed\n"; |
490 | next; |
491 | }; |
492 | $pkg->{src} and $name = $pkg->{src}; |
493 | warn "Uninstalling package '$name'\n"; |
494 | system("rm -f ./package/feeds/*/$name"); |
495 | $uninstall = 1; |
496 | } |
497 | } |
498 | $uninstall and refresh_config(); |
499 | return 0; |
500 | } |
501 | |
502 | sub update_feed($$$$) |
503 | { |
504 | my $type=shift; |
505 | my $name=shift; |
506 | my $src=shift; |
507 | my $perform_update=shift; |
508 | my $force_relocate=update_location( $name, "@$src" ); |
509 | |
510 | if( $force_relocate ) { |
511 | warn "Source of feed $name has changed, replacing copy\n"; |
512 | } |
513 | $update_method{$type} or do { |
514 | warn "Unknown type '$type' in feed $name\n"; |
515 | return 1; |
516 | }; |
517 | $perform_update and do { |
518 | my $failed = 1; |
519 | foreach my $feedsrc (@$src) { |
520 | warn "Updating feed '$name' from '$feedsrc' ...\n"; |
521 | next unless update_feed_via($type, $name, $feedsrc, $force_relocate) == 0; |
522 | $failed = 0; |
523 | last; |
524 | } |
525 | $failed and do { |
526 | warn "failed.\n"; |
527 | return 1; |
528 | }; |
529 | }; |
530 | warn "Create index file './feeds/$name.index' \n"; |
531 | update_index($name) == 0 or do { |
532 | warn "failed.\n"; |
533 | return 1; |
534 | }; |
535 | return 0; |
536 | } |
537 | |
538 | sub update { |
539 | my %opts; |
540 | my $feed_name; |
541 | my $perform_update=1; |
542 | |
543 | $ENV{SCAN_COOKIE} = $$; |
544 | $ENV{OPENWRT_VERBOSE} = 's'; |
545 | |
546 | getopts('ahi', \%opts); |
547 | |
548 | if ($opts{h}) { |
549 | usage(); |
550 | return 0; |
551 | } |
552 | |
553 | if ($opts{i}) { |
554 | # don't update from (remote) repository |
555 | # only re-create index information |
556 | $perform_update=0; |
557 | } |
558 | |
559 | -d "feeds" or do { |
560 | mkdir "feeds" or die "Unable to create the feeds directory"; |
561 | }; |
562 | |
563 | if ( ($#ARGV == -1) or $opts{a}) { |
564 | foreach my $feed (@feeds) { |
565 | my ($type, $name, $src) = @$feed; |
566 | update_feed($type, $name, $src, $perform_update); |
567 | } |
568 | } else { |
569 | while ($feed_name = shift @ARGV) { |
570 | foreach my $feed (@feeds) { |
571 | my ($type, $name, $src) = @$feed; |
572 | if($feed_name ne $name) { |
573 | next; |
574 | } |
575 | update_feed($type, $name, $src, $perform_update); |
576 | } |
577 | } |
578 | } |
579 | |
580 | refresh_config(); |
581 | |
582 | return 0; |
583 | } |
584 | |
585 | sub usage() { |
586 | print <<EOF; |
587 | Usage: $0 <command> [options] |
588 | |
589 | Commands: |
590 | list [options]: List feeds, their content and revisions (if installed) |
591 | Options: |
592 | -s : List of feed names and their URL. |
593 | -r <feedname>: List packages of specified feed. |
594 | -d <delimiter>: Use specified delimiter to distinguish rows (default: spaces) |
595 | |
596 | install [options] <package>: Install a package |
597 | Options: |
598 | -a : Install all packages from all feeds or from the specified feed using the -p option. |
599 | -p <feedname>: Prefer this feed when installing packages. |
600 | -d <y|m|n>: Set default for newly installed packages. |
601 | |
602 | search [options] <substring>: Search for a package |
603 | Options: |
604 | -r <feedname>: Only search in this feed |
605 | |
606 | uninstall -a|<package>: Uninstall a package |
607 | Options: |
608 | -a : Uninstalls all packages. |
609 | |
610 | update -a|<feedname(s)>: Update packages and lists of feeds in feeds.conf . |
611 | Options: |
612 | -a : Update all feeds listed within feeds.conf. Otherwise the specified feeds will be updated. |
613 | -i : Recreate the index only. No feed update from repository is performed. |
614 | |
615 | clean: Remove downloaded/generated files. |
616 | |
617 | EOF |
618 | exit(1); |
619 | } |
620 | |
621 | my %commands = ( |
622 | 'list' => \&list, |
623 | 'update' => \&update, |
624 | 'install' => \&install, |
625 | 'search' => \&search, |
626 | 'uninstall' => \&uninstall, |
627 | 'clean' => sub { |
628 | system("rm -rf feeds"); |
629 | } |
630 | ); |
631 | |
632 | my $arg = shift @ARGV; |
633 | $arg or usage(); |
634 | parse_config; |
635 | foreach my $cmd (keys %commands) { |
636 | $arg eq $cmd and do { |
637 | exit(&{$commands{$cmd}}()); |
638 | }; |
639 | } |
640 | usage(); |
641 | |