1#!/usr/bin/perl -w
2# Copyright © 2008-2018 Jamie Zawinski <jwz@jwz.org>
3#
4# Permission to use, copy, modify, distribute, and sell this software and its
5# documentation for any purpose is hereby granted without fee, provided that
6# the above copyright notice appear in all copies and that both that
7# copyright notice and this permission notice appear in supporting
8# documentation.  No representations are made about the suitability of this
9# software for any purpose.  It is provided "as is" without express or
10# implied warranty.
11#
12# This parses the .c and .xml files and makes sure they are in sync: that
13# options are spelled the same, and that all the numbers are in sync.
14#
15# It also converts the hacks/config/ XML files into the Android XML files.
16#
17# Created:  1-Aug-2008.
18
19require 5;
20use diagnostics;
21use strict;
22
23my $progname = $0; $progname =~ s@.*/@@g;
24my ($version) = ('$Revision: 1.28 $' =~ m/\s(\d[.\d]+)\s/s);
25
26my $verbose = 0;
27my $debug_p = 0;
28
29
30my $text_default_opts = '';
31foreach (qw(text-mode text-literal text-file text-url text-program)) {
32  my $s = $_; $s =~ s/-(.)/\U$1/g; $s =~ s/url/URL/si;
33  $text_default_opts .= "{\"-$_\", \".$s\", XrmoptionSepArg, 0},\n";
34}
35my $image_default_opts = '';
36foreach (qw(choose-random-images grab-desktop-images)) {
37  my $s = $_; $s =~ s/-(.)/\U$1/g;
38  $image_default_opts .= "{\"-$_\", \".$s\", XrmoptionSepArg, 0},\n";
39}
40my $xlockmore_default_opts = '';
41foreach (qw(count cycles delay ncolors size font)) {
42  $xlockmore_default_opts .= "{\"-$_\", \".$_\", XrmoptionSepArg, 0},\n";
43}
44$xlockmore_default_opts .=
45 "{\"-wireframe\", \".wireframe\", XrmoptionNoArg, \"true\"},\n" .
46 "{\"-3d\", \".use3d\", XrmoptionNoArg, \"true\"},\n" .
47 "{\"-no-3d\", \".use3d\", XrmoptionNoArg, \"false\"},\n";
48
49my $thread_default_opts =
50  "{\"-threads\",    \".useThreads\", XrmoptionNoArg, \"True\"},\n" .
51  "{\"-no-threads\", \".useThreads\", XrmoptionNoArg, \"False\"},\n";
52
53my $analogtv_default_opts = '';
54foreach (qw(color tint brightness contrast)) {
55  $analogtv_default_opts .= "{\"-tv-$_\", \".TV$_\", XrmoptionSepArg, 0},\n";
56}
57
58$analogtv_default_opts .= $thread_default_opts;
59
60
61
62# Returns two tables:
63# - A table of the default resource values.
64# - A table of "-switch" => "resource: value", or "-switch" => "resource: %"
65#
66sub parse_src($) {
67  my ($saver) = @_;
68  my $file = lc($saver) . ".c";
69
70  # kludge...
71  $file = 'apple2-main.c' if ($file eq 'apple2.c');
72  $file = 'sproingiewrap.c' if ($file eq 'sproingies.c');
73  $file = 'b_lockglue.c' if ($file eq 'bubble3d.c');
74  $file = 'polyhedra-gl.c' if ($file eq 'polyhedra.c');
75  $file = 'companion.c' if ($file eq 'companioncube.c');
76  $file = 'rd-bomb.c' if ($file eq 'rdbomb.c');
77
78  my $ofile = $file;
79  $file = "glx/$ofile"          unless (-f $file);
80  $file = "../hacks/$ofile"     unless (-f $file);
81  $file = "../hacks/glx/$ofile" unless (-f $file);
82  my $body = '';
83  open (my $in, '<', $file) || error ("$ofile: $!");
84  while (<$in>) { $body .= $_; }
85  close $in;
86  $file =~ s@^.*/@@;
87
88  my $xlockmore_p = 0;
89  my $thread_p = ($body =~ m/THREAD_DEFAULTS/);
90  my $analogtv_p = ($body =~ m/ANALOGTV_DEFAULTS/);
91  my $text_p = ($body =~ m/"textclient\.h"/);
92  my $grab_p = ($body =~ m/load_image_async/);
93
94  $body =~ s@/\*.*?\*/@@gs;
95  $body =~ s@^#\s*(if|ifdef|ifndef|elif|else|endif).*$@@gm;
96  $body =~ s/(THREAD|ANALOGTV)_(DEFAULTS|OPTIONS)(_XLOCK)?//gs;
97  $body =~ s/__extension__//gs;
98
99  print STDERR "$progname: $file: defaults:\n" if ($verbose > 2);
100  my %res_to_val;
101  if ($body =~ m/_defaults\s*\[\]\s*=\s*{(.*?)}\s*;/s) {
102    foreach (split (/,\s*\n/, $1)) {
103      s/^\s*//s;
104      s/\s*$//s;
105      next if m/^0?$/s;
106      my ($key, $val) = m@^\"([^:\s]+)\s*:\s*(.*?)\s*\"$@;
107      print STDERR "$progname: $file: unparsable: $_\n" unless $key;
108      $key =~ s/^[.*]//s;
109      $res_to_val{$key} = $val;
110      print STDERR "$progname: $file:   $key = $val\n" if ($verbose > 2);
111    }
112  } elsif ($body =~ m/\#\s*define\s*DEFAULTS\s*\\?\s*(.*?)\n[\n#]/s) {
113    $xlockmore_p = 1;
114    my $str = $1;
115    $str =~ s/\"\s*\\\n\s*\"//gs;
116    $str =~ m/^\s*\"(.*?)\"\s*\\?\s*$/ ||
117      error ("$file: unparsable defaults: $str");
118    $str = $1;
119    $str =~ s/\s*\\n\s*/\n/gs;
120    foreach (split (/\n/, $str)) {
121      my ($key, $val) = m@^([^:\s]+)\s*:\s*(.*?)\s*$@;
122      print STDERR "$progname: $file: unparsable: $_\n" unless $key;
123      $key =~ s/^[.*]//s;
124      $val =~ s/"\s*"\s*$//s;
125      $res_to_val{$key} = $val;
126      print STDERR "$progname: $file:   $key = $val\n" if ($verbose > 2);
127    }
128
129    while ($body =~ s/^#\s*define\s+(DEF_([A-Z\d_]+))\s+\"([^\"]+)\"//m) {
130      my ($key1, $key2, $val) = ($1, lc($2), $3);
131      $key2 =~ s/_(.)/\U$1/gs;  # "foo_bar" -> "fooBar"
132      $key2 =~ s/Rpm/RPM/;      # kludge
133      $res_to_val{$key2} = $val;
134      print STDERR "$progname: $file:   $key1 ($key2) = $val\n"
135        if ($verbose > 2);
136    }
137
138  } else {
139    error ("$file: no defaults");
140  }
141
142  $body =~ m/XSCREENSAVER_MODULE(_2)?\s*\(\s*\"([^\"]+)\"/ ||
143    error ("$file: no module name");
144  $res_to_val{progclass} = $2;
145  $res_to_val{doFPS} = 'false';
146  $res_to_val{textMode} = 'date';
147  $res_to_val{textLiteral} = '';
148  $res_to_val{textURL} =
149    'https://en.wikipedia.org/w/index.php?title=Special:NewPages&feed=rss';
150  $res_to_val{grabDesktopImages} = 'true';
151  $res_to_val{chooseRandomImages} = 'true';
152
153  print STDERR "$progname: $file:   progclass = $2\n" if ($verbose > 2);
154
155  print STDERR "$progname: $file: switches to resources:\n"
156    if ($verbose > 2);
157  my %switch_to_res;
158  $switch_to_res{'-fps'} = 'doFPS: true';
159  $switch_to_res{'-fg'}  = 'foreground: %';
160  $switch_to_res{'-bg'}  = 'background: %';
161  $switch_to_res{'-no-grab-desktop-images'}  = 'grabDesktopImages: false';
162  $switch_to_res{'-no-choose-random-images'}  = 'chooseRandomImages: false';
163
164  my ($ign, $opts) = ($body =~ m/(_options|\bopts)\s*\[\]\s*=\s*{(.*?)}\s*;/s);
165  if  ($xlockmore_p || $thread_p || $analogtv_p || $opts) {
166    $opts = '' unless $opts;
167    $opts .= ",\n$text_default_opts" if ($text_p);
168    $opts .= ",\n$image_default_opts" if ($grab_p);
169    $opts .= ",\n$xlockmore_default_opts" if ($xlockmore_p);
170    $opts .= ",\n$thread_default_opts" if ($thread_p);
171    $opts .= ",\n$analogtv_default_opts" if ($analogtv_p);
172
173    foreach (split (/,\s*\n/, $opts)) {
174      s/^\s*//s;
175      s/\s*$//s;
176      next if m/^$/s;
177      next if m/^\{\s*0\s*,/s;
178      my ($switch, $res, $type, $v0, $v1, $v2) =
179        m@^ \s* { \s * \"([^\"]+)\" \s* ,
180                  \s * \"([^\"]+)\" \s* ,
181                  \s * ([^\s]+)     \s* ,
182                  \s * (\"([^\"]*)\"|([a-zA-Z\d_]+)) \s* }@xi;
183      print STDERR "$progname: $file: unparsable: $_\n" unless $switch;
184      my $val = defined($v1) ? $v1 : $v2;
185      $val = '%' if ($type eq 'XrmoptionSepArg');
186      $res =~ s/^[.*]//s;
187      $res =~ s/^[a-z\d]+\.//si;
188      $switch =~ s/^\+/-no-/s;
189
190      $val = "$res: $val";
191      if (defined ($switch_to_res{$switch})) {
192        print STDERR "$progname: $file:   DUP! $switch = \"$val\"\n"
193          if ($verbose > 2);
194      } else {
195        $switch_to_res{$switch} = $val;
196        print STDERR "$progname: $file:   $switch = \"$val\"\n"
197          if ($verbose > 2);
198      }
199    }
200  } else {
201    error ("$file: no options");
202  }
203
204  return (\%res_to_val, \%switch_to_res);
205}
206
207my %video_dups;
208
209# Returns a list of:
210#    "resource = default value"
211# or "resource != non-default value"
212#
213# Also a hash of the simplified XML contents.
214#
215sub parse_xml($$$) {
216  my ($saver, $switch_to_res, $src_opts) = @_;
217
218  my $saver_title = undef;
219  my $gl_p = 0;
220  my $file = "config/" . lc($saver) . ".xml";
221  my $ofile = $file;
222  $file = "../hacks/$ofile" unless (-f $file);
223  my $body = '';
224  open (my $in, '<', $file) || error ("$ofile: $!");
225  while (<$in>) { $body .= $_; }
226  close $in;
227  $file =~ s@^.*/@@;
228
229  my @result = ();
230
231  $body =~ s@<xscreensaver-text\s*/?>@
232    <select id="textMode">
233      <option id="date"  _label="Display the date and time"/>
234      <option id="text"  _label="Display static text"
235        arg-set="-text-mode literal"/>
236      <option id="url"   _label="Display the contents of a URL"
237        arg-set="-text-mode url"/>
238    </select>
239    <string id="textLiteral" _label="Text to display" arg="-text-literal %"/>
240    <string id="textURL" _label="URL to display" arg="-text-url %"/>
241    @gs;
242
243  $body =~ s@<xscreensaver-image\s*/?>@
244    <boolean id="grabDesktopImages" _label="Grab screenshots"
245       arg-unset="-no-grab-desktop-images"/>
246    <boolean id="chooseRandomImages" _label="Use photo library"
247       arg-unset="-no-choose-random-images"/>
248    @gs;
249
250  $body =~ s/<!--.*?-->/ /gsi;
251
252  $body =~ s@(<(_description)>.*?</\2>)@{ $_ = $1; s/\n/\002/gs; $_; }@gsexi;
253
254  $body =~ s/\s+/ /gs;
255  $body =~ s/</\001</gs;
256  $body =~ s/\001(<option)/$1/gs;
257
258  my $video = undef;
259
260  my @widgets = ();
261
262  print STDERR "$progname: $file: options:\n" if ($verbose > 2);
263  foreach (split (m/\001/, $body)) {
264    next if (m/^\s*$/s);
265    my ($type, $args) = m@^<([?/]?[-_a-z]+)\b\s*(.*)$@si;
266    error ("$progname: $file: unparsable: $_") unless $type;
267    next if ($type =~ m@^/@);
268
269    my $ctrl = { type => $type };
270
271    if ($type =~ m/^( [hv]group |
272                      \?xml |
273                      command |
274                      file |
275                      xscreensaver-image |
276                      xscreensaver-updater
277                    )/sx) {
278      $ctrl = undef;
279
280    } elsif ($type eq '_description') {
281      $args =~ s/\002/\n/gs;
282      $args =~ s@^>\s*@@s;
283      $args =~ s/^\n*|\s*$//gs;
284      $ctrl->{text} = $args;
285
286    } elsif ($type eq 'screensaver') {
287      ($saver_title) = ($args =~ m/\b_label\s*=\s*\"([^\"]+)\"/s);
288      ($gl_p) = ($args =~ m/\bgl="?yes/s);
289      my $s = $saver_title;
290      $s =~ s/\s+//gs;
291      my $val = "progclass = $s";
292      push @result, $val;
293      print STDERR "$progname: $file:   name:    $saver_title\n"
294        if ($verbose > 2);
295      $ctrl = undef;
296
297    } elsif ($type eq 'video') {
298      error ("$file: multiple videos") if $video;
299      ($video) = ($args =~ m/\bhref="(.*?)"/);
300      error ("$file: unparsable video") unless $video;
301      error ("$file: unparsable video URL")
302        unless ($video =~ m@^https?://www\.youtube\.com/watch\?v=[^?&]+$@s);
303      $ctrl = undef;
304
305    } elsif ($type eq 'select') {
306      $args =~ s/</\001</gs;
307      my @opts = split (/\001/, $args);
308      shift @opts;
309      my $unset_p = 0;
310      my $this_res = undef;
311      my @menu = ();
312      foreach (@opts) {
313        error ("$file: unparsable option: $_") unless (m/^<option\s/);
314
315        my %item;
316        my $opt = $_;
317        $opt =~ s@^<option\s+@@s;
318        $opt =~ s@[?/]>\s*$@@s;
319        while ($opt =~ s/^\s*([^\s]+)\s*=\s*"(.*?)"\s*(.*)/$3/s) {
320          my ($k, $v) = ($1, $2);
321          $item{$k} = $v;
322        }
323
324        error ("unparsable XML option line: $_ [$opt]") if ($opt);
325        push @menu, \%item;
326
327        my ($set) = $item{'arg-set'};
328        if ($set) {
329          my ($set2, $val) = ($set =~ m/^(.*?) (.*)$/s);
330          $set = $set2 if ($set2);
331          my ($res) = $switch_to_res->{$set};
332          error ("$file: no resource for select switch \"$set\"") unless $res;
333
334          my ($res2, $val2) = ($res =~ m/^(.*?): (.*)$/s);
335          error ("$file: unparsable select resource: $res") unless $res2;
336          $res = $res2;
337          $val = $val2 unless ($val2 eq '%');
338          $item{value} = $val;
339
340          error ("$file: mismatched resources: $res vs $this_res")
341            if (defined($this_res) && $this_res ne $res);
342          $this_res = $res;
343
344          $val = "$res != $val";
345          push @result, $val;
346          print STDERR "$progname: $file:   select:  $val\n" if ($verbose > 2);
347
348        } else {
349          error ("$file: multiple default options: $set") if ($unset_p);
350          $unset_p++;
351        }
352      }
353      $ctrl->{resource} = $this_res;
354      $ctrl->{default} = $src_opts->{$this_res};
355      $ctrl->{menu} = \@menu;
356
357    } else {
358
359      my $rest = $args;
360      $rest =~ s@[/?]*>\s*$@@s;
361      while ($rest =~ s/^\s*([^\s]+)\s*=\s*"(.*?)"\s*(.*)/$3/s) {
362        my ($k, $v) = ($1, $2);
363        $ctrl->{$k} = $v;
364      }
365      error ("unparsable XML line: $args [$rest]") if ($rest);
366
367      if ($type eq 'number') {
368        my ($arg) = $ctrl->{arg};
369        my ($val) = $ctrl->{default};
370        $val = '' unless defined($val);
371
372        my $switch = $arg;
373        $switch =~ s/\s+.*$//;
374        my ($res) = $switch_to_res->{$switch};
375        error ("$file: no resource for $type switch \"$arg\"") unless $res;
376
377        $res =~ s/: \%$//;
378        error ("$file: unparsable value: $res") if ($res =~ m/:/);
379        $ctrl->{resource} = $res;
380
381        $val = "$res = $val";
382        push @result, $val;
383        print STDERR "$progname: $file:   number:  $val\n" if ($verbose > 2);
384
385      } elsif ($type eq 'boolean') {
386        my ($set)   = $ctrl->{'arg-set'};
387        my ($unset) = $ctrl->{'arg-unset'};
388        my ($arg) = $set || $unset || error ("$file: unparsable: $args");
389        my ($res) = $switch_to_res->{$arg};
390          error ("$file: no resource for boolean switch \"$arg\"") unless $res;
391
392        my ($res2, $val) = ($res =~ m/^(.*?): (.*)$/s);
393        error ("$file: unparsable boolean resource: $res") unless $res2;
394        $res = $res2;
395
396        $ctrl->{resource} = $res;
397        $ctrl->{convert} = 'invert' if ($val =~ m/off|false|no/i);
398        $ctrl->{default} = ($ctrl->{convert} ? 'true' : 'false');
399
400#       $val = ($set ? "$res != $val" : "$res = $val");
401        $val = "$res != $val";
402        push @result, $val;
403        print STDERR "$progname: $file:   boolean: $val\n" if ($verbose > 2);
404
405      } elsif ($type eq 'string') {
406        my ($arg) = $ctrl->{arg};
407
408        my $switch = $arg;
409        $switch =~ s/\s+.*$//;
410        my ($res) = $switch_to_res->{$switch};
411        error ("$file: no resource for $type switch \"$arg\"") unless $res;
412
413        $res =~ s/: \%$//;
414        error ("$file: unparsable value: $res") if ($res =~ m/:/);
415        $ctrl->{resource} = $res;
416        $ctrl->{default} = $src_opts->{$res};
417        my $val = "$res = %";
418        push @result, $val;
419        print STDERR "$progname: $file:   string:  $val\n" if ($verbose > 2);
420
421      } else {
422        error ("$file: unknown type \"$type\" for no arg");
423      }
424    }
425
426    push @widgets, $ctrl if $ctrl;
427  }
428
429#  error ("$file: no video") unless $video;
430  print STDERR "\n$file: WARNING: no video\n\n" unless $video;
431
432  if ($video && $video_dups{$video} &&
433      $video_dups{$video} ne $saver_title) {
434    print STDERR "\n$file: WARNING: $saver_title: dup video with " .
435      $video_dups{$video} . "\n";
436  }
437  $video_dups{$video} = $saver_title if ($video);
438
439  return ($saver_title, $gl_p, \@result, \@widgets);
440}
441
442
443sub check_config($) {
444  my ($saver) = @_;
445
446  # kludge
447  return 0 if ($saver =~ m/(-helper)$/);
448
449  my ($src_opts, $switchmap) = parse_src ($saver);
450  my ($saver_title, $gl_p, $xml_opts, $widgets) =
451    parse_xml ($saver, $switchmap, $src_opts);
452
453  my $failures = 0;
454  foreach my $claim (@$xml_opts) {
455    my ($res, $compare, $xval) = ($claim =~ m/^(.*) (=|!=) (.*)$/s);
456    error ("$saver: unparsable xml claim: $claim") unless $compare;
457
458    my $sval = $src_opts->{$res};
459    if ($res =~ m/^TV|^text-mode/) {
460      print STDERR "$progname: $saver: OK: skipping \"$res\"\n"
461        if ($verbose > 1);
462    } elsif (!defined($sval)) {
463      print STDERR "$progname: $saver: $res: not in source\n";
464    } elsif ($claim !~ m/ = %$/s &&
465             ($compare eq '!='
466              ? $sval eq $xval
467              : $sval ne $xval)) {
468      print STDERR "$progname: $saver: " .
469        "src has \"$res = $sval\", xml has \"$claim\"\n";
470      $failures++;
471    } elsif ($verbose > 1) {
472      print STDERR "$progname: $saver: OK: \"$res = $sval\" vs \"$claim\"\n";
473    }
474  }
475
476  # Now make sure the progclass in the source and XML also matches
477  # the XCode target name.
478  #
479  my $obd = "../OSX/build/Debug";
480  if (-d $obd) {
481    my $progclass = $src_opts->{progclass};
482    $progclass = 'DNAlogo' if ($progclass eq 'DNALogo');
483    my $f = (glob("$obd/$progclass.saver*"))[0];
484    if (!$f && $progclass ne 'Flurry') {
485      print STDERR "$progname: $progclass.saver does not exist\n";
486      $failures++;
487    }
488  }
489
490  print STDERR "$progname: $saver: OK\n"
491    if ($verbose == 1 && $failures == 0);
492
493  return $failures;
494}
495
496
497# Returns true if the two files differ (by running "cmp")
498#
499sub cmp_files($$) {
500  my ($file1, $file2) = @_;
501
502  my @cmd = ("cmp", "-s", "$file1", "$file2");
503  print STDERR "$progname: executing \"" . join(" ", @cmd) . "\"\n"
504    if ($verbose > 3);
505
506  system (@cmd);
507  my $exit_value  = $? >> 8;
508  my $signal_num  = $? & 127;
509  my $dumped_core = $? & 128;
510
511  error ("$cmd[0]: core dumped!") if ($dumped_core);
512  error ("$cmd[0]: signal $signal_num!") if ($signal_num);
513  return $exit_value;
514}
515
516
517sub diff_files($$) {
518  my ($file1, $file2) = @_;
519
520  my @cmd = ("diff",
521             "-U1",
522#            "-w",
523             "--unidirectional-new-file", "$file1", "$file2");
524  print STDERR "$progname: executing \"" . join(" ", @cmd) . "\"\n"
525    if ($verbose > 3);
526
527  system (@cmd);
528  my $exit_value  = $? >> 8;
529  my $signal_num  = $? & 127;
530  my $dumped_core = $? & 128;
531
532  error ("$cmd[0]: core dumped!") if ($dumped_core);
533  error ("$cmd[0]: signal $signal_num!") if ($signal_num);
534  return $exit_value;
535}
536
537
538# If the two files differ:
539#   mv file2 file1
540# else
541#   rm file2
542#
543sub rename_or_delete($$;$) {
544  my ($file, $file_tmp, $suffix_msg) = @_;
545
546  my $changed_p = cmp_files ($file, $file_tmp);
547
548  if ($changed_p && $debug_p) {
549    print STDOUT "\n" . ('#' x 79) . "\n";
550    diff_files ("$file", "$file_tmp");
551    $changed_p = 0;
552  }
553
554  if ($changed_p) {
555
556    if (!rename ("$file_tmp", "$file")) {
557      unlink "$file_tmp";
558      error ("mv $file_tmp $file: $!");
559    }
560    print STDERR "$progname: wrote $file" .
561      ($suffix_msg ? " $suffix_msg" : "") . "\n";
562
563  } else {
564    unlink "$file_tmp" || error ("rm $file_tmp: $!\n");
565    print STDERR "$file unchanged" .
566                 ($suffix_msg ? " $suffix_msg" : "") . "\n"
567        if ($verbose);
568    print STDERR "$progname: rm $file_tmp\n" if ($verbose > 2);
569  }
570}
571
572
573# Write the given body to the file, but don't alter the file's
574# date if the new content is the same as the existing content.
575#
576sub write_file_if_changed($$;$) {
577  my ($outfile, $body, $suffix_msg) = @_;
578
579  my $file_tmp = "$outfile.tmp";
580  open (my $out, '>', $file_tmp) || error ("$file_tmp: $!");
581  (print $out $body) || error ("$file_tmp: $!");
582  close $out || error ("$file_tmp: $!");
583  rename_or_delete ($outfile, $file_tmp, $suffix_msg);
584}
585
586
587# Read the template file and splice in the @KEYWORDS@ in the hash.
588#
589sub read_template($$) {
590  my ($file, $subs) = @_;
591  my $body = '';
592  open (my $in, '<', $file) || error ("$file: $!");
593  while (<$in>) { $body .= $_; }
594  close $in;
595
596  $body =~ s@/\*.*?\*/@@gs;  # omit comments
597  $body =~ s@//.*$@@gm;
598
599  foreach my $key (keys %$subs) {
600    my $val = $subs->{$key};
601    $body =~ s/@\Q$key\E@/$val/gs;
602  }
603
604  if ($body =~ m/(@[-_A-Z\d]+@)/s) {
605    error ("$file: unmatched: $1 [$body]");
606  }
607
608  $body =~ s/[ \t]+$//gm;
609  $body =~ s/(\n\n)\n+/$1/gs;
610  return $body;
611}
612
613
614# This is duplicated in OSX/update-info-plist.pl
615#
616sub munge_blurb($$$$) {
617  my ($filename, $name, $vers, $desc) = @_;
618
619  $desc =~ s/^([ \t]*\n)+//s;
620  $desc =~ s/\s*$//s;
621
622  # in case it's done already...
623  $desc =~ s@<!--.*?-->@@gs;
624  $desc =~ s/^.* version \d[^\n]*\n//s;
625  $desc =~ s/^From the XScreenSaver.*\n//m;
626  $desc =~ s@^https://www\.jwz\.org/xscreensaver.*\n@@m;
627  $desc =~
628       s/\nCopyright [^ \r\n\t]+ (\d{4})(-\d{4})? (.*)\.$/\nWritten $3; $1./s;
629  $desc =~ s/^\n+//s;
630
631  error ("$filename: description contains markup: $1")
632    if ($desc =~ m/([<>&][^<>&\s]*)/s);
633  error ("$filename: description contains ctl chars: $1")
634    if ($desc =~ m/([\000-\010\013-\037])/s);
635
636  error ("$filename: can't extract authors")
637    unless ($desc =~ m@^(.*)\nWritten by[ \t]+(.+)$@s);
638  $desc = $1;
639  my $authors = $2;
640  $desc =~ s/\s*$//s;
641
642  my $year = undef;
643  if ($authors =~ m@^(.*?)\s*[,;]\s+(\d\d\d\d)([-\s,;]+\d\d\d\d)*[.]?$@s) {
644    $authors = $1;
645    $year = $2;
646  }
647
648  error ("$filename: can't extract year") unless $year;
649  my $cyear = 1900 + ((localtime())[5]);
650  $year = "$cyear" unless $year;
651  if ($year && ! ($year =~ m/$cyear/)) {
652    $year = "$year-$cyear";
653  }
654
655  $authors =~ s/[.,;\s]+$//s;
656
657  # List me as a co-author on all of them, since I'm the one who
658  # did the OSX port, packaged it up, and built the executables.
659  #
660  my $curator = "Jamie Zawinski";
661  if (! ($authors =~ m/$curator/si)) {
662    if ($authors =~ m@^(.*?),? and (.*)$@s) {
663      $authors = "$1, $2, and $curator";
664    } else {
665      $authors .= " and $curator";
666    }
667  }
668
669  my $desc1 = ("$name, version $vers.\n\n" .		# savername.xml
670               $desc . "\n" .
671               "\n" .
672               "From the XScreenSaver collection: " .
673               "https://www.jwz.org/xscreensaver/\n" .
674               "Copyright \302\251 $year by $authors.\n");
675
676  my $desc2 = ("$name $vers,\n" .			# Info.plist
677               "\302\251 $year $authors.\n" .
678               #"From the XScreenSaver collection:\n" .
679               #"https://www.jwz.org/xscreensaver/\n" .
680               "\n" .
681               $desc .
682               "\n");
683
684  # unwrap lines, but only when it's obviously ok: leave blank lines,
685  # and don't unwrap if that would compress leading whitespace on a line.
686  #
687  $desc2 =~ s/^(From |https?:)/\n$1/gm;
688  1 while ($desc2 =~ s/([^\s])[ \t]*\n([^\s])/$1 $2/gs);
689  $desc2 =~ s/\n\n(From |https?:)/\n$1/gs;
690
691  return ($desc1, $desc2);
692}
693
694
695sub build_android(@) {
696  my (@savers) = @_;
697
698  my $package     = "org.jwz.xscreensaver";
699  my $project_dir = "xscreensaver";
700  my $xml_dir     = "$project_dir/res/xml";
701  my $values_dir  = "$project_dir/res/values";
702  my $java_dir    = "$project_dir/src/org/jwz/xscreensaver/gen";
703  my $gen_dir     = "gen";
704
705  my $xml_header = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
706
707  my $manifest = '';
708  my $daydream_java = '';
709  my $settings_java = '';
710  my $wallpaper_java = '';
711  my $fntable_h2 = '';
712  my $fntable_h3 = '';
713  my $arrays   = '';
714  my $strings  = '';
715  my %write_files;
716  my %string_dups;
717
718  my $vers;
719  {
720    my $file = "../utils/version.h";
721    my $body = '';
722    open (my $in, '<', $file) || error ("$file: $!");
723    while (<$in>) { $body .= $_; }
724    close $in;
725    ($vers) = ($body =~ m@ (\d+\.[0-9a-z]+) @s);
726    error ("$file: no version number") unless $vers;
727  }
728
729
730  foreach my $saver (@savers) {
731    next if ($saver =~ m/(-helper)$/);
732    $saver = 'rdbomb' if ($saver eq 'rd-bomb');
733
734    my ($src_opts, $switchmap) = parse_src ($saver);
735    my ($saver_title, $gl_p, $xml_opts, $widgets) =
736      parse_xml ($saver, $switchmap, $src_opts);
737
738    my $saver_class = "${saver_title}";
739    $saver_class =~ s/\s+//gs;
740    $saver_class =~ s/^([a-z])/\U$1/gs;  # upcase first letter
741
742    $saver_title =~ s/(.[a-z])([A-Z\d])/$1 $2/gs;	# Spaces in InterCaps
743    $saver_title =~ s/^(GL|RD)[- ]?(.)/$1 \U$2/gs;	# Space after "GL"
744    $saver_title =~ s/^Apple ?2$/Apple &#x5D;&#x5B;/gs;	# "Apple ]["
745    $saver_title =~ s/(m)oe(bius)/$1&#xF6;$2/gsi;	# &ouml;
746    $saver_title =~ s/(moir)e/$1&#xE9;/gsi;		# &eacute;
747    $saver_title =~ s/^([a-z])/\U$1/s;			# "M6502" for sorting
748
749    my $settings = '';
750
751    my $localize0 = sub($$) {
752      my ($key, $string) = @_;
753      $string =~ s@([\\\"\'])@\\$1@gs;		# backslashify
754      $string =~ s@\n@\\n@gs;			# quote newlines
755      $key =~ s@[^a-z\d_]+@_@gsi;		# illegal characters
756
757      my $old = $string_dups{$key};
758      error ("dup string: $key: \"$old\" != \"$string\"")
759        if (defined($old) && $old ne $string);
760      $string_dups{$key} = $string;
761
762      my $fmt = ($string =~ m/%/ ? ' formatted="false"' : '');
763      $strings .= "<string name=\"${key}\"$fmt>$string</string>\n"
764        unless defined($old);
765      return "\@string/$key";
766    };
767
768    $localize0->('app_name', 'XScreenSaver');
769
770    $settings .= ("<Preference\n" .
771                  "  android:key=\"${saver}_reset\"\n" .
772                  "  android:title=\"" .
773                      $localize0->('reset_to_defaults', 'Reset to defaults') .
774                     "\"\n" .
775                  " />\n");
776
777    my $daydream_desc = '';
778    foreach my $widget (@$widgets) {
779      my $type  = $widget->{type};
780      my $rsrc  = $widget->{resource};
781      my $label = $widget->{_label};
782      my $def   = $widget->{default};
783      my $invert_p = (($widget->{convert} || '') eq 'invert');
784
785      my $key   = "${saver}_$rsrc" if $rsrc;
786
787      #### The menus don't actually have titles on X11 or Cocoa...
788      $label = $widget->{resource} unless $label;
789
790      my $localize = sub($;$) {
791        my ($string, $suf) = @_;
792        $suf = 'title' unless $suf;
793        return $localize0->("${saver}_${rsrc}_${suf}", $string);
794      };
795
796      if ($type eq 'slider' || $type eq 'spinbutton') {
797
798        my $low        = $widget->{low};
799        my $high       = $widget->{high};
800        my $float_p    = $low =~ m/[.]/;
801        my $low_label  = $widget->{'_low-label'};
802        my $high_label = $widget->{'_high-label'};
803
804        $low_label  = $low  unless defined($low_label);
805        $high_label = $high unless defined($high_label);
806
807        ($low, $high) = ($high, $low)
808          if (($widget->{convert} || '') eq 'invert');
809
810        $settings .=
811          ("<$package.SliderPreference\n" .
812           "  android:layout=\"\@layout/slider_preference\"\n" .
813           "  android:key=\"${key}\"\n" .
814           "  android:title=\"" . $localize->($label) . "\"\n" .
815           "  android:defaultValue=\"$def\"\n" .
816           "  low=\"$low\"\n" .
817           "  high=\"$high\"\n" .
818           "  lowLabel=\""  . $localize->($low_label,  'low_label')  . "\"\n" .
819           "  highLabel=\"" . $localize->($high_label, 'high_label') . "\"\n" .
820           "  integral=\"" .($float_p ? 'false' : 'true'). "\" />\n");
821
822      } elsif ($type eq 'boolean') {
823
824        my $def = ($invert_p ? 'true' : 'false');
825        $settings .=
826          ("<CheckBoxPreference\n" .
827           "  android:key=\"${key}\"\n" .
828           "  android:title=\"" . $localize->($label) . "\"\n" .
829           "  android:defaultValue=\"$def\" />\n");
830
831      } elsif ($type eq 'select') {
832
833        $label =~ s/^(.)/\U$1/s;  # upcase first letter of menu title
834        $label =~ s/[-_]/ /gs;
835        $label =~ s/([a-z])([A-Z])/$1 $2/gs;
836        $def = '' unless defined ($def);
837        $settings .=
838          ("<ListPreference\n" .
839           "  android:key=\"${key}\"\n" .
840           "  android:title=\"" . $localize->($label, 'menu') . "\"\n" .
841           "  android:entries=\"\@array/${key}_entries\"\n" .
842           "  android:defaultValue=\"$def\"\n" .
843           "  android:entryValues=\"\@array/${key}_values\" />\n");
844
845        my $a1 = '';
846        foreach my $item (@{$widget->{menu}}) {
847          my $val = $item->{value};
848          if (! defined($val)) {
849            $val = $src_opts->{$widget->{resource}};
850            error ("$saver: no default resource in option menu " .
851                   $item->{_label})
852              unless defined($val);
853          }
854          $val =~ s@([\\\"\'])@\\$1@gs;		# backslashify
855          $a1 .= "  <item>$val</item>\n";
856        }
857
858        my $a2 = '';
859        foreach my $item (@{$widget->{menu}}) {
860          my $val = $item->{value};
861          $val = $src_opts->{$widget->{resource}} unless defined($val);
862          $a2 .= ("  <item>" . $localize->($item->{_label}, $val) .
863                  "</item>\n");
864        }
865
866        my $fmt1 = ($a1 =~ m/%/ ? ' formatted="false"' : '');
867        my $fmt2 = ($a2 =~ m/%/ ? ' formatted="false"' : '');
868        $arrays .= ("<string-array name=\"${key}_values\"$fmt1>\n" .
869                    $a1 .
870                    "</string-array>\n" .
871                    "<string-array name=\"${key}_entries\"$fmt2>\n" .
872                    $a2 .
873                    "</string-array>\n");
874
875      } elsif ($type eq 'string') {
876
877        $def =~ s/&/&amp;/gs;
878        $settings .=
879          ("<EditTextPreference\n" .
880           "  android:key=\"${key}\"\n" .
881           "  android:title=\"" . $localize->($label) . "\"\n" .
882           "  android:defaultValue=\"$def\" />\n");
883
884      } elsif ($type eq 'file') {
885
886      } elsif ($type eq '_description') {
887
888        $type = 'description';
889        $rsrc = $type;
890        my $desc = $widget->{text};
891        (undef, $desc) = munge_blurb ($saver, $saver_title, $vers, $desc);
892
893        # Lose the Wikipedia URLs.
894        $desc =~ s@https?:.*?\b(wikipedia|mathworld)\b[^\s]+[ \t]*\n?@@gm;
895        $desc =~ s/(\n\n)\n+/$1/s;
896        $desc =~ s/\s*$/\n\n\n/s;
897
898        $daydream_desc = $desc;
899
900        my ($year) = ($daydream_desc =~ m/\b((19|20)\d\d)\b/s);
901        error ("$saver: no year") unless $year;
902        $daydream_desc =~ s/^.*?\n\n//gs;
903        $daydream_desc =~ s/\n.*$//gs;
904        $daydream_desc = "$year: $daydream_desc";
905        $daydream_desc =~ s/^(.{72}).+$/$1.../s;
906
907        $settings .=
908          ("<Preference\n" .
909           "  android:icon=\"\@drawable/thumbnail\"\n" .
910           "  android:key=\"${saver}_${type}\"\n" .
911#           "  android:selectable=\"false\"\n" .
912           "  android:persistent=\"false\"\n" .
913           "  android:layout=\"\@layout/preference_blurb\"\n" .
914           "  android:summary=\"" . $localize->($desc) . "\">\n" .
915           "  <intent android:action=\"android.intent.action.VIEW\"\n" .
916           "    android:data=\"https://www.jwz.org/xscreensaver/\" />\n" .
917           "</Preference>\n");
918
919      } else {
920        error ("unhandled type: $type");
921      }
922    }
923
924    my $heading = "XScreenSaver: $saver_title";
925
926    $settings =~ s/^/  /gm;
927    $settings = ($xml_header .
928                 "<PreferenceScreen xmlns:android=\"" .
929                 "http://schemas.android.com/apk/res/android\"\n" .
930                 "  android:title=\"" .
931                 $localize0->("${saver}_settings_title", $heading) . "\">\n" .
932                 $settings .
933                 "</PreferenceScreen>\n");
934
935    my $saver_underscore = $saver;
936    $saver_underscore =~ s/-/_/g;
937    $write_files{"$xml_dir/${saver_underscore}_settings.xml"} = $settings;
938
939    $manifest .= ("<service android:label=\"" .
940                     $localize0->("${saver_underscore}_saver_title",
941                                  $saver_title) .
942                     "\"\n" .
943                  "  android:summary=\"" .
944                       $localize0->("${saver_underscore}_saver_desc",
945                                    $daydream_desc) . "\"\n" .
946                  "  android:name=\".gen.Daydream\$$saver_class\"\n" .
947                  "  android:permission=\"android.permission" .
948                       ".BIND_DREAM_SERVICE\"\n" .
949                  "  android:exported=\"true\"\n" .
950                  "  android:icon=\"\@drawable/${saver_underscore}\">\n" .
951                  "  <intent-filter>\n" .
952                  "    <action android:name=\"android.service.dreams" .
953                        ".DreamService\" />\n" .
954                  "    <category android:name=\"android.intent.category" .
955                        ".DEFAULT\" />\n" .
956                  "  </intent-filter>\n" .
957                  "  <meta-data android:name=\"android.service.dream\"\n" .
958                  "    android:resource=\"\@xml/${saver}_dream\" />\n" .
959                  "</service>\n" .
960                  "<service android:label=\"" .
961                     $localize0->("${saver_underscore}_saver_title",
962                                  $saver_title) .
963                     "\"\n" .
964                  "  android:summary=\"" .
965                       $localize0->("${saver_underscore}_saver_desc",
966                                    $daydream_desc) . "\"\n" .
967                  "  android:name=\".gen.Wallpaper\$$saver_class\"\n" .
968                  "  android:permission=\"android.permission" .
969                       ".BIND_WALLPAPER\">\n" .
970                  "  <intent-filter>\n" .
971                  "    <action android:name=\"android.service.wallpaper" .
972                        ".WallpaperService\" />\n" .
973                  "    <category android:name=\"android.intent.category" .
974                        ".DEFAULT\" />\n" . # TODO: Is the DEFAULT category needed?
975                  "  </intent-filter>\n" .
976                  "  <meta-data android:name=\"android.service.wallpaper\"\n" .
977                  "    android:resource=\"\@xml/${saver}_wallpaper\" />\n" .
978                  "</service>\n" .
979                  "<activity android:label=\"" .
980                   $localize0->("${saver}_settings_title", $heading) . "\"\n" .
981                  "  android:name=\"$package.gen.Settings\$$saver_class\"\n" .
982                  "  android:exported=\"true\">\n" .
983                  "</activity>\n"
984                 );
985
986    my $dream = ("<dream xmlns:android=\"" .
987                   "http://schemas.android.com/apk/res/android\"\n" .
988                 "  android:settingsActivity=\"" .
989                     "$package.gen.Settings\$$saver_class\" />\n");
990    $write_files{"$xml_dir/${saver_underscore}_dream.xml"} = $dream;
991
992    my $wallpaper = ("<wallpaper xmlns:android=\"" .
993                       "http://schemas.android.com/apk/res/android\"\n" .
994                     "  android:settingsActivity=\"" .
995                     "$package.gen.Settings\$$saver_class\"\n" .
996                     "  android:thumbnail=\"\@drawable/${saver_underscore}\" />\n");
997    $write_files{"$xml_dir/${saver_underscore}_wallpaper.xml"} = $wallpaper;
998
999    $daydream_java .=
1000      ("  public static class $saver_class extends $package.Daydream {\n" .
1001       "  }\n" .
1002       "\n");
1003
1004    $wallpaper_java .=
1005      ("  public static class $saver_class extends $package.Wallpaper {\n" .
1006       "  }\n" .
1007       "\n");
1008
1009    $settings_java .=
1010      ("  public static class $saver_class extends $package.Settings\n" .
1011       "    implements SharedPreferences.OnSharedPreferenceChangeListener {\n" .
1012       "  }\n" .
1013       "\n");
1014
1015    $fntable_h2 .= ",\n  " if $fntable_h2 ne '';
1016    $fntable_h3 .= ",\n  " if $fntable_h3 ne '';
1017
1018    $fntable_h2 .= "${saver}_xscreensaver_function_table";
1019    $fntable_h3 .= "{\"${saver}\", &${saver}_xscreensaver_function_table}";
1020  }
1021
1022  $arrays =~ s/^/  /gm;
1023  $arrays = ($xml_header .
1024             "<resources xmlns:xliff=\"" .
1025             "urn:oasis:names:tc:xliff:document:1.2\">\n" .
1026             $arrays .
1027             "</resources>\n");
1028
1029  $strings =~ s/^/  /gm;
1030  $strings = ($xml_header .
1031              "<resources>\n" .
1032              $strings .
1033              "</resources>\n");
1034
1035  $manifest .= "<activity android:name=\"$package.Settings\" />\n";
1036
1037  $manifest .= ("<activity android:name=\"" .
1038                "$package.Activity\"\n" .
1039                "  android:theme=\"\@android:style/Theme.Holo\"\n" .
1040                "  android:label=\"\@string/app_name\">\n" .
1041                "  <intent-filter>\n" .
1042                "    <action android:name=\"android.intent.action" .
1043                ".MAIN\" />\n" .
1044                "    <category android:name=\"android.intent.category" .
1045                ".LAUNCHER\" />\n" .
1046                "  </intent-filter>\n" .
1047                "  <intent-filter>\n" .
1048                "    <action android:name=\"android.intent.action" .
1049                ".VIEW\" />\n" .
1050                "    <category android:name=\"android.intent.category" .
1051                ".DEFAULT\" />\n" .
1052                "    <category android:name=\"android.intent.category" .
1053                ".BROWSABLE\" />\n" .
1054                "  </intent-filter>\n" .
1055                "</activity>\n");
1056
1057
1058  $manifest .= ("<activity android:name=\"" .
1059                "$package.TVActivity\"\n" .
1060                "  android:theme=\"\@android:style/Theme.Holo\"\n" .
1061                "  android:label=\"\@string/app_name\">\n" .
1062                "  <intent-filter>\n" .
1063                "    <action android:name=\"android.intent.action" .
1064                ".MAIN\" />\n" .
1065                "    <category android:name=\"android.intent.category" .
1066                ".LEANBACK_LAUNCHER\" />\n" .
1067                "  </intent-filter>\n" .
1068                "  <intent-filter>\n" .
1069                "    <action android:name=\"android.intent.action" .
1070                ".VIEW\" />\n" .
1071                "    <category android:name=\"android.intent.category" .
1072                ".DEFAULT\" />\n" .
1073                "    <category android:name=\"android.intent.category" .
1074                ".BROWSABLE\" />\n" .
1075                "  </intent-filter>\n" .
1076                "</activity>\n");
1077
1078
1079
1080
1081  # Android wants this to be an int
1082  my $versb = $vers;
1083  $versb =~ s/^(\d+)\.(\d+).*$/{ $1 * 10000 + $2 * 100 }/sex;
1084  $versb++ if ($versb == 53500); # Herp derp
1085
1086  $manifest =~ s/^/   /gm;
1087  $manifest = ($xml_header .
1088               "<manifest xmlns:android=\"" .
1089               "http://schemas.android.com/apk/res/android\"\n" .
1090               "  package=\"$package\"\n" .
1091               "  android:versionCode=\"$versb\"\n" .
1092               "  android:versionName=\"$vers\">\n" .
1093
1094               "  <uses-sdk android:minSdkVersion=\"16\"" .
1095               " android:targetSdkVersion=\"19\" />\n" .
1096
1097               "  <uses-feature android:glEsVersion=\"0x00010001\"\n" .
1098               "    android:required=\"true\" />\n" .
1099
1100               "  <uses-feature android:name=\"android.software.leanback\"\n" .
1101               "    android:required=\"false\" />\n" .
1102
1103               "  <uses-feature" .
1104               " android:name=\"android.hardware.touchscreen\"\n" .
1105               "    android:required=\"false\" />\n" .
1106
1107               "  <uses-permission android:name=\"" .
1108                   "android.permission.INTERNET\" />\n" .
1109               "  <uses-permission android:name=\"" .
1110                   "android.permission.READ_EXTERNAL_STORAGE\" />\n" .
1111
1112               "  <application android:icon=\"\@drawable/thumbnail\"\n" .
1113               "    android:banner=\"\@drawable/thumbnail\"\n" .
1114               "    android:label=\"\@string/app_name\"\n" .
1115               "    android:name=\".App\">\n" .
1116               $manifest .
1117               "  </application>\n" .
1118               "</manifest>\n");
1119
1120  $daydream_java = ("package $package.gen;\n" .
1121                    "\n" .
1122                    "import $package.jwxyz;\n" .
1123                    "\n" .
1124                    "public class Daydream {\n" .
1125                    $daydream_java .
1126                    "}\n");
1127
1128  $wallpaper_java = ("package $package.gen;\n" .
1129                     "\n" .
1130                     "import $package.jwxyz;\n" .
1131                     "\n" .
1132                     "public class Wallpaper {\n" .
1133                     $wallpaper_java .
1134                     "}\n");
1135
1136  $settings_java = ("package $package.gen;\n" .
1137                    "\n" .
1138                    "import android.content.SharedPreferences;\n" .
1139                    "\n" .
1140                    "public class Settings {\n" .
1141                    $settings_java .
1142                    "}\n");
1143
1144  $write_files{"$project_dir/AndroidManifest.xml"}     = $manifest;
1145  $write_files{"$values_dir/settings.xml"} = $arrays;
1146  $write_files{"$values_dir/strings.xml"}  = $strings;
1147  $write_files{"$java_dir/Daydream.java"}  = $daydream_java;
1148  $write_files{"$java_dir/Wallpaper.java"} = $wallpaper_java;
1149  $write_files{"$java_dir/Settings.java"}  = $settings_java;
1150
1151  my $fntable_h = ("extern struct xscreensaver_function_table\n" .
1152                   "  " . $fntable_h2 . ";\n" .
1153                   "\n" .
1154                   "static const struct function_table_entry" .
1155                   " function_table[] = {\n" .
1156                   "  " . $fntable_h3 . "\n" .
1157                   "};\n");
1158  $write_files{"$gen_dir/function-table.h"} = $fntable_h;
1159
1160
1161  $write_files{"$values_dir/attrs.xml"} =
1162    # This file doesn't actually have any substitutions in it, so it could
1163    # just be static, somewhere...
1164    # SliderPreference.java refers to this via "R.styleable.SliderPreference".
1165    ("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" .
1166     "<resources>\n" .
1167     "  <declare-styleable name=\"SliderPreference\">\n" .
1168     "    <attr name=\"android:summary\" />\n" .
1169     "  </declare-styleable>\n" .
1170     "</resources>\n");
1171
1172
1173  foreach my $file (sort keys %write_files) {
1174    my ($dir) = ($file =~ m@^(.*)/[^/]*$@s);
1175    system ("mkdir", "-p", $dir) if (! -d $dir && !$debug_p);
1176    my $body = $write_files{$file};
1177    $body = "// Generated by $progname\n$body"
1178      if ($file =~ m/\.(java|[chm])$/s);
1179    write_file_if_changed ($file, $body);
1180  }
1181
1182  # Unlink any .xml files from a previous run that shouldn't be there:
1183  # if a hack is removed from $ANDROID_HACKS in android/Makefile but
1184  # the old XML files remain behind, the build blows up.
1185  #
1186  foreach my $dd ($xml_dir, $gen_dir, $java_dir) {
1187    opendir (my $dirp, $dd) || error ("$dd: $!");
1188    my @files = readdir ($dirp);
1189    closedir $dirp;
1190    foreach my $f (sort @files) {
1191      next if ($f eq '.' || $f eq '..');
1192      $f = "$dd/$f";
1193      next if (defined ($write_files{$f}));
1194      if ($f =~ m/_(settings|wallpaper|dream)\.xml$/s ||
1195          $f =~ m/(Settings|Daydream)\.java$/s) {
1196        print STDERR "$progname: rm $f\n";
1197        unlink ($f) unless ($debug_p);
1198      } else {
1199        print STDERR "$progname: warning: unrecognised file: $f\n";
1200      }
1201    }
1202  }
1203}
1204
1205
1206sub error($) {
1207  my ($err) = @_;
1208  print STDERR "$progname: $err\n";
1209  exit 1;
1210}
1211
1212sub usage() {
1213  print STDERR "usage: $progname [--verbose] [--debug]" .
1214    " [--build-android] files ...\n";
1215  exit 1;
1216}
1217
1218sub main() {
1219  my $android_p = 0;
1220  my @files = ();
1221  while ($#ARGV >= 0) {
1222    $_ = shift @ARGV;
1223    if (m/^--?verbose$/) { $verbose++; }
1224    elsif (m/^-v+$/) { $verbose += length($_)-1; }
1225    elsif (m/^--?debug$/s) { $debug_p++; }
1226    elsif (m/^--?build-android$/s) { $android_p++; }
1227    elsif (m/^-./) { usage; }
1228    else { push @files, $_; }
1229#    else { usage; }
1230  }
1231
1232  usage unless ($#files >= 0);
1233  my $failures = 0;
1234  foreach my $file (@files) {
1235    $failures += check_config ($file);
1236  }
1237
1238  build_android (@files) if ($android_p);
1239
1240  exit ($failures);
1241}
1242
1243main();
1244