Added --run option.
[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 sub do_dependencies
289 {
290     my $gotem = 1;
291
292     while ($gotem) {
293         $gotem = 0;
294         foreach (keys %files) {
295             my @deps = get_deps_for_file ($_);
296
297             # Add the deps as separate files.
298             foreach (@deps) {
299                 unless (exists $files{$_}) {
300                     $files{$_} = {
301                         name => $_,
302                         root => 0,
303                         dir => 0,
304                         exec => 0,
305                     };
306                     $gotem = 1;
307                 }
308             }
309         }
310     }
311 }
312
313 my $path_warning = 0;
314
315 sub get_deps_for_file
316 {
317     my $file = shift;
318     my @paths = split (/:/, $ENV{PATH});
319
320     # If we already fetched the dependencies for this file, just
321     # return that list now.
322     if (exists $files{$file}->{deps}) {
323         return @{$files{$file}->{deps}}
324     }
325
326     my @deps = ();
327
328     # We only know how to do this for *.exe and *.dll files.
329     if (m/\.exe$/i || m/\.dll$/i) {
330         my $cmd = "$objdump -p '$file' |
331                    grep 'DLL Name:' |
332                    grep -Eo '[-._[:alnum:]]+\.dll' |
333                    sort -u"; # XXX quoting
334         open DEPS, "$cmd |" or die "$cmd: $!";
335         foreach (<DEPS>) {
336             chomp; $_ = lc;
337
338             # Ignore Windows system DLL deps.
339             next if is_windows_system_dll ($_);
340
341             # Does the file exist on the path?
342             my $found = check_path ($_, @paths);
343             if ($found) {
344                 push @deps, $found;
345             } else {
346                 warn "MISSING DEPENDENCY: $_ (for $file)\n";
347                 unless ($path_warning) {
348                     warn "You may need to add the directory containing this file to your \$PATH\n";
349                     $path_warning = 1;
350                 }
351             }
352         }
353         close DEPS;
354
355         if ($verbose) {
356             if (@deps > 0) {
357                 print "dependencies found for binary $file:\n\t",
358                   join ("\n\t", @deps), "\n";
359             } else {
360                 print "no dependencies found for $file\n"
361             }
362         }
363
364     }
365
366     # Cache the list of dependencies so we can just return it
367     # immediately next time.
368     $files{$file}->{deps} = \@deps;
369     return @deps;
370 }
371
372 sub is_windows_system_dll
373 {
374     local $_ = shift;
375
376     $_ eq 'gdi32.dll' ||
377         $_ eq 'kernel32.dll' ||
378         $_ eq 'ole32.dll' ||
379         $_ eq 'mscoree.dll' ||
380         $_ eq 'msvcrt.dll' ||
381         $_ eq 'user32.dll'
382 }
383
384 # Decide how we will name the output files.  This removes the
385 # common prefix from filenames, if it can determine one.
386
387 sub install_names
388 {
389     my @names = keys %files;
390
391     # Determine if all the names share a common prefix.
392     my @namelens = map { length } @names;
393     my $shortest = min (@namelens);
394
395     my $prefixlen;
396     for ($prefixlen = $shortest; $prefixlen >= 0; --$prefixlen) {
397         my @ns = map { $_ = substr $_, 0, $prefixlen } @names;
398         last if same (@ns);
399     }
400
401     if ($verbose) { print "prefix length = $prefixlen\n" }
402
403     # Remove the prefix from each name and save the install directory
404     # and install filename separately.
405     foreach my $name (keys %files) {
406         my $install_as = substr $name, $prefixlen;
407
408         my ($install_dir, $install_name);
409
410         if ($install_as =~ m{(.*)/(.*)}) {
411             $install_dir = $1;
412             $install_name = $2;
413         } else {
414             $install_dir = ".";
415             $install_name = $install_as;
416         }
417
418         # Convert / in install_dir into backslashes.
419         $install_dir =~ s{/}{\\}g;
420
421         $files{$name}->{install_dir} = $install_dir;
422         $files{$name}->{install_name} = $install_name;
423     }
424 }
425
426 sub max
427 {
428     my $max = $_[0];
429     for (@_[1..$#_]) {
430         $max = $_ if $_ > $max;
431     }
432     $max
433 }
434
435 sub min
436 {
437     my $min = $_[0];
438     for (@_[1..$#_]) {
439         $min = $_ if $_ < $min;
440     }
441     $min
442 }
443
444 sub same
445 {
446     my  $s = $_[0];
447     for (@_[1..$#_]) {
448         return 0 if $_ ne $s;
449     }
450     1;
451 }
452
453 # Print the list of files.
454
455 sub print_files
456 {
457     print "Files:\n";
458     foreach (sort keys %files) {
459         print "\t$_";
460         if ($files{$_}->{root}) {
461             print " [root]";
462         }
463         if ($files{$_}->{dir}) {
464             print " [dir]";
465         }
466         print STDOUT ("\n\t  => ",
467                $files{$_}->{install_dir}, " \\ ", $files{$_}->{install_name},
468                "\n");
469     }
470     print "End of files.\n";
471 }
472
473 # Write the NSIS script.
474
475 sub write_script
476 {
477     my $io = shift;
478
479     print $io <<EOT;
480 #!Nsis Installer Command Script
481 #
482 # This is an NSIS Installer Command Script generated automatically
483 # by the Fedora nsiswrapper program.  For more information see:
484 #
485 #   http://fedoraproject.org/wiki/MinGW
486 #
487 # To build an installer from the script you would normally do:
488 #
489 #   makensis this_script
490 #
491 # which will generate the output file '$outfile' which is a Windows
492 # installer containing your program.
493
494 Name "$name"
495 OutFile "$outfile"
496 InstallDir "$installdir"
497 InstallDirRegKey $installdirregkey "Install_Dir"
498
499 ShowInstDetails hide
500 ShowUninstDetails hide
501
502 # Uncomment this to enable BZip2 compression, which results in
503 # slightly smaller files but uses more memory at install time.
504 #SetCompressor bzip2
505
506 XPStyle on
507
508 Page components
509 Page directory
510 Page instfiles
511
512 ComponentText "Select which optional components you want to install."
513
514 DirText "Please select the installation folder."
515
516 Section "$name"
517   SectionIn RO
518 EOT
519
520     # Set the output files.
521     my $olddir;
522     foreach (sort keys %files) {
523         if (!$olddir || $files{$_}->{install_dir} ne $olddir) {
524             # Moved into a new install directory.
525             my $dir = $files{$_}->{install_dir};
526             print $io "\n  SetOutPath \"\$INSTDIR\\$dir\"\n";
527             $olddir = $dir;
528         }
529
530         # If it's a directory, we copy it recursively, otherwise
531         # just copy the single file.
532         if ($files{$_}->{dir}) {
533             print $io "  File /r \"$_\"\n";
534         } else {
535             print $io "  File \"$_\"\n";
536         }
537     }
538
539     print $io <<EOT;
540 SectionEnd
541
542 Section "Start Menu Shortcuts"
543   CreateDirectory "\$SMPROGRAMS\\$name"
544   CreateShortCut "\$SMPROGRAMS\\$name\\Uninstall $name.lnk" "\$INSTDIR\\Uninstall $name.exe" "" "\$INSTDIR\\Uninstall $name.exe" 0
545 EOT
546
547     # Start menu entries for each executable.
548     foreach (sort keys %files) {
549         if ($files{$_}->{exec}) {
550             my $install_dir = $files{$_}->{install_dir};
551             my $install_name = $files{$_}->{install_name};
552             print $io "  CreateShortCut \"\$SMPROGRAMS\\$name\\$install_name.lnk\" \"\$INSTDIR\\$install_dir\\$install_name\" \"\" \"\$INSTDIR\\$install_dir\\$install_name\" 0\n";
553         }
554     }
555
556     print $io <<EOT;
557 SectionEnd
558
559 Section "Desktop Icons"
560 EOT
561
562     # Desktop icons for each executable.
563     foreach (sort keys %files) {
564         if ($files{$_}->{exec}) {
565             my $install_dir = $files{$_}->{install_dir};
566             my $install_name = $files{$_}->{install_name};
567             print $io "  CreateShortCut \"\$DESKTOP\\$install_name.lnk\" \"\$INSTDIR\\$install_dir\\$install_name\" \"\" \"\$INSTDIR\\$install_dir\\$install_name\" 0\n";
568         }
569     }
570
571     print $io <<EOT;
572 SectionEnd
573
574 Section "Uninstall"
575 EOT
576
577     # Remove desktop icons and menu shortcuts.
578     foreach (reverse sort keys %files) {
579         if ($files{$_}->{exec}) {
580             my $install_name = $files{$_}->{install_name};
581             print $io "  Delete /rebootok \"\$DESKTOP\\$install_name.lnk\"\n";
582             print $io "  Delete /rebootok \"\$SMPROGRAMS\\$name\\$install_name.lnk\"\n";
583         }
584     }
585     print $io "  Delete /rebootok \"\$SMPROGRAMS\\$name\\Uninstall $name.lnk\"\n\n";
586
587     # Remove remaining files.
588     $olddir = '';
589     foreach (reverse sort keys %files) {
590         if (!$olddir || $files{$_}->{install_dir} ne $olddir) {
591             # Moved into a new install directory, so delete the previous one.
592             print $io "  RMDir \"\$INSTDIR\\$olddir\"\n\n"
593                 if $olddir;
594             $olddir = $files{$_}->{install_dir};
595         }
596
597         # If it's a directory, we delete it recursively, otherwise
598         # just delete the single file.
599         my $install_dir = $files{$_}->{install_dir};
600         my $install_name = $files{$_}->{install_name};
601         if ($files{$_}->{dir}) {
602             print $io "  RMDir /r \"\$INSTDIR\\$install_dir\"\n\n";
603             $olddir = ''; # Don't double-delete directory.
604         } else {
605             print $io "  Delete /rebootok \"\$INSTDIR\\$install_dir\\$install_name\"\n";
606         }
607     }
608
609     print $io "  RMDir \"\$INSTDIR\\$olddir\"\n" if $olddir;
610
611     print $io <<EOT;
612   RMDir "\$INSTDIR"
613 SectionEnd
614
615 Section -post
616   WriteUninstaller "\$INSTDIR\\Uninstall $name.exe"
617 SectionEnd
618 EOT
619 }
620
621 # Run makensis on the named file.
622
623 sub run_makensis
624 {
625     my $filename = shift;
626
627     system ("makensis", $filename) == 0 or die "makensis: $?"
628 }
629
630 # Main program.
631
632 sub main
633 {
634     get_options ();
635     check_prereqs ();
636     print_config () if $verbose;
637     do_dependencies ();
638     install_names ();
639     print_files () if $verbose;
640     if ($run) {
641         my ($io, $filename) = tempfile ("nswXXXXXX", UNLINK => 1);
642         write_script ($io);
643         close $io;
644         run_makensis ($filename);
645     } else {
646         write_script (\*STDOUT);
647     }
648 }
649
650 main ()