Stop if there are missing deps.
[fedora-mingw.git] / nsiswrapper / nsiswrapper.pl
1 #!/usr/bin/perl -w
2 #
3 # NSISWrapper - a helper program for making Windows installers.
4 # Copyright (C) 2008 Red Hat Inc.
5 # Written by Richard W.M. Jones <rjones@redhat.com>,
6 # http://fedoraproject.org/wiki/MinGW
7 #
8 # This program is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or (at
11 # your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful, but
14 # WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16 # General Public License for more details.
17 #
18 # You should have received a copy of the GNU General Public License
19 # along with this program; if not, write to the Free Software
20 # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
21
22 use strict;
23 use Getopt::Long;
24 use Pod::Usage;
25 use File::Temp qw/tempfile/;
26
27 =pod
28
29 =head1 NAME
30
31 nsiswrapper - Helper program for making NSIS Windows installers
32
33 =head1 SYNOPSIS
34
35  nsiswrapper [options] [roots...]
36
37  nsiswrapper myprogram.exe anotherprog.exe docs/ > script.nsis
38
39  nsiswrapper --run myprogram.exe anotherprog.exe docs/
40
41 =head1 DESCRIPTION
42
43 nsiswrapper is a helper program for making it easier to create Windows
44 installers in a cross-compiler environment.  It still requires NSIS (a
45 Windows installer generator) but cuts out the tedium of writing the
46 NSIS command script, and can even invoke NSIS automatically to
47 generate a final Windows executable.
48
49 The general way to use it is to list out some files that you want
50 packaged.  For example:
51
52   nsiswrapper myprogram.exe
53
54 This will search for C<myprogram.exe> and any libraries (C<*.dll>)
55 that it depends upon, and then it will print out an NSIS script.
56
57 If you want to have it run C<makensis> as well (to automatically
58 create a Windows installer) then do:
59
60   nsiswrapper --run myprogram.exe
61
62 which will generate C<installer.exe> output file that contains
63 C<myprogram.exe> plus any dependencies.
64
65 You can list other files and directories that you want to have
66 contained in your installer.  For example:
67
68   nsiswrapper myprogram.exe anotherprog.exe docs/*.html
69
70 There are many other command line options which control aspects of the
71 NSIS command script (and hence, the final installer), such as:
72
73 =over 4
74
75 =item *
76
77 The name of the final installer.
78
79 =item *
80
81 Desktop shortcuts and menu items.
82
83 =item *
84
85 License files.
86
87 =back
88
89 It's a good idea to examine the NSIS command script, to check that
90 nsiswrapper is including all the right dependencies.
91
92 =head1 OPTIONS
93
94 =over 4
95
96 =item B<--help>
97
98 Print brief help message and exit.
99
100 =item B<--man>
101
102 Print the full manual page for the command and exit.
103
104 =item B<--verbose>
105
106 Print verbose messages while running.  If this is not given then we
107 try to operate quietly.
108
109 =item B<--run>
110
111 Normally this program just prints out the NSIS installer command
112 script.  However if you supply this option, then we run C<makensis>
113 and attempt to generate an actual Windows installer.
114
115 =item B<--name "Name">
116
117 Set the long name of the installer.
118
119 If not set, the script tries to invent a suitable name based on the
120 first root file given on the command line.
121
122 See also B<--outfile>.
123
124 =item B<--outfile myinstaller.exe>
125
126 Set the output filename for the installer.
127
128 If not set, this defaults to C<installer.exe>.
129
130 This is the same as the C<OutFile> option to NSIS.
131
132 =item B<--installdir 'C:\foo'>
133
134 Set the default Windows installation directory.  If not set, this
135 program will choose a suitable default based on the name.
136
137 In any case, the end user can override this when they run the
138 installer.
139
140 Note that since this string will contain backslashes, you should
141 single-quote it to protect it from the shell.
142
143 This is the same as the C<InstallDir> option to NSIS.
144
145 =item B<--installdirregkey 'HKLM SOFTWARE\FOO'>
146
147 Set the name of the registry key used to save the installation
148 directory.  This has two purposes: Firstly it is used to automagically
149 remember the installation directory between installs.  Secondly your
150 program can use this as one method to find its own installation
151 directory (there are other ways to do this).
152
153 The default is C<HKLM SOFTWARE\Name> where C<Name> is derived from the
154 name of the installer.
155
156 Note that since this string will contain backslashes and spaces, you
157 should single-quote it to protect it from the shell.
158
159 This is the same as the C<InstallDirRegKey> option to NSIS.
160
161 =back
162
163 =cut
164
165 my $objdump;
166 my %files;
167
168 my $help = '';
169 my $man = '';
170 my $verbose = '';
171 my $run = '';
172 my $name = '';
173 my $outfile = 'installer.exe';
174 my $installdir = '';
175 my $installdirregkey = '';
176
177 sub get_options
178 {
179     my $result = GetOptions (
180         "help|?" => \$help,
181         "man" => \$man,
182         "verbose" => \$verbose,
183         "run" => \$run,
184         "name=s" => \$name,
185         "outfile=s" => \$outfile,
186         "installdir=s" => \$installdir,
187         "installdirregkey=s" => \$installdirregkey,
188     );
189     die "nsiswrapper: use --help for information about command line options\n"
190         unless $result;
191
192     pod2usage(1) if $help;
193     pod2usage(-exitstatus => 0, -verbose => 2) if $man;
194
195     # Add the roots to the list of files.
196     die "nsiswrapper: no roots specified: use --help for more help\n"
197         if @ARGV == 0;
198     foreach (@ARGV) {
199         my $exec = 0;
200         $exec = 1 if m/\.exe$/i;
201
202         $files{$_} = {
203             name => $_,
204             root => 1,
205             dir => -d $_,
206             exec => $exec,
207         }
208     }
209
210     # Name not set?
211     if (!$name) {
212         # Massage the first root into a suitable package name.
213         $_ = $ARGV[0];
214         s{.*/}{};
215         s{\.\w\w\w\w?$}{};
216         $_ = ucfirst;
217         $name = $_;
218     }
219
220     # InstallDir not set?
221     if (!$installdir) {
222         $_ = $name;
223         s/\W/-/g;
224         $installdir = "c:\\$_"
225     }
226
227     # InstallDirRegKey not set?
228     if (!$installdirregkey) {
229         $_ = $name;
230         s/\W/-/g;
231         $installdirregkey = "HKLM SOFTWARE\\$_"
232     }
233 }
234
235 # Check prerequisites.
236
237 sub check_prereqs
238 {
239     my @paths = split (/:/, $ENV{PATH});
240
241     if (! $objdump) {
242         $objdump = check_path ("i686-pc-mingw32-objdump", @paths);
243         if (! $objdump || ! -x $objdump) {
244             die "i686-pc-mingw32-objdump: program not found on \$PATH\n"
245         }
246     }
247 }
248
249 # Check for the existance of a file at the given paths (not
250 # necessarily executable).  Returns the pathname of the file or
251 # undefined if not found.
252
253 sub check_path
254 {
255     local $_ = shift;
256     my @paths = @_;
257
258     my $found;
259     foreach my $dir (@paths) {
260         my $file = $dir . "/" . $_;
261         if (-f $file) {
262             $found = $file;
263             last;
264         }
265     }
266     $found
267 }
268
269 # Print configuration.
270
271 sub print_config
272 {
273     print "Configuration:\n";
274     print "\t\$PATH = $ENV{PATH}\n";
275     print "\t\$objdump = $objdump\n";
276     print "\t\$verbose = $verbose\n";
277     print "\t\$name = \"$name\"\n";
278     print "\t\$outfile = \"$outfile\"\n";
279     print "\t\$installdir = \"$installdir\"\n";
280     print "\t\$installdirregkey = \"$installdirregkey\"\n";
281     my @roots = keys %files;
282     print "\t\@roots = (", join (", ", @roots), ")\n";
283     print "End of configuration.\n";
284 }
285
286 # Starting at the roots, get the dependencies.
287
288 my $missing_deps = 0;
289
290 sub do_dependencies
291 {
292     my $gotem = 1;
293
294     while ($gotem) {
295         $gotem = 0;
296         foreach (keys %files) {
297             my @deps = get_deps_for_file ($_);
298
299             # Add the deps as separate files.
300             foreach (@deps) {
301                 unless (exists $files{$_}) {
302                     $files{$_} = {
303                         name => $_,
304                         root => 0,
305                         dir => 0,
306                         exec => 0,
307                     };
308                     $gotem = 1;
309                 }
310             }
311         }
312     }
313
314     die "please correct missing dependencies shown above\n"
315         if $missing_deps > 0;
316 }
317
318 my $path_warning = 0;
319
320 sub get_deps_for_file
321 {
322     my $file = shift;
323     my @paths = split (/:/, $ENV{PATH});
324
325     # If we already fetched the dependencies for this file, just
326     # return that list now.
327     if (exists $files{$file}->{deps}) {
328         return @{$files{$file}->{deps}}
329     }
330
331     my @deps = ();
332
333     # We only know how to do this for *.exe and *.dll files.
334     if (m/\.exe$/i || m/\.dll$/i) {
335         my $cmd = "$objdump -p '$file' |
336                    grep 'DLL Name:' |
337                    grep -Eo '[-._[:alnum:]]+\.dll' |
338                    sort -u"; # XXX quoting
339         open DEPS, "$cmd |" or die "$cmd: $!";
340         foreach (<DEPS>) {
341             chomp; $_ = lc;
342
343             # Ignore Windows system DLL deps.
344             next if is_windows_system_dll ($_);
345
346             # Does the file exist on the path?
347             my $found = check_path ($_, @paths);
348             if ($found) {
349                 push @deps, $found;
350             } else {
351                 warn "MISSING DEPENDENCY: $_ (for $file)\n";
352                 $missing_deps++;
353                 unless ($path_warning) {
354                     warn "You may need to add the directory containing this file to your \$PATH\n";
355                     $path_warning = 1;
356                 }
357             }
358         }
359         close DEPS;
360
361         if ($verbose) {
362             if (@deps > 0) {
363                 print "dependencies found for binary $file:\n\t",
364                   join ("\n\t", @deps), "\n";
365             } else {
366                 print "no dependencies found for $file\n"
367             }
368         }
369
370     }
371
372     # Cache the list of dependencies so we can just return it
373     # immediately next time.
374     $files{$file}->{deps} = \@deps;
375     return @deps;
376 }
377
378 sub is_windows_system_dll
379 {
380     local $_ = shift;
381
382     $_ eq 'gdi32.dll' ||
383         $_ eq 'kernel32.dll' ||
384         $_ eq 'ole32.dll' ||
385         $_ eq 'mscoree.dll' ||
386         $_ eq 'msvcrt.dll' ||
387         $_ eq 'user32.dll'
388 }
389
390 # Decide how we will name the output files.  This removes the
391 # common prefix from filenames, if it can determine one.
392
393 sub install_names
394 {
395     my @names = keys %files;
396
397     # Determine if all the names share a common prefix.
398     my @namelens = map { length } @names;
399     my $shortest = min (@namelens);
400
401     my $prefixlen;
402     for ($prefixlen = $shortest; $prefixlen >= 0; --$prefixlen) {
403         my @ns = map { $_ = substr $_, 0, $prefixlen } @names;
404         last if same (@ns);
405     }
406
407     if ($verbose) { print "prefix length = $prefixlen\n" }
408
409     # Remove the prefix from each name and save the install directory
410     # and install filename separately.
411     foreach my $name (keys %files) {
412         my $install_as = substr $name, $prefixlen;
413
414         my ($install_dir, $install_name);
415
416         if ($install_as =~ m{(.*)/(.*)}) {
417             $install_dir = $1;
418             $install_name = $2;
419         } else {
420             $install_dir = ".";
421             $install_name = $install_as;
422         }
423
424         # Convert / in install_dir into backslashes.
425         $install_dir =~ s{/}{\\}g;
426
427         $files{$name}->{install_dir} = $install_dir;
428         $files{$name}->{install_name} = $install_name;
429     }
430 }
431
432 sub max
433 {
434     my $max = $_[0];
435     for (@_[1..$#_]) {
436         $max = $_ if $_ > $max;
437     }
438     $max
439 }
440
441 sub min
442 {
443     my $min = $_[0];
444     for (@_[1..$#_]) {
445         $min = $_ if $_ < $min;
446     }
447     $min
448 }
449
450 sub same
451 {
452     my  $s = $_[0];
453     for (@_[1..$#_]) {
454         return 0 if $_ ne $s;
455     }
456     1;
457 }
458
459 # Print the list of files.
460
461 sub print_files
462 {
463     print "Files:\n";
464     foreach (sort keys %files) {
465         print "\t$_";
466         if ($files{$_}->{root}) {
467             print " [root]";
468         }
469         if ($files{$_}->{dir}) {
470             print " [dir]";
471         }
472         print STDOUT ("\n\t  => ",
473                $files{$_}->{install_dir}, " \\ ", $files{$_}->{install_name},
474                "\n");
475     }
476     print "End of files.\n";
477 }
478
479 # Write the NSIS script.
480
481 sub write_script
482 {
483     my $io = shift;
484
485     print $io <<EOT;
486 #!Nsis Installer Command Script
487 #
488 # This is an NSIS Installer Command Script generated automatically
489 # by the Fedora nsiswrapper program.  For more information see:
490 #
491 #   http://fedoraproject.org/wiki/MinGW
492 #
493 # To build an installer from the script you would normally do:
494 #
495 #   makensis this_script
496 #
497 # which will generate the output file '$outfile' which is a Windows
498 # installer containing your program.
499
500 Name "$name"
501 OutFile "$outfile"
502 InstallDir "$installdir"
503 InstallDirRegKey $installdirregkey "Install_Dir"
504
505 ShowInstDetails hide
506 ShowUninstDetails hide
507
508 # Uncomment this to enable BZip2 compression, which results in
509 # slightly smaller files but uses more memory at install time.
510 #SetCompressor bzip2
511
512 XPStyle on
513
514 Page components
515 Page directory
516 Page instfiles
517
518 ComponentText "Select which optional components you want to install."
519
520 DirText "Please select the installation folder."
521
522 Section "$name"
523   SectionIn RO
524 EOT
525
526     # Set the output files.
527     my $olddir;
528     foreach (sort keys %files) {
529         if (!$olddir || $files{$_}->{install_dir} ne $olddir) {
530             # Moved into a new install directory.
531             my $dir = $files{$_}->{install_dir};
532             print $io "\n  SetOutPath \"\$INSTDIR\\$dir\"\n";
533             $olddir = $dir;
534         }
535
536         # If it's a directory, we copy it recursively, otherwise
537         # just copy the single file.
538         if ($files{$_}->{dir}) {
539             print $io "  File /r \"$_\"\n";
540         } else {
541             print $io "  File \"$_\"\n";
542         }
543     }
544
545     print $io <<EOT;
546 SectionEnd
547
548 Section "Start Menu Shortcuts"
549   CreateDirectory "\$SMPROGRAMS\\$name"
550   CreateShortCut "\$SMPROGRAMS\\$name\\Uninstall $name.lnk" "\$INSTDIR\\Uninstall $name.exe" "" "\$INSTDIR\\Uninstall $name.exe" 0
551 EOT
552
553     # Start menu entries for each executable.
554     foreach (sort keys %files) {
555         if ($files{$_}->{exec}) {
556             my $install_dir = $files{$_}->{install_dir};
557             my $install_name = $files{$_}->{install_name};
558             print $io "  CreateShortCut \"\$SMPROGRAMS\\$name\\$install_name.lnk\" \"\$INSTDIR\\$install_dir\\$install_name\" \"\" \"\$INSTDIR\\$install_dir\\$install_name\" 0\n";
559         }
560     }
561
562     print $io <<EOT;
563 SectionEnd
564
565 Section "Desktop Icons"
566 EOT
567
568     # Desktop icons for each executable.
569     foreach (sort keys %files) {
570         if ($files{$_}->{exec}) {
571             my $install_dir = $files{$_}->{install_dir};
572             my $install_name = $files{$_}->{install_name};
573             print $io "  CreateShortCut \"\$DESKTOP\\$install_name.lnk\" \"\$INSTDIR\\$install_dir\\$install_name\" \"\" \"\$INSTDIR\\$install_dir\\$install_name\" 0\n";
574         }
575     }
576
577     print $io <<EOT;
578 SectionEnd
579
580 Section "Uninstall"
581 EOT
582
583     # Remove desktop icons and menu shortcuts.
584     foreach (reverse sort keys %files) {
585         if ($files{$_}->{exec}) {
586             my $install_name = $files{$_}->{install_name};
587             print $io "  Delete /rebootok \"\$DESKTOP\\$install_name.lnk\"\n";
588             print $io "  Delete /rebootok \"\$SMPROGRAMS\\$name\\$install_name.lnk\"\n";
589         }
590     }
591     print $io "  Delete /rebootok \"\$SMPROGRAMS\\$name\\Uninstall $name.lnk\"\n\n";
592
593     # Remove remaining files.
594     $olddir = '';
595     foreach (reverse sort keys %files) {
596         if (!$olddir || $files{$_}->{install_dir} ne $olddir) {
597             # Moved into a new install directory, so delete the previous one.
598             print $io "  RMDir \"\$INSTDIR\\$olddir\"\n\n"
599                 if $olddir;
600             $olddir = $files{$_}->{install_dir};
601         }
602
603         # If it's a directory, we delete it recursively, otherwise
604         # just delete the single file.
605         my $install_dir = $files{$_}->{install_dir};
606         my $install_name = $files{$_}->{install_name};
607         if ($files{$_}->{dir}) {
608             print $io "  RMDir /r \"\$INSTDIR\\$install_dir\"\n\n";
609             $olddir = ''; # Don't double-delete directory.
610         } else {
611             print $io "  Delete /rebootok \"\$INSTDIR\\$install_dir\\$install_name\"\n";
612         }
613     }
614
615     print $io "  RMDir \"\$INSTDIR\\$olddir\"\n" if $olddir;
616
617     print $io <<EOT;
618   RMDir "\$INSTDIR"
619 SectionEnd
620
621 Section -post
622   WriteUninstaller "\$INSTDIR\\Uninstall $name.exe"
623 SectionEnd
624 EOT
625 }
626
627 # Run makensis on the named file.
628
629 sub run_makensis
630 {
631     my $filename = shift;
632
633     system ("makensis", $filename) == 0 or die "makensis: $?"
634 }
635
636 # Main program.
637
638 sub main
639 {
640     get_options ();
641     check_prereqs ();
642     print_config () if $verbose;
643     do_dependencies ();
644     install_names ();
645     print_files () if $verbose;
646     if ($run) {
647         my ($io, $filename) = tempfile ("nswXXXXXX", UNLINK => 1);
648         write_script ($io);
649         close $io;
650         run_makensis ($filename);
651     } else {
652         write_script (\*STDOUT);
653     }
654 }
655
656 main ()