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