]> git.netwichtig.de Git - user/henk/code/inspircd.git/blob - modulemanager
Amend modulemanager to use {read,write}_config_file.
[user/henk/code/inspircd.git] / modulemanager
1 #!/usr/bin/env perl
2
3 #
4 # InspIRCd -- Internet Relay Chat Daemon
5 #
6 #   Copyright (C) 2012-2017 Peter Powell <petpow@saberuk.com>
7 #   Copyright (C) 2008-2009 Robin Burchell <robin+git@viroteck.net>
8 #
9 # This file is part of InspIRCd.  InspIRCd is free software: you can
10 # redistribute it and/or modify it under the terms of the GNU General Public
11 # License as published by the Free Software Foundation, version 2.
12 #
13 # This program is distributed in the hope that it will be useful, but WITHOUT
14 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
16 # details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21
22
23 use strict;
24 use warnings FATAL => qw(all);
25
26 use make::common;
27
28 BEGIN {
29         unless (module_installed("LWP::Simple")) {
30                 die "Your system is missing the LWP::Simple Perl module!";
31         }
32         unless (module_installed("Crypt::SSLeay") || module_installed("IO::Socket::SSL")) {
33                 die "Your system is missing the Crypt::SSLeay or IO::Socket::SSL Perl modules!";
34         }
35
36 }
37
38 use File::Basename;
39 use LWP::Simple;
40
41 my %installed;
42 # $installed{name} = $version
43
44 my %modules;
45 # $modules{$name}{$version} = {
46 #       url => URL of this version
47 #       depends => [ 'm_foo 1.2.0-1.3.0', ... ]
48 #       conflicts => [ ]
49 #       from => URL of source document
50 #       mask => Reason for not installing (INSECURE/DEPRECATED)
51 #       description => some string
52 # }
53
54 my %url_seen;
55
56 sub parse_url;
57
58 # retrieve and parse entries from sources.list
59 sub parse_url {
60         chomp(my $src = shift);
61         return if $url_seen{$src};
62         $url_seen{$src}++;
63
64         my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
65         my $response = $ua->get($src);
66
67         unless ($response->is_success) {
68                 my $err = $response->message;
69                 die "Could not retrieve $src: $err";
70         }
71
72         my $mod;
73         for (split /\n+/, $response->decoded_content) {
74                 s/^\s+//; # ignore whitespace at start
75                 next if /^#/;
76                 if (/^module (\S+) (\S+) (\S+)/) {
77                         my($name, $ver, $url) = ($1,$2,$3);
78                         if ($modules{$name}{$ver}) {
79                                 my $origsrc = $modules{$name}{$ver}{from};
80                                 warn "Overriding module $name $ver defined from $origsrc with one from $src";
81                         }
82                         $mod = {
83                                 from => $src,
84                                 url => $url,
85                                 depends => [],
86                                 conflicts => [],
87                         };
88                         $modules{$name}{$ver} = $mod;
89                 } elsif (/^depends (.*)/) {
90                         push @{$mod->{depends}}, $1;
91                 } elsif (/^conflicts (.*)/) {
92                         push @{$mod->{conflicts}}, $1;
93                 } elsif (/^description (.*)/) {
94                         $mod->{description} = $1;
95                 } elsif (/^mask (.*)/) {
96                         $mod->{mask} = $1;
97                 } elsif (m#^source (http://\S+)#) {
98                         parse_url $1;
99                 } else {
100                         print "Unknown line in $src: $_\n";
101                 }
102         }
103 }
104
105 # hash of installed module versions from our mini-database, key (m_foobar) to version (00abacca..).
106 my %mod_versions = read_config_file '.modulemanager';
107
108 # useless helper stub
109 sub getmodversion {
110         my ($file) = @_;
111         return $mod_versions{$file};
112 }
113
114 # read in external URL sources
115 open SRC, 'sources.list' or die "Could not open sources.list: $!";
116 while (<SRC>) {
117         next if /^\s*#/;
118         parse_url($_);
119 }
120 close SRC;
121
122 # determine core version
123 `./src/version.sh` =~ /InspIRCd-([0-9.]+)/ or die "Cannot determine inspircd version";
124 $installed{core} = $1;
125 for my $mod (keys %modules) {
126         MODVER: for my $mver (keys %{$modules{$mod}}) {
127                 for my $dep (@{$modules{$mod}{$mver}{depends}}) {
128                         next unless $dep =~ /^core (.*)/;
129                         if (!ver_in_range($installed{core}, $1)) {
130                                 delete $modules{$mod}{$mver};
131                                 next MODVER;
132                         }
133                 }
134         }
135         delete $modules{$mod} unless %{$modules{$mod}};
136 }
137 $modules{core}{$1} = {
138         url => 'NONE',
139         depends => [],
140         conflicts => [],
141         from => 'local file',
142 };
143
144 # set up core module list
145 for my $modname (<src/modules/m_*.cpp>) {
146         my $mod = basename($modname, '.cpp');
147         my $ver = getmodversion($mod) || '0.0';
148         $ver =~ s/\$Rev: (.*) \$/$1/; # for storing revision in SVN
149         $installed{$mod} = $ver;
150         next if $modules{$mod}{$ver};
151         $modules{$mod}{$ver} = {
152                 url => 'NONE',
153                 depends => [],
154                 conflicts => [],
155                 from => 'local file',
156         };
157 }
158
159 my %todo = %installed;
160
161 sub ver_cmp {
162         ($a,$b) = @_ if @_;
163
164         if ($a !~ /^[0-9.]+$/ or $b !~ /^[0-9.]+$/)
165         {
166                 # not a valid version number, don't try to sort
167                 return $a ne $b;
168         }
169
170         # else it's probably a numerical type version.. i.e. 1.0
171         my @a = split /\./, $a;
172         my @b = split /\./, $b;
173         push @a, 0 while $#a < $#b;
174         push @b, ($_[2] || 0) while $#b < $#a;
175         for my $i (0..$#a) {
176                 my $d = $a[$i] <=> $b[$i];
177                 return $d if $d;
178         }
179         return 0;
180 }
181
182 sub ver_in_range {
183         my($ver, $range) = @_;
184         return 1 unless defined $range;
185         my($l,$h) = ($range, $range);
186         if ($range =~ /(.*)-(.*)/) {
187                 ($l,$h) = ($1,$2);
188         }
189         return 0 if $l && ver_cmp($ver, $l) < 0;
190         return 0 if $h && ver_cmp($ver, $h, 9999) > 0;
191         return 1;
192 }
193
194 sub find_mod_in_range {
195         my($mod, $vers, $force) = @_;
196         my @versions = keys %{$modules{$mod}};
197         @versions = sort { -ver_cmp() } @versions;
198         for my $ver (@versions) {
199                 next if $modules{$mod}{$ver}{mask} && !$force;
200                 return $ver if ver_in_range($ver, $vers);
201         }
202         return undef;
203 }
204
205 sub resolve_deps {
206         my($trial) = @_;
207         my $tries = 100;
208         my $changes = 'INIT';
209         my $fail = undef;
210         while ($changes && $tries) {
211                 $tries--;
212                 $changes = '';
213                 $fail = undef;
214                 my @modsnow = sort keys %todo;
215                 for my $mod (@modsnow) {
216                         my $ver = $todo{$mod};
217                         my $info = $modules{$mod}{$ver} or die "no dependency information on $mod $ver";
218                         for my $dep (@{$info->{depends}}) {
219                                 $dep =~ /^(\S+)(?: (\S+))?/ or die "Bad dependency $dep from $info->{from}";
220                                 my($depmod, $depvers) = ($1,$2);
221                                 next if $todo{$depmod} && ver_in_range($todo{$depmod}, $depvers);
222                                 # need to install a dependency
223                                 my $depver = find_mod_in_range($depmod, $depvers);
224                                 if (defined $depver) {
225                                         $todo{$depmod} = $depver;
226                                         $changes .= " $mod-$ver->$depmod-$depver";
227                                 } else {
228                                         $fail ||= "Could not find module $depmod $depvers required by $mod $ver";
229                                 }
230                         }
231                         for my $dep (@{$info->{conflicts}}) {
232                                 $dep =~ /^(\S+)(?: (\S+))?/ or die "Bad dependency $dep from $info->{from}";
233                                 my($depmod, $depvers) = ($1,$2);
234                                 next unless $todo{$depmod} && ver_in_range($todo{$depmod}, $depvers);
235                                 # if there are changes this round, maybe the conflict won't come up after they are resolved.
236                                 $fail ||= "Cannot install: module $mod ($ver) conflicts with $depmod version $todo{$depmod}";
237                         }
238                 }
239         }
240         if ($trial) {
241                 return !($changes || $fail);
242         }
243         if ($changes) {
244                 print "Infinite dependency loop:$changes\n";
245                 exit 1;
246         }
247         if ($fail) {
248                 print "$fail\n";
249                 exit 1;
250         }
251 }
252
253 my $action = $#ARGV >= 0 ? lc shift @ARGV : 'help';
254
255 if ($action eq 'install') {
256         for my $mod (@ARGV) {
257                 my $vers = $mod =~ s/=([-0-9.]+)// ? $1 : undef;
258                 $mod = lc $mod;
259                 unless ($modules{$mod}) {
260                         print "Cannot find module $mod\n";
261                         exit 1;
262                 }
263                 my $ver = find_mod_in_range($mod, $vers, $vers ? 1 : 0);
264                 unless ($ver) {
265                         print "Cannot find suitable version of $mod\n";
266                         exit 1;
267                 }
268                 $todo{$mod} = $ver;
269         }
270 } elsif ($action eq 'upgrade') {
271         my @installed = sort keys %installed;
272         for my $mod (@installed) {
273                 next unless $mod =~ /^m_/;
274                 my %saved = %todo;
275                 $todo{$mod} = find_mod_in_range($mod);
276                 if (!resolve_deps(1)) {
277                         %todo = %saved;
278                 }
279         }
280 } elsif ($action eq 'list') {
281         my @all = sort keys %modules;
282         for my $mod (@all) {
283                 my @vers = sort { ver_cmp() } keys %{$modules{$mod}};
284                 my $desc = '';
285                 for my $ver (@vers) {
286                         # latest defined description wins
287                         $desc = $modules{$mod}{$ver}{description} || $desc;
288                 }
289                 next if @vers == 1 && $modules{$mod}{$vers[0]}{url} eq 'NONE';
290                 my $instver = $installed{$mod} || '';
291                 my $vers = join ' ', map { $_ eq $instver ? "\e[1m$_\e[m" : $_ } @vers;
292                 print "$mod ($vers) - $desc\n";
293         }
294 } else {
295         print <<ENDUSAGE
296 Use: $0 <action> <args>
297 Action is one of the following
298  install   install new modules
299  upgrade   upgrade installed modules
300  list      lists available modules
301
302 For installing a package, specify its name or name=version to force the
303 installation of a specific version.
304 ENDUSAGE
305 ;exit 1;
306 }
307
308 resolve_deps(0);
309
310 $| = 1; # immediate print of lines without \n
311
312 print "Processing changes for $action...\n";
313 for my $mod (keys %installed) {
314         next if $todo{$mod};
315         print "Uninstalling $mod $installed{$mod}\n";
316         unlink "src/modules/$mod.cpp";
317 }
318
319 my $count = scalar keys %todo;
320 print "Checking $count items...\n";
321 for my $mod (sort keys %todo) {
322         my $ver = $todo{$mod};
323         my $oldver = $installed{$mod};
324         if ($modules{$mod}{$ver}{mask}) {
325                 print "Module $mod $ver is masked: $modules{$mod}{$ver}{mask}\n";
326         }
327         next if $oldver && $oldver eq $ver;
328         my $url = $modules{$mod}{$ver}{url};
329         if ($oldver) {
330                 print "Upgrading $mod from $oldver to $ver using $url"
331         } else {
332                 print "Installing $mod $ver from $url";
333         }
334         $mod_versions{$mod} = $ver;
335
336         my $ua = LWP::UserAgent->new(ssl_opts => { verify_hostname => 0 });
337         my $response = $ua->get($url);
338
339         if ($response->is_success) {
340                 open(MF, ">src/modules/$mod.cpp") or die "\nFilesystem not writable: $!";
341                 print MF $response->decoded_content;
342                 close(MF);
343                 print " - done\n";
344         } else {
345                 printf "\nHTTP %s: %s\n", $response->code, $response->message;
346         }
347 }
348
349 # write database of installed versions
350 write_config_file '.modulemanager', %mod_versions;
351
352 print "Finished!\n";