From http://www.jwz.org/xscreensaver/xscreensaver-5.37.tar.gz
[xscreensaver] / OSX / update-info-plist.pl
1 #!/usr/bin/perl -w
2 # Copyright © 2006-2017 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 # Updates the NAME.xml file of a .saver bundle to include the current year,
13 # version number, etc.  Also updates the Info.plist file to include the
14 # short documentation, authors, etc. in the Finder "Get Info" properties.
15 #
16 # This is invoked by a final "Shell Script" build action on each of the
17 # .saver targets in the XCode project.
18 #
19 # Created:  8-Mar-2006.
20
21 require 5;
22 #use diagnostics;       # Fails on some MacOS 10.5 systems
23 use strict;
24 use IPC::Open3;
25 use IO::Uncompress::Gunzip qw(gunzip $GunzipError);
26 use IO::Compress::Gzip qw(gzip $GzipError);
27
28 my ($exec_dir, $progname) = ($0 =~ m@^(.*?)/([^/]+)$@);
29
30 my ($version) = ('$Revision: 1.47 $' =~ m/\s(\d[.\d]+)\s/s);
31
32 $ENV{PATH} = "/usr/local/bin:$ENV{PATH}";   # for seticon
33 $ENV{PATH} = "/opt/local/bin:$ENV{PATH}";   # for macports wget
34
35 my $thumbdir = 'build/screenshots';
36
37
38
39 my $verbose = 1;
40
41 sub convert_plist($$) {
42   my ($data, $to_binary_p) = @_;
43   my $is_binary_p = ($data =~ m/^bplist/s);
44   if ($data && (!$is_binary_p) != (!$to_binary_p)) {
45     print STDERR "$progname: converting plist\n" if ($verbose > 2);
46     my $which = ($to_binary_p ? 'binary1' : 'xml1');
47     my @cmd = ('plutil', '-convert', $which, '-s', '-o', '-', '-');
48     my $pid = open3 (my $in, my $out, undef, @cmd) ||
49       error ("pipe: $cmd[0]: $!");
50     error ("$cmd[0]: $!") unless $pid;
51     print $in $data;
52     close $in;
53     local $/ = undef;  # read entire file
54     $data = <$out>;
55     close $out;
56     waitpid ($pid, 0);
57     if ($?) {
58       my $exit_value  = $? >> 8;
59       my $signal_num  = $? & 127;
60       my $dumped_core = $? & 128;
61       error ("$cmd[0]: core dumped!") if ($dumped_core);
62       error ("$cmd[0]: signal $signal_num!") if ($signal_num);
63       error ("$cmd[0]: exited with $exit_value!") if ($exit_value);
64     }
65   }
66   return $data;
67 }
68
69
70 sub read_info_plist($) {
71   my ($app_dir) = @_;
72   my $file  = "$app_dir/Contents/Info.plist";
73   my $file2 = "$app_dir/Info.plist";
74   $file =~ s@/+@/@g;
75   my $in;
76   if (open ($in, '<', $file)) {
77   } elsif (open ($in, '<', $file2)) {
78     $file = $file2;
79   } else {
80     error ("$file: $!");
81   }
82   print STDERR "$progname: read $file\n" if ($verbose > 2);
83   local $/ = undef;  # read entire file
84   my $body = <$in>;
85   close $in;
86
87   $body = convert_plist ($body, 0);  # convert to xml plist
88   return ($file, $body);
89 }
90
91
92 sub read_saver_xml($) {
93   my ($app_dir) = @_;
94   error ("$app_dir: no name") 
95     unless ($app_dir =~ m@/([^/.]+).(app|saver)/?$@x);
96   my $name  = $1;
97
98   return () if ($name eq 'XScreenSaver');
99   return () if ($name eq 'SaverTester');
100   return () if ($name eq 'XScreenSaverUpdater');
101
102   my $file  = "$app_dir/Contents/Resources/" . lc($name) . ".xml";
103   my $file2 = "$app_dir/" . lc($name) . ".xml";
104   my $file3 = "$app_dir/Contents/PlugIns/$name.saver/Contents/Resources/" .
105               lc($name) . ".xml";
106   $file =~ s@/+@/@g;
107   my $in;
108   if (open ($in, '<', $file)) {
109   } elsif (open ($in, '<', $file2)) { $file = $file2;
110   } elsif (open ($in, '<', $file3)) { $file = $file3;
111   } else {
112     error ("$file: $!");
113   }
114   print STDERR "$progname: read $file\n" if ($verbose > 2);
115   local $/ = undef;  # read entire file
116   my $body = <$in>;
117   close $in;
118
119   # Uncompress the XML if it is compressed.
120   my $body2 = '';
121   gunzip (\$body, \$body2) || error ("$app_dir: xml gunzip: $GunzipError");
122   my $was_compressed_p = ($body ne $body2);
123   return ($file, $body2, $was_compressed_p);
124 }
125
126
127 # This is duplicated in hacks/check-configs.pl for Android
128 #
129 sub munge_blurb($$$$) {
130   my ($filename, $name, $vers, $desc) = @_;
131
132   $desc =~ s/^([ \t]*\n)+//s;
133   $desc =~ s/\s*$//s;
134
135   # in case it's done already...
136   $desc =~ s@<!--.*?-->@@gs;
137   $desc =~ s/^.* version \d[^\n]*\n//s;
138   $desc =~ s/^From the XScreenSaver.*\n//m;
139   $desc =~ s@^https://www\.jwz\.org/xscreensaver.*\n@@m;
140   $desc =~
141        s/\nCopyright [^ \r\n\t]+ (\d{4})(-\d{4})? (.*)\.$/\nWritten $3; $1./s;
142   $desc =~ s/^\n+//s;
143
144   error ("$filename: description contains markup: $1")
145     if ($desc =~ m/([<>&][^<>&\s]*)/s);
146   error ("$filename: description contains ctl chars: $1")
147     if ($desc =~ m/([\000-\010\013-\037])/s);
148
149   error ("$filename: can't extract authors")
150     unless ($desc =~ m@^(.*)\nWritten by[ \t]+(.+)$@s);
151   $desc = $1;
152   my $authors = $2;
153   $desc =~ s/\s*$//s;
154
155   my $year = undef;
156   if ($authors =~ m@^(.*?)\s*[,;]\s+(\d\d\d\d)([-\s,;]+\d\d\d\d)*[.]?$@s) {
157     $authors = $1;
158     $year = $2;
159   }
160
161   error ("$filename: can't extract year") unless $year;
162   my $cyear = 1900 + ((localtime())[5]);
163   $year = "$cyear" unless $year;
164   if ($year && ! ($year =~ m/$cyear/)) {
165     $year = "$year-$cyear";
166   }
167
168   $authors =~ s/[.,;\s]+$//s;
169
170   # List me as a co-author on all of them, since I'm the one who
171   # did the OSX port, packaged it up, and built the executables.
172   #
173   my $curator = "Jamie Zawinski";
174   if (! ($authors =~ m/$curator/si)) {
175     if ($authors =~ m@^(.*?),? and (.*)$@s) {
176       $authors = "$1, $2, and $curator";
177     } else {
178       $authors .= " and $curator";
179     }
180   }
181
182   my $desc1 = ("$name, version $vers.\n\n" .            # savername.xml
183                $desc . "\n" .
184                "\n" . 
185                "From the XScreenSaver collection: " .
186                "https://www.jwz.org/xscreensaver/\n" .
187                "Copyright \302\251 $year by $authors.\n");
188
189   my $desc2 = ("$name $vers,\n" .                       # Info.plist
190                "\302\251 $year $authors.\n" .
191                "From the XScreenSaver collection:\n" .
192                "https://www.jwz.org/xscreensaver/\n" .
193                "\n" .
194                $desc .
195                "\n");
196
197   # unwrap lines, but only when it's obviously ok: leave blank lines,
198   # and don't unwrap if that would compress leading whitespace on a line.
199   #
200   $desc2 =~ s/^(From |https?:)/\n$1/gm;
201   1 while ($desc2 =~ s/([^\s])[ \t]*\n([^\s])/$1 $2/gs);
202   $desc2 =~ s/\n\n(From |https?:)/\n$1/gs;
203
204   return ($desc1, $desc2);
205 }
206
207
208 sub update_saver_xml($$) {
209   my ($app_dir, $vers) = @_;
210   my ($filename, $body, $was_compressed_p) = read_saver_xml ($app_dir);
211   my $obody = $body;
212
213   return () unless defined ($filename);
214
215   $body =~ m@<screensaver[^<>]*?[ \t]_label=\"([^\"]+)\"@m ||
216     error ("$filename: no name label");
217   my $name = $1;
218
219   $body =~ m@<_description>(.*?)</_description>@s ||
220     error ("$filename: no description tag");
221   my $desc = $1;
222
223   error ("$filename: description contains non-ASCII and is not UTF-8: $1")
224     if ($body !~ m/\Q<?xml version="1.0" encoding="UTF-8"/s &&
225         $desc =~ m/([^\000-\176])/s);
226
227   my ($desc1, $desc2) = munge_blurb ($filename, $name, $vers, $desc);
228
229   $body =~ s@(<_description>)(.*?)(</_description>)@$1$desc1$3@s;
230
231   # NSXMLParser doesn't seem to work properly on Latin1 XML documents,
232   # so we convert these to UTF8 when embedding them in the .saver bundle.
233   $body =~ s@encoding="ISO-8859-1"@encoding="UTF-8"@gsi;
234
235   if ($obody eq $body && $was_compressed_p) {
236     print STDERR "$progname: $filename: unchanged\n" if ($verbose > 1);
237   } else {
238
239     # Gzip the XML.
240     my $body2 = '';
241     gzip (\$body, \$body2) || error ("$app_dir: xml gzip: $GzipError");
242     $body = $body2;
243
244     my $file_tmp = "$filename.tmp";
245     open (my $out, '>:raw', $file_tmp) || error ("$file_tmp: $!");
246     print $out $body || error ("$file_tmp: $!");
247     close $out || error ("$file_tmp: $!");
248
249     if (!rename ("$file_tmp", "$filename")) {
250       unlink "$file_tmp";
251       error ("mv \"$file_tmp\" \"$filename\": $!");
252     }
253     print STDERR "$progname: wrote $filename\n" if ($verbose);
254   }
255
256   return ($desc1, $desc2);
257 }
258
259
260 sub compress_all_xml_files($) {
261   my ($dir) = @_;
262   opendir (my $dirp, $dir) || error ("$dir: $!");
263   my @files = readdir ($dirp);
264   closedir $dirp;
265   foreach my $f (sort @files) {
266     next unless ($f =~ m/\.xml$/si);
267     my $filename = "$dir/$f";
268     open (my $in, '<', $filename) || error ("$filename: $!");
269     print STDERR "$progname: read $filename\n" if ($verbose > 2);
270     local $/ = undef;  # read entire file
271     my $body = <$in>;
272     close $in;
273
274     if ($body =~ m/^<\?xml/s) {
275       my $body2 = '';
276       gzip (\$body, \$body2) || error ("$filename: xml gzip: $GzipError");
277       $body = $body2;
278       my $file_tmp = "$filename.tmp";
279       open (my $out, '>:raw', $file_tmp) || error ("$file_tmp: $!");
280       print $out $body || error ("$file_tmp: $!");
281       close $out || error ("$file_tmp: $!");
282
283       if (!rename ("$file_tmp", "$filename")) {
284         unlink "$file_tmp";
285         error ("mv \"$file_tmp\" \"$filename\": $!");
286       }
287       print STDERR "$progname: compressed $filename\n" if ($verbose);
288     } elsif ($verbose > 2) {
289       print STDERR "$filename: already compressed\n";
290     }
291   }
292 }
293
294
295 sub set_plist_key($$$$) {
296   my ($filename, $body, $key, $val) = @_;
297
298   if ($body =~ m@^(.*
299                   \n\t<key>$key</key>
300                   \n\t<string>)([^<>]*)(</string>
301                   .*)$@xs) {
302 #    print STDERR "$progname: $filename: $key was: $2\n" if ($verbose);
303     $body = $1 . $val . $3;
304   } else {
305     error ("$filename: unparsable")
306       unless ($body =~ m@^(.*)(\n</dict>\n</plist>\n)$@s);
307     $body = ($1 .
308              "\n\t<key>$key</key>" .
309              "\n\t<string>$val</string>" .
310              $2);
311   }
312
313   return $body;
314 }
315
316
317 sub set_icon($) {
318   my ($app_dir) = @_;
319   $app_dir =~ s@/+$@@s;
320
321   my $icon = ($app_dir =~ m/\.saver$/ ? 'XScreenSaver' : 'SaverRunner');
322   $icon = "$app_dir/../../../$icon.icns";
323   my @cmd = ("$app_dir/../../../seticon.pl", "-d", $icon, $app_dir);
324   print STDERR "$progname: exec: " . join(' ', @cmd) . "\n"
325     if ($verbose > 1);
326   system (@cmd);
327 }
328
329
330 sub set_thumb($) {
331   my ($app_dir) = @_;
332
333   return unless ($app_dir =~ m@\.saver/?$@s);
334
335   my $name = $app_dir;
336   $name =~ s@^.*/@@s;
337   $name =~ s@\..*?$@@s;
338   $name = lc($name);
339
340   $name = 'rd-bomb' if ($name eq 'rdbomb');  # sigh
341
342   if (! -f "$thumbdir/$name.png") {
343     system ("make", "$thumbdir/$name.png");
344     my $exit  = $? >> 8;
345     exit ($exit) if $exit;
346     error ("unable to download $name.png")
347       unless (-f "$thumbdir/$name.png");
348   }
349
350   $app_dir =~ s@/+$@@s;
351   $app_dir .= "/Contents/Resources";
352   error ("$app_dir does not exist") unless (-d $app_dir);
353
354   system ("cp", "-p", "$thumbdir/$name.png", "$app_dir/thumbnail.png");
355   my $exit  = $? >> 8;
356   exit ($exit) if $exit;
357 }
358
359
360 sub enable_gc($) {
361   my ($app_dir) = @_;
362
363   return unless ($app_dir =~ m@\.saver/?$@s);
364   my ($dir, $name) = ($app_dir =~ m@^(.*)/([^/]+)\.saver$@s);
365   error ("unparsable: $app_dir") unless $name;
366   my $exe = "$app_dir/Contents/MacOS/$name";
367   my @cmd = ("$dir/enable_gc", $exe);
368   print STDERR "$progname: exec: " . join(' ', @cmd) . "\n"
369     if ($verbose > 1);
370   system (@cmd);
371   my $exit  = $? >> 8;
372   exit ($exit) if $exit;
373 }
374
375
376 sub fix_coretext($) {
377   my ($app_dir) = @_;
378
379   # In MacOS 10.8, they moved CoreText.framework from
380   # /System/Library/Frameworks/ApplicationServices.framework/Frameworks/
381   # to /System/Library/Frameworks/ which means that executables compiled
382   # on 10.8 and newer won't run on 10.7 and older because they can't find
383   # the library. Fortunately, 10.8 and later leave a symlink behind, so
384   # the old location still works. So we need our executables to contain
385   # an LC_LOAD_DYLIB pointing at the old directory instead of the new
386   # one.
387   # 
388   return if ($app_dir =~ m@-iphone@s);
389   my ($dir, $name) = ($app_dir =~ m@^(.*)/([^/]+)\.(app|saver)$@s);
390   error ("unparsable: $app_dir") unless $name;
391   my $exe = "$app_dir/Contents/MacOS/$name";
392
393   my $new = ("/System/Library/Frameworks/CoreText.framework/" .
394              "Versions/A/CoreText");
395   my $old = ("/System/Library/Frameworks/ApplicationServices.framework/" .
396              "Frameworks/CoreText.framework/Versions/A/CoreText");
397   my @cmd = ("install_name_tool", "-change", $new, $old, $exe);
398
399   print STDERR "$progname: exec: " . join(' ', @cmd) . "\n"
400     if ($verbose > 1);
401   system (@cmd);
402   my $exit  = $? >> 8;
403   exit ($exit) if $exit;
404 }
405
406
407 sub update($) {
408   my ($app_dir) = @_;
409
410   error ("$app_dir: no name") 
411     unless ($app_dir =~ m@/([^/.]+).(app|saver)/?$@x);
412   my $app_name = $1;
413
414   my ($filename, $plist) = read_info_plist ($app_dir);
415   my $oplist = $plist;
416
417   error ("$filename: no version number")
418     unless ($plist =~ m@<key>CFBundleShortVersionString</key>\s*
419                         <string>([^<>]+)</string>@sx);
420   my $vers = $1;
421   my ($ignore, $info_str) = update_saver_xml ($app_dir, $vers);
422
423   # No, don't do this -- the iOS version reads the XML file in a few
424   # different places, and most of those places don't understand gzip.
425
426   if ($app_name eq 'XScreenSaver') {
427     compress_all_xml_files ($app_dir);
428   } elsif (! defined($info_str)) {
429     print STDERR "$progname: $filename: no XML file\n" if ($verbose > 1);
430   } else {
431
432     $info_str =~ m@^([^\n]+)\n@s ||
433       error ("$filename: unparsable copyright");
434     my $copyright = "$1";
435     $copyright =~ s/\b\d{4}-(\d{4})\b/$1/;
436
437     # Lose the Wikipedia URLs.
438     $info_str =~ s@https?:.*?\b(wikipedia|mathworld)\b[^\s]+[ \t]*\n?@@gm;
439
440     $info_str =~ s/(\n\n)\n+/$1/gs;
441     $info_str =~ s/(^\s+|\s+$)//gs;
442     $plist = set_plist_key ($filename, $plist, 
443                             "NSHumanReadableCopyright", $copyright);
444     $plist = set_plist_key ($filename, $plist,
445                             "CFBundleLongVersionString",$copyright);
446     $plist = set_plist_key ($filename, $plist,
447                             "CFBundleGetInfoString",    $info_str);
448     $plist = set_plist_key ($filename, $plist,
449                             "CFBundleIdentifier",
450                             "org.jwz.xscreensaver." . $app_name);
451
452     if ($oplist eq $plist) {
453       print STDERR "$progname: $filename: unchanged\n" if ($verbose > 1);
454     } else {
455       $plist = convert_plist ($plist, 1);  # convert to binary plist
456       my $file_tmp = "$filename.tmp";
457       open (my $out, '>:raw', $file_tmp) || error ("$file_tmp: $!");
458       print $out $plist || error ("$file_tmp: $!");
459       close $out || error ("$file_tmp: $!");
460
461       if (!rename ("$file_tmp", "$filename")) {
462         unlink "$file_tmp";
463         error ("mv \"$file_tmp\" \"$filename\": $!");
464       }
465       print STDERR "$progname: wrote $filename\n" if ($verbose);
466     }
467   }
468
469   # MacOS 10.12: codesign says "resource fork, Finder information, or
470   # similar detritus not allowed" if any bundle has an Icon\r file.
471   # set_icon ($app_dir);
472
473   set_thumb ($app_dir);
474 # enable_gc ($app_dir);
475   fix_coretext ($app_dir)
476 }
477
478
479 sub error($) {
480   my ($err) = @_;
481   print STDERR "$progname: $err\n";
482   exit 1;
483 }
484
485 sub usage() {
486   print STDERR "usage: $progname [--verbose] program.app ...\n";
487   exit 1;
488 }
489
490 sub main() {
491
492   my @files = ();
493   while ($_ = $ARGV[0]) {
494     shift @ARGV;
495     if    (m/^--?verbose$/s)  { $verbose++; }
496     elsif (m/^-v+$/)          { $verbose += length($_)-1; }
497     elsif (m/^--?q(uiet)?$/s) { $verbose = 0; }
498     elsif (m/^-/s)            { usage(); }
499     else                      { push @files, $_; }
500   }
501   usage() unless ($#files >= 0);
502   foreach (@files) {
503     update ($_);
504   }
505 }
506
507 main();
508 exit 0;